Spring Boot 集成 Apache CXF 发布与消费 SOAP WebService:WSDL、WS-Security

一、场景与痛点

银行核心、信贷、支付、政务审批、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 服务跟这些系统对接时,常见坑有:

  1. 版本冲突。Spring Boot 3 全面切到 Jakarta EE,老系统还大量用 javax.*,CXF 4 上来之后包名得同步迁。
  2. WSDL 和代码对不上。手工改 stub 或服务端注解,短期能跑,长期契约漂移。
  3. 安全策略散。每个遗留系统一套证书、密码策略,客户端配置东一块西一块。
  4. 报文解析出问题。XXE、超大 XML、字符集不一致,轻则乱码,重则 OOM。
  5. 联调回归靠人。SoapUI、Mock、契约回归没自动化,上线前靠手工点。
  6. 跟 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 程序员需要掌握的核心知识,有需要的自取不谢。

相关推荐
软件技术NINI1 小时前
基于JavaWeb 汽车租聘
java·vue.js·spring boot·后端·python·毕业设计·毕设
yz_aiks1 小时前
RestTemplate 废弃后迁移 RestClient 实战:Spring Boot 4.2 升级指南
java·spring boot·后端·微服务·restclient
2301_32241428048 小时前
基于SpringBoot+Vue的休闲零食超市管理系统的设计与实现59004-计算机毕设 原创(免费领源码+部署教程+开发环境)
java·vue.js·spring boot·redis·mysql·决策树·推荐算法
金銀銅鐵10 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路11 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x11 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn13 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈14 小时前
AI 读你的提示词,其实很没耐心
后端
用户83562907805114 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python