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
枚举支持 USD
、EUR
、INR
等值。通过 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 服务的调用:
- ✅ 利用 WSDL 自动生成客户端代码,避免手写 XML 解析逻辑
- ✅ 通过动态代理实现远程调用本地化,提升开发体验
- ✅ 结合 JUnit 快速验证接口行为,降低集成风险
虽然 REST 已成为主流,但在企业级系统、银行、运营商等领域,SOAP 依然广泛存在。掌握这套技能,关键时刻能少踩不少坑。
📌 替代方案提示:除了 JAX-WS RI,你也可以选择 Apache CXF、Axis2 或 Spring-WS 实现类似功能,各有优劣,按项目需求选型即可。