1. 概述

本文将演示如何在 Java 11 环境下,使用 JAX-WS RI 构建一个 SOAP 客户端。

整个过程分为两步:

✅ 使用 wsimport 工具根据 WSDL 自动生成客户端代码
✅ 通过 JUnit 编写测试用例验证调用逻辑

如果你对 JAX-WS 基础还不熟悉,建议先阅读我们的 JAX-WS 入门指南,有助于理解后续内容。


2. Web 服务简介

在编写客户端之前,我们需要一个可用的 SOAP 服务作为目标。本文使用的示例服务提供根据国家名称查询国家详情的功能。

2.1. 服务实现概要

该服务通过 CountryService 接口暴露,并使用 javax.xml.ws.Endpoint API 发布。只需运行 CountryServicePublisher 类即可启动本地服务。

服务启动后,WSDL 地址如下:

http://localhost:8888/ws/country?wsdl

这个 WSDL 就是客户端与服务端之间的契约(Contract),定义了接口方法和数据结构,是我们生成客户端代码的依据。

2.2. 理解 WSDL:Web Services Description Language

以下是 country 服务的 WSDL 片段:

<?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>

WSDL 定义了服务的结构和操作。本例中仅有一个操作 findByName,接收一个字符串参数(国家名),返回一个 country 对象。

country 对象包含以下字段:

  • name:国家名称
  • capital:首都
  • currency:货币(枚举类型)
  • population:人口

其中 currency 枚举支持 USDEURINR 等值。通过 WSDL + XSD,客户端可以完全了解通信所需的数据格式。

2.3. 数据模型定义(XSD)

country 类型及相关结构定义在如下 XSD 中:

http://localhost:8888/ws/country?xsd=1

XSD 内容如下:

<?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>

有了 WSDL 和 XSD,我们就可以开始生成客户端代码了。


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

3.1. 添加依赖并生成代码

要使用 wsimport,首先需要在 pom.xml 中引入 JAX-WS 相关依赖:

<dependencies>
    <dependency>
        <groupId>jakarta.xml.ws</groupId>
        <artifactId>jakarta.xml.ws-api</artifactId>
        <version>3.0.1</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>3.0.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>

⚠️ 注意:虽然 jaxws-ri 版本是 2.3.1,但它在 Java 11 下仍可正常工作。若使用更高版本需注意模块兼容性问题。

执行以下命令生成客户端代码:

mvn clean jaxws:wsimport

生成的代码会放在 src/main/java/com/baeldung/soap/ws/client/generated 目录下。

3.2. 生成的 POJO 类

wsimport 根据 XSD 自动生成了 Country.java 类,使用 JAXB 注解处理 XML 序列化:

@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
}

@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.3. CountryService 接口

CountryService 是远程服务的本地代理接口,定义了 findByName 方法:

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

@WebService 表示这是一个 Web Service 接口
@SOAPBinding(style = RPC) 对应 WSDL 中的 RPC 风格绑定
@WebMethod 标记可远程调用的方法

3.4. CountryServiceImplService 类

这是服务客户端的入口类,继承自 javax.xml.ws.Service

@WebServiceClient(name = "CountryServiceImplService", 
  wsdlLocation = "http://localhost:8888/ws/country?wsdl")
public class CountryServiceImplService extends Service {
    public CountryServiceImplService() {
        super(QName("http://server.ws.soap.baeldung.com/", "CountryServiceImplService"));
    }
    @WebEndpoint(name = "CountryServiceImplPort")
    public CountryService getCountryServiceImplPort() {
        return super.getPort(CountryService.class);
    }
}

关键点:

  • @WebServiceClient 表明这是一个客户端服务类
  • getCountryServiceImplPort() 返回 CountryService 的动态代理实例
  • 所有远程调用都被底层框架封装,开发者只需像调用本地方法一样使用

4. 测试客户端

接下来我们用 JUnit 验证客户端是否能正确调用远程接口。

4.1. 初始化服务代理

在测试前先获取服务代理:

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

这里 countryService 是一个动态代理,后续所有方法调用都会被转发为 SOAP 请求。

4.2. 编写测试用例

测试逻辑非常直观:

@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());
}

✅ 调用方式和本地方法无异
✅ 返回值是强类型的 Country 对象,可直接断言字段
✅ 异常处理由框架自动完成(如网络超时、服务不可达等)

简单粗暴,但非常有效。


5. 总结

本文演示了如何在 Java 11 中使用 JAX-WS RI + wsimport 实现对 SOAP 服务的调用:

  1. ✅ 利用 WSDL 自动生成客户端代码,避免手写 XML 解析逻辑
  2. ✅ 通过动态代理实现远程调用本地化,提升开发体验
  3. ✅ 结合 JUnit 快速验证接口行为,降低集成风险

虽然 REST 已成为主流,但在企业级系统、银行、运营商等领域,SOAP 依然广泛存在。掌握这套技能,关键时刻能少踩不少坑。

📌 替代方案提示:除了 JAX-WS RI,你也可以选择 Apache CXF、Axis2 或 Spring-WS 实现类似功能,各有优劣,按项目需求选型即可。


原始标题:Invoking a SOAP Web Service in Java | Baeldung