恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SpringBoot集成Apache CXF实现WebService服务端与客户端开发
首页
资讯中心
/
SpringBoot集成Apache CXF实现WebService服务端与客户端开发
SpringBoot集成Apache CXF实现WebService服务端与客户端开发
发布时间:2026/10/13 21:52:28
“SpringBoot 实现 WebService 接口的服务端和客户端开发”这个标题放在今天多少有点复古。但说实话这两年我在实际项目里做这类对接的次数远比我预想的多物流系统、制造企业 ERP、高校教务平台、银行老核心甚至是海关报关这类链路里SOAP 协议依然是被当成“标准姿势”在工作。SpringBoot 项目里要快速接住这些 WebService 请求又要对外发布自己的 WebService 接口目前最顺手的方案还是 Apache CXF。这篇文章我打算把服务端和客户端两条链路完整拆开写从依赖版本、接口定义到发布配置再到实际调用的坑和 Token 鉴权尽量做到照着抄就能用。适合在 SpringBoot 项目里第一次接触 WebService、或者是看过文档但还是没跑通的人。1. 老技术新问题SOAP 为什么还能在 SpringBoot 项目里活得好好的1.1 JAX-WS 与 SOAP 的本质不要把 WebService 和 SOAP 混为一谈。广义的 WebService 可以指任意通过网络暴露的服务接口但我们平时说“做 WebService 对接”基本默认是 SOAP 风格。SOAP 本质是一个基于 XML 的通信协议它做的事情和 REST 很接近都是把方法调用包装成网络请求但它和 REST 的思路完全不同REST 是面向资源用 HTTP 方法表达操作SOAP 是面向操作直接在 XML 信封里写明“我要调用哪个方法、传什么参数”。在 Java 世界里这套东西对应的是 JAX-WS 规范。SpringBoot 本身并没有内置完整的 JAX-WS 实现它提供的是 spring-boot-starter-web-services 这个轻量封装底层需要配合具体实现框架才能跑起来。Apache CXF 就是目前最主流的 JAX-WS 实现它同时支持 JAX-WS 和 JAX-RS能跟 Spring 容器深度整合这也是我推荐在 SpringBoot 项目里用它的原因。1.2 什么场景下不需要犹豫直接用 WebService很多人一听到 SOAP 就觉得慢、觉得重但实际场景里它往往不是“可选项”而是“必选项”。我归纳了一下下面这几种情况你基本不需要纠结对接方是大型企业老系统尤其以 Java 或 .NET 技术栈为主的企业他们的对外接口规范可能十年前就定好了只认 SOAP。数据交换涉及跨组织、跨防火墙需要一个形式化、自描述的 XML 协议WSDL 本身就是“接口文档”双方照着看就行。某些行业或政务场景强制要求 WS-Security、数字签名这类安全规范这些在 SOAP 体系里是原生支持的REST 要实现同等强度反而要堆很多定制代码。接口需要严格的契约比如“这个报文必须长这样、这个字段必填、这个操作必须返回指定错误码”SOAP 的 XML Schema 天然适合做这种强约束。1.3 和 REST 相比SOAP 弱在哪、强在哪理解差异不是为了抬杠而是为了做技术选型时不被“流行度”带跑偏。维度SOAP WebServiceREST 接口协议基于 XML 信封可以走 HTTP 也可以走其他传输一般基于 HTTP(S)接口描述WSDL XSD机器可读、契约严谨OpenAPI虽然也可机器读但宽松得多技术栈支持主流语言都有实现但 Java/.NET 生态最顺几乎所有语言都很顺手性能报文大、解析重性能开销明显偏高轻量性能好安全扩展WS-Security 体系完善主要靠 HTTPS OAuth/JWT 等调用方式客户端可以生成存根代码也可以动态代理直接拼 URL 和 JSON 即可我个人的判断是如果你的服务只给自己的前端调REST 永远是更优解但如果你是在做一个“企业级接口”对接方可能是一个你没法控制的第三方系统SOAP 反而省心。因为 WSDL 把能暴露的操作和数据结构全写清楚了两边对契约不需要反复沟通“这个字段到底叫啥”。1.4 集成方案选型CXF、Spring-WS、原生 JAX-WS 怎么选社区里常见的方案有三类我简单对比一下Apache CXF最推荐。它能直接用 Spring 的 Bean 作为 WebService 实现类通过 EndpointImpl 把服务发布出去客户端又可以用 JaxWsProxyFactoryBean 动态生成代理一整套东西都围绕 SpringBoot 设计坑少文档多。Spring-WSspring-ws-core它对消息层面的控制更强但开发起来偏底层适合对 SOAP 报文要做深度定制的人一般业务项目用不到这么重。JDK 自带的 JAX-WS RI不引入额外依赖也能跑但跟 SpringBoot 集成很别扭发布方式停留在 Servlet 时代处理 Spring 依赖注入很麻烦团队里有人试过就果断放弃了。所以下面的例子全部基于 Apache CXF版本组合是 SpringBoot 2.7.x CXF 3.5.x。如果你用的是 SpringBoot 3.x记得换成 CXF 4.x并且把代码里的 javax.* 包名改成 jakarta.*这点后面会专门提。2. 服务端从零到 WSDL依赖、接口、发布三步走2.1 先选对依赖版本省得后面全报错服务端第一步是加依赖。我用的是 Maven核心就两个一个是 SpringBoot 的 Web 启动器一个是 CXF 的 SpringBoot 起步依赖。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version2.7.18/version /dependency dependency groupIdorg.apache.cxf/groupId artifactIdcxf-spring-boot-starter-jaxws/artifactId version3.5.5/version /dependency这里有个版本对应关系建议记住SpringBoot 2.x 用 CXF 3.xSpringBoot 3.x 用 CXF 4.x。原因不是“新的就一定好”而是 CXF 4.x 全面切换到了 Jakarta EE 命名空间而你引入 SpringBoot 3.x 时内置的依赖已经不支持 javax 包了。混用的话最典型的报错是ClassNotFoundException: javax.xml.ws.EndpointReference看着难查其实就是版本匹配问题。如果你用 Gradle对应的写法是implementation org.apache.cxf:cxf-spring-boot-starter-jaxws:3.5.5 implementation org.springframework.boot:spring-boot-starter-web:2.7.18加完依赖之后SpringBoot 会自动配置 CXF 的自动装配但有一个东西它是不会替你做的CXFServlet 的注册。很多人卡在这一步以为依赖够了就能访问 WSDL结果浏览器打开 404。我习惯直接通过自定义配置类把 CXFServlet 挂到一个路径前缀下。2.2 定义接口和模型注解填对了WSDL 才能生成得标准先写一个接口模拟“用户信息查询”这个业务。实际项目里你可以换成商品查询、订单同步、物流状态获取等等套路完全一样。接口的核心是把下面这几件事通过注解声明清楚targetNamespace命名空间两边联调时必须完全一致这是 SOAP 里最容易出问题的地方。operationNameSOAP 报文中实际调用的操作名。WebParam 的 name每个参数在 XML 报文里的标签名不写的话会默认成 arg0、arg1。WebResult 的 name返回结果在报文里的标签名不写默认是 return。package com.demo.webservice.api; import javax.jws.WebMethod; import javax.jws.WebParam; import javax.jws.WebResult; import javax.jws.WebService; WebService( targetNamespace http://ws.demo.com/user, name UserInfoQueryPortType ) public interface UserInfoQueryService { WebMethod(operationName queryUserInfo) WebResult(name userInfoResult) UserInfo queryUserInfo( WebParam(name userId) String userId ); }对应的实体类就是一个普通的 POJO字段类型以字符串为主。这里我强烈建议跨系统的 WebService 报文里能传 String 就传 String日期用标准字符串格式金额用字符串带上币种。虽然 JAXB 支持各种类型之间的转换但一旦服务端和客户端用的技术栈不对齐类型转换错误会成为联调阶段最折磨人的问题。package com.demo.webservice.api; public class UserInfo { private String userId; private String userName; private String department; private String email; private String phone; // getter / setter这里省略实际代码需要完整生成 }2.3 实现类的正确姿势避免被事务代理坑掉接口写完后实现类是重点。我见过不少同事在这里翻车实现类上加了 Service同时为了写日志或事务又加了 Transactional、Aspect 之类的注解结果 CXF 发布服务时发现“实现类不是理想的 WebService 实现”。原因简单说Spring 在创建带事务或切面的 Bean 时返回的是一个代理对象。如果代理方式不匹配CXF 通过反射拿不到 WebService 注解或者拿不到 endpointInterface。正确且稳妥的做法是两层分离接口层用 WebService 标注实现层用 WebService 指定 endpointInterface实现类本身不放大事务、切面等代理注解。package com.demo.webservice.api.impl; import com.demo.webservice.api.UserInfoQueryService; import com.demo.webservice.api.UserInfo; import javax.jws.WebService; WebService( targetNamespace http://ws.demo.com/user, serviceName UserInfoQueryService, portName UserInfoQueryPort, endpointInterface com.demo.webservice.api.UserInfoQueryService ) public class UserInfoQueryServiceImpl implements UserInfoQueryService { private static final String HTTP_ADDRESS http://ws.demo.com/user; Override public UserInfo queryUserInfo(String userId) { // 实际业务里这里可以注入任意的 Spring Service、Mapper UserInfo info new UserInfo(); info.setUserId(userId); info.setUserName(示例用户); info.setDepartment(研发中心); info.setEmail(demoexample.com); info.setPhone(13800000000); return info; } }如果业务确实需要事务处理方式是把事务放到实现类调用的内部 Service 上而不是放在 WebService 实现类本身。这样既保留了事务能力又不会干扰 CXF 获取真实的实现类型。2.4 发布配置把 CXF 端点挂到指定路径现在写配置类。这一步包括两件事注册 CXFServlet、发布 Endpoint。package com.demo.webservice.config; import com.demo.webservice.api.impl.UserInfoQueryServiceImpl; import org.apache.cxf.Bus; import org.apache.cxf.bus.spring.SpringBus; import org.apache.cxf.jaxws.EndpointImpl; import org.apache.cxf.transport.servlet.CXFServlet; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.xml.ws.Endpoint; Configuration public class WebServiceConfig { Bean public ServletRegistrationBeanCXFServlet cxfServletRegistration() { CXFServlet cxfServlet new CXFServlet(); return new ServletRegistrationBean(cxfServlet, /ws/*); } Bean public SpringBus springBus() { return new SpringBus(); } Bean public Endpoint userInfoQueryEndpoint(Bus bus, UserInfoQueryServiceImpl impl) { EndpointImpl endpoint new EndpointImpl(bus, impl); endpoint.publish(/userInfoQuery); return endpoint; } }注意两个细节我在 Endpoint 参数里注入了 Bus 和实现类Spring 会自动把实现类换成同一个单例对象。如果你看过老教程里面经常直接 new 一个 SpringBus其实没必要注入进来的更稳。endpoint.publish(/userInfoQuery) 这里的路径是相对于 CXFServlet 的映射前缀的。所以完整访问地址是http://localhost:8080/ws/userInfoQuery。2.5 启动项目用 WSDL 验证服务是否发布成功跑起来后直接浏览器打开http://localhost:8080/ws/userInfoQuery?wsdl正常情况下你会看到一坨很长的 XML里面有wsdl:definitions、wsdl:portType、wsdl:binding等节点。看到这些说明服务端已经发布成功了。还可用一个简单的 curl 命令做冒烟测试curl -X POST http://localhost:8080/ws/userInfoQuery \ -H Content-Type: text/xml;charsetUTF-8 \ -d ?xml version1.0 encodingUTF-8? soapenv:Envelope xmlns:soapenvhttp://schemas.xmlsoap.org/soap/envelope/ xmlns:userhttp://ws.demo.com/user soapenv:Body user:queryUserInfo userId1001/userId /user:queryUserInfo /soapenv:Body /soapenv:Envelope响应里如果看到了userInfoResult和userName说明接口链路是通的。到这里服务端的开发就算完成了。3. 客户端调用动态代理是最省事的打开方式3.1 为什么我用 JaxWsProxyFactoryBean 而不是 wsimport 生成代码Java 客户端调用 SOAP 有几种常见方式最传统的是用wsimport根据 WSDL 生成一堆客户端代码然后把生成的类放进项目里调用。我早期做这类需求也这么干但后来放弃了。原因有三个生成的代码非常臃肿一个简单的接口能给你生成五六七八个类。WSDL 一变你必须重新跑一遍生成命令稍有疏漏就会出现 NoSuchMethodError。生成代码的包名、命名空间和你自己接口里定义的容易冲突。Apache CXF 提供的JaxWsProxyFactoryBean是动态代理方式只要客户端有和服务端一致的接口定义哪怕只是把接口文件拷过来它就能在运行时直接生成代理对象。调用代理对象的方法时CXF 在底层自动把方法调用包装成 SOAP 报文发出去再把响应解析回来。好处很明显没有代码生成WSDL 变了通常只需要改接口注解客户端代码零改动。3.2 一个完整的客户端调用示例假设客户端也是个独立的 SpringBoot 项目或者就是一个普通的主函数。最关键的是客户端必须有与服务端完全一样的接口定义文件UserInfoQueryService.class和实体类UserInfo.class。注意是“接口全限定名一致”不是“只要方法签名一致就行”。package com.demo.webservice.client; import com.demo.webservice.api.UserInfo; import com.demo.webservice.api.UserInfoQueryService; import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; public class ClientDemo { public static void main(String[] args) { JaxWsProxyFactoryBean factory new JaxWsProxyFactoryBean(); factory.setServiceClass(UserInfoQueryService.class); factory.setAddress(http://localhost:8080/ws/userInfoQuery); UserInfoQueryService client (UserInfoQueryService) factory.create(); UserInfo userInfo client.queryUserInfo(1001); System.out.println(userInfo.getUserName()); System.out.println(userInfo.getDepartment()); } }这几行代码完整解释了动态代理的魔法setServiceClass告诉 CXF 要用哪个接口来生成代理setAddress告诉 CXF 请求发到哪里之后用factory.create()拿到代理对象调用接口方法时 CXF 会自动完成 SOAP 信封的构造与解析。3.3 不想用代理的话手动拼 SOAP 报文也可以有些情况下你手里连接口文件都没有比如对接方只给了你一个 WSDL 文档或者对方用了一个你不想引入的复杂对象模型。这时候用最底层的办法手动组装 SOAP 报文用普通 HTTP 客户端发出去。package com.demo.webservice.client; import java.io.StringWriter; import javax.xml.soap.*; public class RawSoapClient { public static void main(String[] args) throws Exception { // 创建 SOAP 信封 MessageFactory factory MessageFactory.newInstance(); SOAPMessage soapMessage factory.createMessage(); SOAPPart part soapMessage.getSOAPPart(); SOAPEnvelope envelope part.getEnvelope(); envelope.addNamespaceDeclaration(user, http://ws.demo.com/user); SOAPBody body soapMessage.getSOAPBody(); SOAPBodyElement operation body.addBodyElement(envelope.createName(queryUserInfo, user, http://ws.demo.com/user)); operation.addChildElement(userId).setTextContent(1001); StringWriter writer new StringWriter(); soapMessage.writeTo(writer); System.out.println(writer); // 后续就是普通的 HTTP POSTContent-Type 用 text/xmlbody 用上面生成的 XML // 推荐用 JDK11 的 java.net.http.HttpClient 或者 OkHttp / RestTemplate 发送 } }手动方案不推荐日常使用但非常适合排查问题当动态代理调用失败时你把报文抓出来看基本上问题就一目了然了。3.4 客户端的超时、连接池与日志设置动态代理虽然方便但默认超时往往不符合生产要求。我遇到过对接方响应很慢默认 30 秒不够用的也遇到过反向代理把连接掐断导致调用失败的。CXF 客户端支持通过BindingProvider设置通用超时参数import javax.xml.ws.BindingProvider; import java.util.Map; UserInfoQueryService client (UserInfoQueryService) factory.create(); BindingProvider bp (BindingProvider) client; MapString, Object ctx bp.getRequestContext(); ctx.put(javax.xml.ws.client.connectionTimeout, 5000); ctx.put(javax.xml.ws.client.receiveTimeout, 15000);这两个参数的取值相对通用耗时较短的接口建议分别设成 5 秒和 15 秒足够覆盖大多数场景。如果对接的是批处理任务可以放宽到 30 秒以上。另外建议在生产环境显式开启 CXF 的日志拦截器这样每次请求的报文都能看到方便出问题后复制报文给对接方最典型的方式是import org.apache.cxf.ext.logging.LoggingInInterceptor; import org.apache.cxf.ext.logging.LoggingOutInterceptor; factory.getInInterceptors().add(new LoggingInInterceptor()); factory.getOutInterceptors().add(new LoggingOutInterceptor());注意这个功能来自 cxf-rt-features-logging 模块如果类找不到需要额外引入依赖。日志打印出来的信息里包含 HTTP 状态码、SOAP 报文整体内容排查起来比猜方便得多。4. 联调时最经典的 5 个坑每一个都能让你怀疑人生4.1 命名空间不一致改了半小时其实只差一个斜杠这是我在跨团队对接里遇到最多的问题。A 团队发布的 WSDL 里命名空间是http://ws.demo.com/userB 团队客户端代码里写成http://ws.demo.com/user/多了个尾斜杠。XML 里可以认为这是完全不同的命名空间所以客户端发过去的报文即使操作名一模一样服务端也解析不到对应的方法返回的报错多半是找不到操作或Operation not found。解决方式拿到对方的 WSDL 后第一件事就是搜索targetNamespace的值复制到自己的接口注解里不要手打。肉眼看着一样的字符串很可能藏着不可见字符或大小写差异。我在代码评审时都会要求两边把这个值贴出来对比省得联调阶段互相甩锅。4.2 参数名变成 arg0明明传了参数对方却收到 null如果一个 WebService 接口里没有显式地给 WebParam 写 nameJAX-WS 规范允许实现把它默认成 arg0。这在 HTTP 报文层面不会有问题但如果你和对接方都在解析 SOAP 动作或者依赖参数名做映射就会出现“参数好像传过去了但对方解析出来是 null”。解决方式也很直接在接口注解上把参数名写清楚不要偷懒。比如WebMethod(operationName queryUserInfo) WebResult(name userInfoResult) UserInfo queryUserInfo(WebParam(name userId) String userId);同理返回结果的 name 最好也显式声明。这个习惯养成之后生成的 WSDL 会漂亮很多别人看你的 WSDL 也不会问“queryUserInfo 的入参到底叫什么”这种低级问题。4.3 Date 类型和 XMLGregorianCalendar 的纠缠JAXB 对 Date 的处理一直是重灾区。如果你在接口里直接写UserInfo query(WebParam(name userId) String userId, WebParam(name queryDate) Date queryDate);CXF / JAXB 在生成 Schema 时会把 Date 映射成XMLGregorianCalendar于是 WSDL 里会看到xsd:dateTime。客户端如果也用 CXF 动态代理它还会尝试把 XML 字符串转回 Date这层转换在大多数场景没问题但一旦遇到时区问题就会出幺蛾子。建议对外数据类型统一用 String日期格式固定成yyyy-MM-dd HH:mm:ss要带时区就统一用带时区的 ISO 格式。虽然看起来“不够面向对象”但跨系统对接的真相就是字符串是成本最低、歧义最少的协议。4.4 服务实现类被 AOP 代理后CXF 找不到接口前面服务端部分我提醒过核心还是 Spring 的代理机制。如果你把 Transactional 加在 WebService 实现类上Spring 默认会用 JDK 动态代理代理对象只实现了业务接口没保留 WebService 注解。CXF 在 EndpointImpl 发布时拿不到正确的 WebService 元数据就会抛出 ServiceConstructionException。最省心的策略有两层WebService 实现类别加事务、切面注解保持纯净。如果不得不加可以通过EnableAspectJAutoProxy(proxyTargetClass true)强制使用 CGLIB 代理这样保留的是目标类信息CXF 能拿到接口定义。不过我的经验还是建议第一层。架构上把 WebService 层当成薄薄的适配层里面只做参数校验和异常转换真正的业务逻辑全部委托给内部的 Service 组件这样 CXF、事务、切面互不干扰。4.5 中文乱码问题报文能通但中文全变问号这个问题最隐蔽。现象是报文能收到但中文全是“”。原因通常是客户端在发送 SOAP 请求时没有显式指定编码或者服务端收到的 Content-Type 里 charset 不对。CXF 的默认编码一般是 UTF-8但如果你手动构造报文时用平台默认编码比如 Windows 下是 GBK就会出现中文乱码。排查时先用日志拦截器把服务端实际收到的报文打出来看 HTTP 头里的 Content-Type 是text/xml; charsetUTF-8还是其他值。然后在客户端显式设置ctx.put(org.apache.cxf.message.Message.CONTENT_TYPE, text/xml; charsetUTF-8);或者手动构造报文时指定setEncoding(UTF-8)。一句话总结只要两边都明确用 UTF-8这个坑就不会出现。5. 加一道锁基于 CXF 拦截器的 Token 鉴权5.1 服务端拦截器的设计思路WebService 接口往往暴露在企业边界如果直接裸奔在公网风险很高。SOAP 本身不提供认证能力但 CXF 给了非常灵活的拦截器机制可以在消息链路的不同阶段插入自定义逻辑。我的做法是定义一个服务端拦截器在调用具体方法之前校验 SOAP Header 里的 Token 字段。思路是这样的挂载阶段选Phase.PRE_INVOKE也就是请求已经被解析成对象、但业务方法还没被调用之前。从SoapMessage里拿到SOAPMessage再从 Header 节点里找自定义的authToken标签。如果取不到或校验失败直接抛 Fault把响应设置成一个业务层面的认证错误。package com.demo.webservice.interceptor; import org.apache.cxf.binding.soap.SoapMessage; import org.apache.cxf.interceptor.Fault; import org.apache.cxf.phase.AbstractPhaseInterceptor; import org.apache.cxf.phase.Phase; import javax.xml.soap.SOAPHeader; import javax.xml.soap.SOAPMessage; import org.w3c.dom.NodeList; public class AuthInInterceptor extends AbstractPhaseInterceptorSoapMessage { private static final String TOKEN a1b2c3d4-e5f6-7890-ABCD; public AuthInInterceptor() { super(Phase.PRE_INVOKE); } Override public void handleMessage(SoapMessage message) throws Fault { String token extractToken(message); if (!TOKEN.equals(token)) { throw new Fault(new RuntimeException(invalid auth token)); } } private String extractToken(SoapMessage message) { try { SOAPMessage soapMessage message.getContent(SOAPMessage.class); SOAPHeader header soapMessage.getSOAPHeader(); if (header null) { return null; } NodeList nodes header.getElementsByTagName(authToken); if (nodes.getLength() 0) { return nodes.item(0).getTextContent(); } } catch (Exception ignored) { // 取不到就当没有 token } return null; } }把这个拦截器注册到发布端点上endpoint.getInInterceptors().add(new AuthInInterceptor());这样服务端就带上了最简单的 Token 校验能力。实际生产里你可以把 Token 改成数据库里的白名单、动态签发的 JWT或者按调用方分配独立 Key原理都一样。5.2 客户端自动注入 Token让调用方无感携带凭证如果服务端要求鉴权那么客户端每次调用都得在 Header 里带上 Token。如果靠业务代码手动去拼 SOAP 头部那调用代码会被污染得很厉害。更友好的方案还是拦截器客户端注册一个 Out 拦截器在报文发出前自动把 Header 加上。package com.demo.webservice.interceptor; import org.apache.cxf.binding.soap.SoapMessage; import org.apache.cxf.interceptor.Fault; import org.apache.cxf.phase.AbstractPhaseInterceptor; import org.apache.cxf.phase.Phase; import javax.xml.soap.SOAPElement; import javax.xml.soap.SOAPFactory; public class TokenOutInterceptor extends AbstractPhaseInterceptorSoapMessage { private final String token; public TokenOutInterceptor(String token) { super(Phase.PRE_PROTOCOL); this.token token; } Override public void handleMessage(SoapMessage message) throws Fault { try { SOAPFactory factory SOAPFactory.newInstance(); SOAPElement tokenEl factory.createElement(authToken); tokenEl.setTextContent(token); SOAPMessage soapMessage message.getContent(SOAPMessage.class); soapMessage.getSOAPPart().getEnvelope().addHeader().addChildElement(tokenEl); } catch (Exception e) { throw new Fault(e); } } }客户端创建代理后通过 ClientProxy 拿到 CXF 的 Client 对象再注册拦截器import org.apache.cxf.endpoint.Client; import org.apache.cxf.frontend.ClientProxy; UserInfoQueryService client (UserInfoQueryService) factory.create(); Client cxfClient ClientProxy.getClient(client); cxfClient.getOutInterceptors().add(new TokenOutInterceptor(a1b2c3d4-e5f6-7890-ABCD));这样业务代码调用client.queryUserInfo(...)时完全不需要关心 Token 是怎么放进去的。底层拦截器会在每个 SOAP 请求发出前自动把 Header 组装好服务端校验通过后正常返回结果。5.3 生产环境还建议叠加的几层保护Header 拦截器只是“能用”的起点。真实生产环境我一般还会叠加这些措施强度按需求取HTTPS最简单也最有效SOAP 报文不加密的话Token 在网络上等于明文。IP 白名单如果调用方固定是几个服务器直接在接入层限制来源 IP成本最低。调用方证书用双向 HTTPS 或者 WS-Security 的 X.509 证书方案适合安全要求严格的政企项目。全链路日志脱敏Token 本身属于敏感信息日志打印时要打码。CXF 的日志拦截器默认会打印全部 Header这点在安全评审里容易被亮红灯。6. 最后再分享一点我自己的实操体会做 WebService 对接这几年我最大的感受是代码本身并不难难的是两边对协议的理解同频。每次联调出问题我都先把“自己这边能证明的东西”打印出来比如服务端真实收到的报文、真实的 WSDL、真实的响应然后把问题拆成“协议理解不一致”和“实现 bug”两类逐个排除。如果你正在做的场景是“新系统接老系统”我强烈建议第一次联调时就让双方各出一个能跑通的 Demo哪怕功能再简略。先把最小的 WSDL 链路跑通了再去接复杂的转账、单据同步这类业务你的心态会稳很多。技术选型上一旦确定用 CXF就别中途换框架拦截器、代理、日志这套体系跟着项目走比任何一个“旁门左道的技巧”都可靠。至于“SOAP 到底是不是过时技术”这个话题做业务系统的人其实没那么多执念。协议只是工具能稳定地解决跨组织、跨平台、跨语言的接口互通问题它就是好工具。SpringBoot 里集成 CXF 服务端和客户端这件事整条链路熟练之后从零到上线其实花不了太久。希望这篇内容能帮你少走一些我当年走过的弯路。