Invoking a SOAP Web Service in Java – 在Java中调用一个SOAP网络服务

最后修改: 2020年 4月 29日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll learn how to build a SOAP client in Java with JAX-WS RI in Java 8 and 11.

在本教程中,我们将学习如何在Java 8和11中使用JAX-WS RI构建一个SOAP客户端。

First, we’ll generate the client code using the wsimport utility and then test it using a JUnit.

首先,我们将使用wsimport工具生成客户端代码,然后使用JUnit进行测试。

For those starting out, our introduction to JAX-WS provides great background on the subject.

对于那些刚刚起步的人来说,我们的JAX-WS介绍提供了很好的背景。

2. The Web Service

2.网络服务

Before we start building a client, we need a server. In this case, we need a server exposing a JAX-WS web service.

在我们开始建立一个客户端之前,我们需要一个服务器。在这种情况下,我们需要一个暴露于JAX-WS网络服务的服务器。

For the purpose of this tutorial, we’ll use a web service that will fetch us a country’s data, given its name.

在本教程中,我们将使用一个网络服务,它将为我们获取一个国家的数据,给定其名称。

2.1. Summary of Implementation

2.1.实施情况总结

Since we’re focusing on building the client, we won’t get into the implementation details of our service.

由于我们专注于建立客户端,所以我们不会去讨论我们服务的实施细节。

Let’s say that an interface CountryService is used to expose the web service to the external world. To keep things simple, we’ll build and deploy the web service using the javax.xml.ws.Endpoint API in our class CountryServicePublisher.

比方说,一个接口CountryService被用来将Web服务暴露给外部世界。为了保持简单,我们将使用javax.xml.ws.Endpoint API在我们的类CountryServicePublisher中构建和部署网络服务。

We’ll run CountryServicePublisher as a Java application to publish an endpoint that’ll accept the incoming requests. In other words, this will be our server.

我们将运行CountryServicePublisher作为一个Java应用程序,发布一个接受传入请求的端点。换句话说,这将是我们的服务器。

After starting the server, hitting the URL http://localhost:8888/ws/country?wsdl gives us the web service description file. The WSDL acts as a guide to understand the service’s offerings and generate implementation code for the client.

在启动服务器后,点击URL http://localhost:8888/ws/country?wsdl,我们就可以得到网络服务描述文件。WSDL是理解服务内容的指南,并为客户端生成实施代码。

2.2. The Web Services Description Language

2.2.网络服务描述语言

Let’s look at our web service’s WSDL, country:

让我们看看我们的网络服务的WSDL,country

<?xml version="1.0" encoding="UTF-8"?>
<definitions <!-- namespace declarations -->
    targetNamespace="http://server.ws.soap.baeldung.com/" name="CountryServiceImplService">
    <types>
        <xsd:schema>
            <xsd:import namespace="http://server.ws.soap.baeldung.com/" 
              schemaLocation="http://localhost:8888/ws/country?xsd=1"></xsd:import>
        </xsd:schema>
    </types>
    <message name="findByName">
        <part name="arg0" type="xsd:string"></part>
    </message>
    <message name="findByNameResponse">
        <part name="return" type="tns:country"></part>
    </message>
    <portType name="CountryService">
        <operation name="findByName">
            <input wsam:Action="http://server.ws.soap.baeldung.com/CountryService/findByNameRequest" 
              message="tns:findByName"></input>
            <output wsam:Action="http://server.ws.soap.baeldung.com/CountryService/findByNameResponse" 
              message="tns:findByNameResponse"></output>
        </operation>
    </portType>
    <binding name="CountryServiceImplPortBinding" type="tns:CountryService">
        <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="rpc"></soap:binding>
        <operation name="findByName">
            <soap:operation soapAction=""></soap:operation>
            <input>
                <soap:body use="literal" namespace="http://server.ws.soap.baeldung.com/"></soap:body>
            </input>
            <output>
                <soap:body use="literal" namespace="http://server.ws.soap.baeldung.com/"></soap:body>
            </output>
        </operation>
    </binding>
    <service name="CountryServiceImplService">
        <port name="CountryServiceImplPort" binding="tns:CountryServiceImplPortBinding">
            <soap:address location="http://localhost:8888/ws/country"></soap:address>
        </port>
    </service>
</definitions>

In a nutshell, this is the useful information it provides:

简而言之,这是它提供的有用信息。

  • We can invoke the method findByName with a string argument.
  • In response, the service will return us a custom type of country.
  • Types are defined in an xsd schema generated at the location http://localhost:8888/ws/country?xsd=1:
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema <!-- namespace declarations -->
    targetNamespace="http://server.ws.soap.baeldung.com/">
    <xs:complexType name="country">
        <xs:sequence>
            <xs:element name="capital" type="xs:string" minOccurs="0"></xs:element>
            <xs:element name="currency" type="tns:currency" minOccurs="0"></xs:element>
            <xs:element name="name" type="xs:string" minOccurs="0"></xs:element>
            <xs:element name="population" type="xs:int"></xs:element>
        </xs:sequence>
    </xs:complexType>
    <xs:simpleType name="currency">
        <xs:restriction base="xs:string">
            <xs:enumeration value="EUR"></xs:enumeration>
            <xs:enumeration value="INR"></xs:enumeration>
            <xs:enumeration value="USD"></xs:enumeration>
        </xs:restriction>
    </xs:simpleType>
</xs:schema>

That’s all we need to implement a client.

这就是我们实现一个客户所需要的一切。

Let’s see how in the next section.

让我们在下一节中看看如何。

3. Using wsimport to Generate Client Code

3.使用wsimport来生成客户端代码

3.1. For JDK 8

3.1. 对于JDK 8

First, let’s see how to generate client code using JDK 8.

首先,让我们看看如何使用JDK 8生成客户端代码。

To begin with, let’s add a plugin to our pom.xml to use this tool via Maven:

首先,让我们在pom.xml中添加一个插件,通过Maven使用这个工具。

<plugin> 
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>jaxws-maven-plugin</artifactId>
    <version>2.6</version>
    <executions> 
        <execution> 
            <id>wsimport-from-jdk</id>
            <goals>
                <goal>wsimport</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <wsdlUrls>
            <wsdlUrl>http://localhost:8888/ws/country?wsdl</wsdlUrl> 
        </wsdlUrls>
        <keep>true</keep> 
        <packageName>com.baeldung.soap.ws.client.generated</packageName> 
        <sourceDestDir>src/main/java</sourceDestDir>
    </configuration>
</plugin>

Second, let’s execute this plugin:

第二,我们来执行这个插件。

mvn clean jaxws:wsimport

That’s all! The above command will generate code in the specified package com.baeldung.soap.ws.client.generated inside the sourceDestDir we provided in the plugin configuration.

这就是全部!上述命令将在我们在插件配置中提供的com.baeldung.soap.ws.client.generated内的指定包中生成代码。

Another way to achieve the same would be to use the wsimport utility. It comes out of the box with the standard JDK 8 distribution and can be found under JAVA_HOME/bin directory.

另一种实现同样目的的方法是使用wsimport工具。它随标准JDK 8发行版一起开箱,可以在JAVA_HOME/bin目录下找到。

To generate client code using wsimport, we can navigate to the project’s root and run this command:

要使用wsimport生成客户端代码,我们可以导航到项目的根目录并运行这个命令。

JAVA_HOME/bin/wsimport -s src/main/java/ -keep -p com.baeldung.soap.ws.client.generated "http://localhost:8888/ws/country?wsdl"

It’s important to bear in mind that the service endpoint should be available in order to successfully execute the plugin or command.

重要的是要记住,服务端点应该是可用的,以便成功执行插件或命令。

3.2. For JDK 11

3.2. 对于JDK 11

Starting JDK 11, wsimport was removed as part of the JDK and no longer comes out of the box with the standard distribution.

从 JDK 11 开始,wsimport作为 JDK 的一部分被移除,并且不再随标准发行版一起开箱。

However, it was open-sourced to the Eclipse foundation.

然而,它被开源给了Eclipse基金会。

In order to use wsimport to generate client code for Java 11 and above, we need to add the jakarta.xml.ws-api, jaxws-rt and jaxws-ri dependencies in addition to the jaxws-maven-plugin:

为了使用wsimport来生成Java 11及以上版本的客户端代码,我们需要添加jakarta.xml.ws-apijaxws-rtjaxws-ri依赖,此外还有jaxws-maven-插件

<dependencies>
    <dependency>
        <groupId>jakarta.xml.ws</groupId
        <artifactId>jakarta.xml.ws-api</artifactId
        <version>3.0.0</version>
    </dependency>
    <dependency>
        <groupId>com.sun.xml.ws</groupId>
        <artifactId>jaxws-rt</artifactId>
        <version>3.0.0</version
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.sun.xml.ws</groupId>
        <artifactId>jaxws-ri</artifactId>
        <version>2.3.1</version
        <type>pom</type>
    </dependency>
</dependencies>
<build>
    <plugins>        
        <plugin>
            <groupId>com.sun.xml.ws</groupId>
            <artifactId>jaxws-maven-plugin</artifactId>
            <version>2.3.2</version>
            <configuration>
                <wsdlUrls>
                    <wsdlUrl>http://localhost:8888/ws/country?wsdl</wsdlUrl>
                </wsdlUrls>
                <keep>true</keep>
                <packageName>com.baeldung.soap.ws.client.generated</packageName>
                <sourceDestDir>src/main/java</sourceDestDir>
            </configuration>
        </plugin>
    </plugins>
</build>

Now, to generate the client code in the package com.baeldung.soap.ws.client.generated, we’ll need the same Maven command as before:

现在,为了生成com.baeldung.soap.ws.client.generated包中的客户端代码,我们需要使用与之前相同的Maven命令。

mvn clean jaxws:wsimport

Next, let’s look at the generated artifacts that are the same for both the Java versions.

接下来,让我们看看所生成的工件,这两个Java版本的工件是相同的。

3.3. Generated POJOs

3.3.生成的POJO

Based on the xsd we saw earlier, the tool will generate a file named Country.java:

基于我们之前看到的xsd,该工具将生成一个名为Country.java的文件。

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "country", propOrder = { "capital", "currency", "name", "population" })
public class Country {
    protected String capital;
    @XmlSchemaType(name = "string")
    protected Currency currency;
    protected String name;
    protected int population;
    // standard getters and setters
}

As we can see, the generated class is decorated with JAXB annotations for marshalling and unmarshalling the object to and from XML.

我们可以看到,生成的类用JAXB注解进行了装饰,用于将对象编入和从XML中解开。

Also, it generates a Currency enum:

此外,它还生成了一个Currency枚举。

@XmlType(name = "currency")
@XmlEnum
public enum Currency {
    EUR, INR, USD;
    public String value() {
        return name();
    }
    public static Currency fromValue(String v) {
        return valueOf(v);
    }
}

3.4. CountryService

3.4.国家服务

The second generated artifact is an interface that acts as a proxy to the actual web service.

第二个生成的工件是一个接口,作为实际网络服务的代理。

The interface CountryService declares the same method as our server, findByName:

接口CountryService声明了与我们的服务器相同的方法,findByName

@WebService(name = "CountryService", targetNamespace = "http://server.ws.soap.baeldung.com/")
@SOAPBinding(style = SOAPBinding.Style.RPC)
@XmlSeeAlso({ ObjectFactory.class })
public interface CountryService {
    @WebMethod
    @WebResult(partName = "return")
    @Action(input = "http://server.ws.soap.baeldung.com/CountryService/findByNameRequest", 
      output = "http://server.ws.soap.baeldung.com/CountryService/findByNameResponse")
    public Country findByName(@WebParam(name = "arg0", partName = "arg0") String arg0);
}

Notably, the interface is marked as a javax.jws.WebService, with a SOAPBinding.Style as RPC as defined by the service’s WSDL.

值得注意的是,该接口被标记为javax.jws.WebService,具有SOAPBinding.Style作为RPC,正如该服务的WSDL所定义。

The method findByName is annotated to declare that it’s a javax.jws.WebMethod, with its expected input and output parameter types.

方法findByName被注解为声明它是一个javax.jws.WebMethod,并有其预期的输入和输出参数类型。

3.5. CountryServiceImplService

3.5.CountryServiceImplService

Our next generated class, CountryServiceImplService, extends javax.xml.ws.Service.

我们下一个生成的类,CountryServiceImplService,扩展了javax.xml.ws.Service

Its annotation WebServiceClient denotes that it is the client view of a service:

它的注释WebServiceClient表示它是一个服务的客户端视图。

@WebServiceClient(name = "CountryServiceImplService", 
  targetNamespace = "http://server.ws.soap.baeldung.com/", 
  wsdlLocation = "http://localhost:8888/ws/country?wsdl")
public class CountryServiceImplService extends Service {

    private final static URL COUNTRYSERVICEIMPLSERVICE_WSDL_LOCATION;
    private final static WebServiceException COUNTRYSERVICEIMPLSERVICE_EXCEPTION;
    private final static QName COUNTRYSERVICEIMPLSERVICE_QNAME = 
      new QName("http://server.ws.soap.baeldung.com/", "CountryServiceImplService");

    static {
        URL url = null;
        WebServiceException e = null;
        try {
            url = new URL("http://localhost:8888/ws/country?wsdl");
        } catch (MalformedURLException ex) {
            e = new WebServiceException(ex);
        }
        COUNTRYSERVICEIMPLSERVICE_WSDL_LOCATION = url;
        COUNTRYSERVICEIMPLSERVICE_EXCEPTION = e;
    }

    public CountryServiceImplService() {
        super(__getWsdlLocation(), COUNTRYSERVICEIMPLSERVICE_QNAME);
    }

    // other constructors 

    @WebEndpoint(name = "CountryServiceImplPort")
    public CountryService getCountryServiceImplPort() {
        return super.getPort(new QName("http://server.ws.soap.baeldung.com/", "CountryServiceImplPort"), 
          CountryService.class);
    }

    private static URL __getWsdlLocation() {
        if (COUNTRYSERVICEIMPLSERVICE_EXCEPTION != null) {
            throw COUNTRYSERVICEIMPLSERVICE_EXCEPTION;
        }
        return COUNTRYSERVICEIMPLSERVICE_WSDL_LOCATION;
    }

}

The important method to note here is getCountryServiceImplPort. Given a qualified name of the service endpoint, or QName, and the dynamic proxy’s service endpoint interface name, it returns a proxy instance.

这里需要注意的重要方法是getCountryServiceImplPort。给出服务端点的限定名称,即QName,以及动态代理的服务端点接口名称,它返回一个代理实例。

To invoke the web service, we need to use this proxy, as we’ll see shortly.

为了调用网络服务,我们需要使用这个代理,我们很快就会看到。

Using a proxy makes it seem as if we are calling a service locally, abstracting away the intricacies of remote invocation.

使用代理使我们看起来就像在本地调用一个服务,抽象出远程调用的复杂性。

4. Testing the Client

4.测试客户

Next, we’ll write a JUnit test to connect to the web service using the generated client code.

接下来,我们将编写一个JUnit测试,使用生成的客户端代码连接到网络服务。

Before we can do that, we need to get the service’s proxy instance at the client end:

在这样做之前,我们需要在客户端获得服务的代理实例。

@BeforeClass
public static void setup() {
    CountryServiceImplService service = new CountryServiceImplService();
    CountryService countryService = service.getCountryServiceImplPort();
}

For more advanced scenarios such as enabling or disabling a WebServiceFeature, we can use other generated constructors for CountryServiceImplService.

对于更高级的情况,如启用或禁用WebServiceFeature,我们可以使用CountryServiceImplService的其他生成的构造函数。

Now let’s look at some tests:

现在我们来看看一些测试。

@Test
public void givenCountryService_whenCountryIndia_thenCapitalIsNewDelhi() {
    assertEquals("New Delhi", countryService.findByName("India").getCapital());
}

@Test
public void givenCountryService_whenCountryFrance_thenPopulationCorrect() {
    assertEquals(66710000, countryService.findByName("France").getPopulation());
}

@Test
public void givenCountryService_whenCountryUSA_thenCurrencyUSD() {
    assertEquals(Currency.USD, countryService.findByName("USA").getCurrency());
}

As we can see, invoking the remote service’s methods became as simple as calling methods locally. The proxy’s findByName method returned a Country instance matching the name we provided. Then we used various getters of the POJO to assert expected values.

正如我们所看到的,调用远程服务的方法变得和调用本地方法一样简单。代理的findByName方法返回一个与我们提供的name匹配的Country实例。然后,我们使用POJO的各种获取器来断言预期的值。

5. Conclusion

5.总结

In this article, we saw how to invoke a SOAP web service in Java using JAX-WS RI and the wsimport utility for Java 8 as well as 11.

在这篇文章中,我们看到了如何使用JAX-WS RI和wsimport工具在Java中调用一个SOAP Web服务,适用于Java 8以及11。

Alternatively, we can use other JAX-WS implementations such as Apache CXF, Apache Axis2 and Spring to do the same.

另外,我们还可以使用其他的JAX-WS实现,如Apache CXF、Apache Axis2和Spring来做同样的事情。

As always, the source code is available over on GitHub for both JDK 8 and JDK 11 versions.

一如既往,GitHub上提供了JDK 8JDK 11两个版本的源代码。