
一、场景与痛点
银行核心、信贷、支付、政务审批、ERP 集成这些地方,SOAP 基本绕不过去。不是它多先进,而是契约已经定死了,调用方可能还是 Axis1、JAX-WS RI 或 .NET ASMX,动一个字段都要拉一堆人开会。典型情况大概是这样:
- 契约非常硬。WSDL 加 XSD 摆在那里,字段类型、命名空间、元素顺序都不能随便变。
- 安全要求不低。WS-Security 用户名令牌、签名、加密、时间戳、证书轮换,客户往往都有明确要求。
- 附件少不了。发票、影像、报表经常走 MTOM 或 SwA,不能直接塞 Base64 大字段。
- 协议版本比较乱。SOAP 1.1/1.2、Document/Literal、RPC/Encoded、wrapped/bare 都可能碰到。
- 遗留系统锁死。老系统不一定不能改,但升级成本通常高到没人愿意批。
Spring Boot 服务跟这些系统对接时,常见坑有:
- 版本冲突。Spring Boot 3 全面切到 Jakarta EE,老系统还大量用
javax.*,CXF 4 上来之后包名得同步迁。 - WSDL 和代码对不上。手工改 stub 或服务端注解,短期能跑,长期契约漂移。
- 安全策略散。每个遗留系统一套证书、密码策略,客户端配置东一块西一块。
- 报文解析出问题。XXE、超大 XML、字符集不一致,轻则乱码,重则 OOM。
- 联调回归靠人。SoapUI、Mock、契约回归没自动化,上线前靠手工点。
- 跟 Spring Boot 不熟。CXF 的 Bus、Interceptor、Endpoint 怎么和自动配置、Servlet 容器配合,容易踩坑。
目标不复杂:Spring Boot 里能发新的 SOAP 服务,兼容旧调用方;也能调遗留 SOAP 服务,并且把安全、日志、异常、测试这些事收口。下面按这个路子来。
二、CXF 架构概览
CXF 的东西不用全背,先抓住几个核心:
- Bus:运行时总线。扩展、Factory、Interceptor、Feature、Binding、Transport 都挂在上面,可以理解成 CXF 自己的应用上下文。
- Frontend:前端模型。最常用 JAX-WS,也支持 Simple Frontend 和 JAX-RS。
- Endpoint、Server、Client:服务端靠
Endpoint暴露,客户端靠Client调。Spring 里常见的是EndpointImpl、JaxWsProxyFactoryBean。 - Interceptor Chain:拦截器链是 CXF 的核心。消息按阶段走,
RECEIVE、PRE_STREAM、UNMARSHAL、PRE_INVOKE、POST_INVOKE、MARSHAL、SEND这些阶段都能插逻辑。日志、安全、审计、编码转换基本都在这里做。 - Data Binding:默认 JAXB,负责 Java 对象和 XML 互转。也支持 Aegis、XMLBeans。
- Transport:HTTP、JMS、Local 等。Spring Boot 里一般就是内嵌 Tomcat/Jetty/Undertow 扛 HTTP。
WSDL 优先还是代码优先?
- WSDL 优先:先有 WSDL/XSD,再用
cxf-codegen-plugin生成接口和客户端。遗留系统适配、对外发布、契约严格的项目走这条路。 - 代码优先:先写 Java 和 JAX-WS 注解,CXF 自动生成 WSDL。新服务快速迭代可以用。
- 实际选择:对接遗留系统、对外暴露服务,优先 WSDL 优先。内部新服务图省事可以代码优先,但只要对外暴露,就尽快把 WSDL 固化下来。
在 Spring Boot 里,cxf-spring-boot-starter-jaxws 会自动配置 CXFServlet、SpringBus、Bus 和 Servlet 映射。大部分时候你只需要关心 Endpoint 和客户端 Bean。
三、服务发布:在 Spring Boot 中暴露 WSDL
3.1 依赖与版本选择
xml
<!-- Spring Boot 3.x + Jakarta -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-spring-boot-starter-jaxws</artifactId>
<version>4.0.4</version>
</dependency>
<!-- Spring Boot 2.7 + javax,适配遗留系统 -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-spring-boot-starter-jaxws</artifactId>
<version>3.5.7</version>
</dependency>
如果遗留系统还跑在 Java 8/11 上,而且依赖 javax.jws,那就选 Spring Boot 2.7 + CXF 3.5.x。新系统直接上 Spring Boot 3 + CXF 4。两边 API 差别不算大,主要是包名从 javax.* 变成 jakarta.*,但这个变化很要命,迁移时别漏。
配置可以简单点:
yaml
cxf:
path: /services
servlet:
load-on-startup: 1
发布后的 WSDL 地址类似:http://localhost:8080/services/order/v1?wsdl。
3.2 接口定义
java
@WebService(
targetNamespace = "http://example.com/order/v1",
name = "OrderService",
serviceName = "OrderService",
portName = "OrderServicePort"
)
public interface OrderService {
@WebMethod(operationName = "queryOrder")
@WebResult(name = "OrderResponse", targetNamespace = "http://example.com/order/v1")
OrderResponse queryOrder(
@WebParam(name = "request", targetNamespace = "http://example.com/order/v1")
OrderQuery request
) throws OrderFault;
}
实现类:
java
@WebService(
endpointInterface = "com.example.order.OrderService",
targetNamespace = "http://example.com/order/v1"
)
@Service
public class OrderServiceImpl implements OrderService {
@Override
public OrderResponse queryOrder(OrderQuery request) throws OrderFault {
// 业务逻辑
}
}
3.3 Endpoint 配置
java
@Configuration
public class CxfConfig {
@Bean
public SpringBus cxfBus() {
SpringBus bus = new SpringBus();
// 注册日志、安全、审计等 Feature 和 Interceptor
return bus;
}
@Bean
public Endpoint orderEndpoint(SpringBus bus, OrderServiceImpl orderService) {
EndpointImpl endpoint = new EndpointImpl(bus, orderService);
endpoint.publish("/order/v1");
return endpoint;
}
}
服务端可以同时挂多个 Endpoint,按路径隔离版本。别把所有接口都塞到一个路径下,后面排查和灰度都很麻烦。
3.4 WSDL 暴露与版本管理
- URL 版本化:
/order/v1、/order/v2。简单直观,Nginx 也容易配。 - 命名空间版本化:
http://example.com/order/v1、.../v2。适合同一地址长期兼容。 - 兼容性规则:只新增操作、新增可选元素。不要删操作,不要改已有元素类型和命名空间,不要调元素顺序。
- WSDL 固化:把 WSDL 纳入版本控制,CI 里比较新旧 WSDL,防止契约漂移。
- 旧地址保持:旧地址不能变,就用 Nginx 反代到新版本路径。
四、客户端调用
4.1 静态 Stub 生成(推荐)
用 cxf-codegen-plugin 从 WSDL 生成客户端代码:
xml
<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<version>4.0.4</version>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<configuration>
<wsdlOptions>
<wsdlOption>
<wsdl>src/main/resources/wsdl/order.wsdl</wsdl>
<extraargs>
<extraarg>-client</extraarg>
<extraarg>-p</extraarg>
<extraarg>com.example.order.client</extraarg>
</extraargs>
</wsdlOption>
</wsdlOptions>
</configuration>
<goals>
<goal>wsdl2java</goal>
</goals>
</execution>
</executions>
</plugin>
生成 OrderService 和 OrderServiceService,类型安全,生产环境优先考虑这种。
4.2 JAX-WS Proxy
java
@Bean
public OrderService orderService() {
JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();
factory.setServiceClass(OrderService.class);
factory.setAddress("https://legacy.example.com/order/v1");
OrderService port = (OrderService) factory.create();
Client client = ClientProxy.getClient(port);
HTTPConduit conduit = (HTTPConduit) client.getConduit();
HTTPClientPolicy policy = new HTTPClientPolicy();
policy.setConnectionTimeout(3000);
policy.setReceiveTimeout(10000);
conduit.setClient(policy);
return port;
}
代理实例一般可以当单例复用,但 BindingProvider 的 request context 别在请求线程里乱改,后面会提。
4.3 动态客户端
java
JaxWsDynamicClientFactory factory = JaxWsDynamicClientFactory.newInstance();
Client client = factory.createClient("https://legacy.example.com/order/v1?wsdl");
Object[] result = client.invoke("queryOrder", request);
动态客户端适合 WSDL 经常变、调用次数不多的场景。但它依赖 wsdl4j、编译工具,运行时解析 WSDL 性能差,而且类型不安全。生产能生成静态 Stub 就别用动态。
4.4 拦截器
- 日志:CXF 3.2+ 用
LoggingFeature,旧版用LoggingInInterceptor/LoggingOutInterceptor。 - 自定义:继承
AbstractPhaseInterceptor<Message>或AbstractSoapInterceptor,在RECEIVE、PRE_INVOKE这些阶段塞逻辑。 - 关联 ID:客户端在
Out拦截器写SOAPHeader,服务端在In拦截器读出来放进 MDC,全链路追踪会方便很多。
java
public class TraceOutInterceptor extends AbstractSoapInterceptor {
public TraceOutInterceptor() {
super(Phase.WRITE);
}
@Override
public void handleMessage(SoapMessage message) {
String traceId = MDC.get("traceId");
// 写入 SOAP Header
}
}
4.5 超时、重试与连接池
- 超时:
HTTPClientPolicy.setConnectionTimeout和setReceiveTimeout,单位毫秒。 - 重试:CXF 自带
FailoverFeature,支持多地址切换;业务层可以用 Spring Retry 做幂等重试。非幂等操作别瞎重试。 - 连接池:CXF 默认走
HttpURLConnection,没有连接池。引入cxf-rt-transports-http-hc5后,可以交给 Apache HttpClient 5 的PoolingHttpClientConnectionManager管连接。
java
@Bean
public HTTPConduitConfigurer httpConduitConfigurer() {
return (name, address, conduit) -> {
conduit.getClient().setConnectionTimeout(3000);
conduit.getClient().setReceiveTimeout(10000);
if (conduit instanceof HttpClientHTTPConduit) {
// 配置连接池、TLS、代理等
}
};
}
五、WS-Security
依赖:
xml
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-ws-security</artifactId>
</dependency>
5.1 用户名令牌
客户端:
java
Map<String, Object> outProps = new HashMap<>();
outProps.put(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN);
outProps.put(WSHandlerConstants.USER, "app-user");
outProps.put(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_DIGEST);
outProps.put(WSHandlerConstants.PW_CALLBACK_REF, new ClientPasswordCallback());
WSS4JOutInterceptor out = new WSS4JOutInterceptor(outProps);
((BindingProvider) port).getRequestContext()
.put(WSS4JOutInterceptor.class.getName(), out);
服务端:
java
Map<String, Object> inProps = new HashMap<>();
inProps.put(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN);
inProps.put(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_TEXT);
inProps.put(WSHandlerConstants.PW_CALLBACK_REF, new ServerPasswordCallback());
endpoint.getInInterceptors().add(new WSS4JInInterceptor(inProps));
密码回调里根据 WSPasswordCallback.getIdentifier() 返回密码。生产建议用 PasswordDigest,再配时间戳防重放。明文密码不是不能用,但要看对端要求。
5.2 签名与加密
签名、加密通过 WSHandlerConstants.SIGNATURE_PROP、ENCRYPTION_PROP 和 WSS4J 的 Merlin Crypto 配置。注意 CXF 3.5/4 用的是 WSS4J 2.x/3.x,配置前缀是 org.apache.wss4j.crypto,不是老版本的 org.apache.ws.security.crypto:
properties
org.apache.wss4j.crypto.provider=org.apache.wss4j.common.crypto.Merlin
org.apache.wss4j.crypto.merlin.keystore.type=JKS
org.apache.wss4j.crypto.merlin.keystore.file=classpath:keystore/client.jks
org.apache.wss4j.crypto.merlin.keystore.password=changeit
典型动作组合:
- 客户端签名 + 加密 + 时间戳
- 服务端验签 + 解密 + 校验时间戳
- 算法建议
RSA-SHA256、AES-256,SHA1 和 3DES 能不用就不用
证书轮换时,用双证书策略:新旧证书先同时放信任库,先切签名,再切加密,最后下线旧证书。别反过来,不然很容易把调用方切死。
5.3 时间戳与 MTOM 附件
时间戳:
java
outProps.put(WSHandlerConstants.ACTION,
WSHandlerConstants.TIMESTAMP + " " + WSHandlerConstants.SIGNATURE);
MTOM:
java
@WebService
@MTOM
@BindingType(SOAPBinding.SOAP11HTTP_MTOM_BINDING)
public class FileService {
public void upload(@XmlMimeType("application/octet-stream") DataHandler file) {
// 流式处理附件
}
}
客户端启用 MTOM:
java
((BindingProvider) port).getRequestContext().put("mtom-enabled", true);
MTOM 能避免附件 Base64 膨胀,内存占用会好很多。但旧客户端不一定支持 xmime:expectedContentTypes,切换前得回归,别直接上生产。
六、报文治理
6.1 XML 校验与命名空间
服务端可以开 Schema 校验:
java
@SchemaValidation(type = SchemaValidationType.IN)
@WebService(endpointInterface = "com.example.order.OrderService")
public class OrderServiceImpl implements OrderService { ... }
命名空间建议在 package-info.java 里统一定义:
java
@XmlSchema(
namespace = "http://example.com/order/v1",
elementFormDefault = XmlNsForm.QUALIFIED
)
package com.example.order;
遗留系统里经常看到 elementFormDefault="unqualified"。这种情况别直接改生成代码,优先用 JAXB 绑定文件(-b)或包级 @XmlSchema 修正。wsdl2java 的 -xjc 是往 XJC 传参数,但不是所有命名空间问题都能靠它解决,硬调容易把生成代码搞乱。
6.2 异常转换
用 @WebFault 定义业务异常,保证 SOAP Fault 结构稳定:
java
@WebFault(name = "OrderFault", targetNamespace = "http://example.com/order/v1")
public class OrderFault extends Exception {
private final OrderFaultDetail detail;
public OrderFault(OrderFaultDetail detail) {
super(detail.getMessage());
this.detail = detail;
}
public OrderFaultDetail getFaultInfo() {
return detail;
}
}
未捕获的运行时异常,CXF 会转成 SOAPFault。生产环境建议通过 FaultListener 或自定义 FaultOutInterceptor 统一错误码,别把堆栈直接丢给调用方。
6.3 日志脱敏与审计
LoggingFeature 支持敏感元素脱敏:
java
LoggingFeature logging = new LoggingFeature();
logging.setPrettyLogging(true);
logging.setSensitiveElementNames(Set.of("password", "idCard", "bankCard"));
logging.setSensitiveProtocolHeaderNames(Set.of("Authorization"));
bus.getFeatures().add(logging);
审计可以用自定义 SOAP 拦截器做,记请求时间、来源、操作名、耗时、结果码,然后落审计表。审计日志本身也要脱敏,别把密码和证件号又写进去。
七、测试与联调
7.1 SoapUI 与 Mock 服务
- SoapUI/ReadyAPI:导入 WSDL,自动生成请求模板,XPath 断言、安全测试、Mock Service 都能用。
- WireMock:适合客户端单元测试,映射 SOAP 响应,模拟超时、Fault、异常 HTTP 状态。
- CXF Mock:用
ServerFactoryBean起个最小本地服务,不依赖外部系统。 - 接口 Mock:Mockito mock 生成的 Port 接口,最快,但不校验 XML 契约,适合业务逻辑测试。
7.2 契约回归与 WSDL 兼容性检查
- WSDL 和 XSD 进 Git,CI 里比较新旧差异。
- 规则还是那几条:不能删操作,不能改已有元素类型和命名空间,不能改元素顺序,只能新增可选元素或新操作。
- 用
wsdl2java重新生成 Stub 并编译,确保旧客户端代码不受影响。 - SOAP 1.1/1.2、MTOM 开关、安全策略变更,必须做端到端回归。只跑单元测试不够。
八、生产避坑
8.1 线程安全
Bus、Endpoint、客户端代理通常线程安全,可以当单例。- 自定义拦截器不要持有可变共享状态,否则并发下容易串数据。
- JAX-WS 的
BindingProvider.getRequestContext()并发修改时可能互相影响。建议启动时配置好,不要在请求线程里动态改。 WSS4JOutInterceptor这类安全拦截器如果包含可变配置,建议每个客户端实例独立创建,别图省事共享。
8.2 XXE 防护
禁用 DTD 和外部实体:
java
System.setProperty("javax.xml.stream.supportDTD", "false");
System.setProperty("javax.xml.stream.isSupportingExternalEntities", "false");
System.setProperty("org.apache.cxf.stax.allowInsecureParser", "0");
同时开启 Schema 校验,不符合 XSD 的报文直接拒掉。外部 WSDL 不要运行时动态解析并执行其中的任意代码,这个口子不能开。
8.3 大报文内存
- 优先用 MTOM、
DataHandler、Source、Streaming,避免 DOM/JAXB 全量绑定超大 XML。 - 限制 XML 深度、属性数量、元素数量,防 Billion Laughs 和深度嵌套。
- JVM 堆和直接内存要合理设置,GC 要监控;附件传输走流式,别让
byte[]全量驻留。
8.4 字符集与版本兼容
- 统一 UTF-8,HTTP
Content-Type的 charset 要和 XML 声明一致。 - 对接 GBK/GB2312 遗留系统时,通过拦截器强制编码,避免乱码。
javax和jakarta不能混用。迁移时同步替换javax.jws、javax.xml.ws、javax.xml.bind。- SOAP 1.1 和 1.2 绑定不能混用,
@BindingType必须写清楚。 - RPC/Encoded 风格在 CXF 里支持有限。碰到 Axis1 遗留服务,可能得降 CXF 版本或定制 DataBinding,别指望默认配置一把过。
九、最后几句话
Spring Boot 接 Apache CXF,不是什么新潮架构,但它确实是接遗留 SOAP 系统比较稳的一条路。真要做,重点不在会不会写 @WebService,而在 WSDL 契约能不能管住、WS-Security 能不能统一、老系统的怪癖能不能提前兜住。WSDL 进版本库,CI 做兼容性检查;安全策略封装成公共客户端配置,证书轮换提前演练;日志脱敏、审计、异常转换、Schema 校验尽量放在拦截器层;测试别只测业务,契约回归和端到端回归都得有;生产上 XXE、大报文、字符集、线程安全、javax/jakarta 兼容这些坑,逐条有预案。把这几件事做扎实,Spring Boot + CXF 在银行、政务、ERP 这类场景里就能安安稳稳跑着,不用天天救火。
🎁 福利时间
如果你正在备战面试或者想要学习其他知识,给大家推荐一个宝藏知识库,作者整理了一些列 Java 程序员需要掌握的核心知识,有需要的自取不谢。
