有一类接口技术很多年没被提起,但真要碰上时,网上资料却大多停留在老古董阶段——我说的是WebService。这几年微服务接口基本都是RESTful风格的天下了,JSON一梭子打天下,但只要你接触过传统制造业、金融老旧系统、运营商内部平台,就会发现基于SOAP协议的WebService依旧扛着大量核心业务。尤其是一些十多年前建设的系统,对外提供数据能力的方式就是WebService,新系统必须去对接它。
我之前就碰到过这样一个需求:模拟项目X需要把一个业务系统跟某个老平台打通,对方只暴露了一个基于SOAP的WebService接口,参数格式、安全认证方式都是人家定死的。我们这边用的技术栈是SpringBoot,这就出现了一个几十年前的经典问题——如何用SpringBoot实现WebService服务端,同时又作为客户端去调用别人家的接口。
网上搜索WebService + SpringBoot,翻来覆去就是几篇老博客,结论不少但代码缺胳膊少腿,真正能落地跑通的很少。花了两天时间把这块从服务端发布到客户端调用完整踩了一遍,故写一篇实操记录,希望能帮你省下时间。博主介绍:Mr. Yoho,关注全栈技术,喜欢折腾和研究技术细节。
1. 核心思路拆解:SpringBoot到底怎么跟WebService扯上关系
先说一个根上的认知问题:WebService和SpringBoot是否冲突?答案是——根本不冲突,WebService是基于SOAP协议的一种远程调用方式,跟SpringBoot这种应用开发框架是两个维度的东西。SpringBoot负责把你的服务跑起来、管理Bean、提供内嵌容器,WebService只是暴露在某个URL路径上的一组业务方法。
不过这里有一个历史坑。传统的WebService开发在Java领域使用的是JAX-WS规范,最常用的实现是JDK自带的Endpoint发布方式。单独用JDK发布WebService很简单,写一个实现类,用Endpoint.publish()方法就能发布。但是到了SpringBoot里,你不需要自己去创建Endpoint并绑定端口,因为服务本身已经运行在Tomcat容器里了,你要做的就是通过某种方式把WebService的请求路径注册到SpringBoot内嵌的Servlet容器中。
理解这一点非常关键:SpringBoot本身不解析SOAP请求,它只是一个宿主环境。你要做的是引入一个WebService框架(CXF或者其它),让这个框架在SpringBoot内嵌容器中注册一个Servlet,专门拦截某个URL路径的SOAP请求,再转发给你写的业务类。
还有一种思路是用Spring自带的HttpInvoker或者JaxWsPortProxyFactoryBean来做客户端调用。但如果你要同时兼顾服务端发布和客户端调用,选型上最省心的还是Apache CXF。CXF既能配合SpringBoot做服务端发布,又能作为客户端框架生成代理进行调用,一套框架两端解决,不用来回切技术栈。
我这次使用的组合是:SpringBoot 2.x + CXF 3.x。CXF 3.x对JAX-WS注解支持得非常好,几乎可以像写普通类一样写WebService服务端。
2. 工具选型解析:CXF不是唯一方案,但综合成本最低
Java领域做WebService,大体上有三条路线:JDK自带JAX-WS实现(轻量但整合SpringBoot麻烦)、Apache CXF(功能丰富、整合方便)、Spring自家的Spring WS(更偏文档型和XML操作,配置繁琐)。对大多数只用过SpringBoot的同学来说,CXF是最容易上手的。
JDK自带实现的问题在于:你要自己管Endpoint的生命周期,还要把它挂到Spring容器里。虽然可以用ServletRegistrationBean把一个CXFServlet(如果是CXF)或者JAX-WS的Servlet注册进去,但自己写这些桥接逻辑非常容易出错。用cxf-spring-boot-starter的话,框架帮我们把Servlet注册、Spring整合、自动配置全部做完了。
项目引入依赖的时候要注意版本协同问题。SpringBoot 2.x对应CXF 3.x,SpringBoot 3.x对应CXF 4.x。如果你用SpringBoot 3.x却引入CXF 3.x的包,启动时会出现大量ClassNotFoundException或者NoSuchMethodError,排查起来很浪费时间。
最终引入依赖(Maven方式)大概像这样:
<dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-spring-boot-starter-jaxws</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-transports-http</artifactId> <version>3.5.5</version> </dependency>对了,如果你用JDK 17以上,还需要额外注意:
<dependency> <groupId>javax.xml.ws</groupId> <artifactId>jaxws-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>javax.jws</groupId> <artifactId>javax.jws-api</artifactId> <version>1.1</version> </dependency>原因也很直接:JDK 11以后,Java EE相关的API模块被移出了JDK默认引入范围,而CXF依赖了这些API的编译期类。不补依赖的话,编译时会报找不到javax.jws.WebService之类的错误。
3. 全局配置文件:注册路径与服务发布的那些坑
引入依赖之后,需要在SpringBoot配置文件中把CXF的基础路径设置好。这里涉及的一个概念是org.apache.cxf.transport.servlet.CXFServlet的默认URL匹配规则。CXF启动时会自动注册一个Servlet,默认映射路径是/services/*,也就是说你最终发布的WebService地址基本长这样:http://ip:port/context-path/services/接口名。
我习惯在配置文件中显式指定路径,防止默认值跟业务路径冲突:
server: port: 8080 servlet: context-path: /demo cxf: path: /ws这样配置之后,CXF的Servlet映射路径就是/demo/ws/*,接口服务的最终访问地址就是http://localhost:8080/demo/ws/接口名?wsdl。
有人会问,为什么自定义配置之后,生成的WSDL地址有时候不对?这是CXF的一个老毛病:你通过http://localhost:8080/demo/ws/xxx?wsdl访问时,WSDL文档里面引用的地址往往是发布时CXF自动解析到的内部地址,可能与外部访问地址不一致。这个问题我后面会在重构地址配置那部分详细说。
这里值得多说一句:如果你用的是SpringBoot 2.6或更高版本,由于spring.mvc.pathmatch.matching-strategy默认值改成了PATH_PATTERN_PARSER,启动时CXF偶尔会报路径匹配相关的警告,一般不会导致故障,但最好在配置里显式设置:
spring: mvc: pathmatch: matching-strategy: ant_path_matcher这个配置主要是给Swagger之类的组件用的,不影响CXF,但能避免控制台一堆异常堆栈,看着舒坦也不会误判系统有问题。
4. 服务端开发:接口定义、实现类的三个核心注解
服务端开发的核心工作其实就是三个注解的事。先说接口,定义WebService接口用@WebService标注,接口方法上按需加@WebMethod。这里我踩过最重要的一个坑:CXF发布的WebService,如果接口里有多个方法,而某个方法没有加@WebMethod注解,它默认不会出现在发布内容里。这一点跟很多网上教程说的模糊情况不一样,实测下来不加@WebMethod的方法根本不会被发布出来。
接口定义大概长这样:
package com.example.ws.service; import javax.jws.WebMethod; import javax.jws.WebService; @WebService(name = "UserService", targetNamespace = "http://service.ws.example.com") public interface UserService { @WebMethod String getUserInfo(String userId); @WebMethod String createUser(String userName, String age); }有几个细节要注意。
name属性决定了生成WSDL里wsdl:portType的名称,targetNamespace一定要定义,因为SOAP请求的namespace必须跟它一致,客户端生成的代理才匹配得上。如果你不写targetNamespace,CXF会拿Java包名反推一个默认值,两种服务端、客户端写法不一致时,调用端很容易报SOAP解析错误。
getUserInfo和createUser这种参数很简单的接口,直接定义为String即可。但如果你在真实项目中接口参数是复杂对象,比如需要传一个User对象,那么这个User类也需要用@XmlType注解标注,否则CXF序列化时无法把Java对象转成XML片段。
接下来是真正干活的时候——我在一个实际项目中就是没给参数对象加@XmlRootElement注解,结果服务端启动正常,但是客户端一调用就报javax.xml.bind.MarshalException,当时一度以为是CXF版本问题,排查了很久才定位到是XML序列化注解缺失。所以复杂参数对象的类型上务必注意,别让坑藏在这里。
实现类代码如下:
package com.example.ws.service.impl; import javax.jws.WebService; import com.example.ws.service.UserService; @WebService( serviceName = "UserServiceService", portName = "UserServicePort", targetNamespace = "http://service.ws.example.com", endpointInterface = "com.example.ws.service.UserService" ) public class UserServiceImpl implements UserService { @Override public String getUserInfo(String userId) { return "{\"userId\":\"" + userId + "\",\"name\":\"张三\"}"; } @Override public String createUser(String userName, String age) { return "create user success: " + userName + ", age: " + age; } }实现类上的@WebService注解比接口上的更讲究:
serviceName会出现在WSDL的wsdl:service节点,一般是接口名+Service后缀,用于客户端定位服务。portName对应绑定端口名称。endpointInterface必须写清楚接口的全限定名,它是连接接口和实现类的桥梁。
在线下单出现过的一个问题是,serviceName随便写了一个跟包名不符的值,结果线上服务调试时客户端的代理生成总是找不到对应服务节点。后来查资料才弄明白,CXF实际上以实现类的@WebService注解配置为准生成WSDL,如果serviceName没有严格对应WSDL解析方式,客户端在搜索目标服务时就会报错。所以如果你手动写了serviceName和portName,一定要跟客户端调用的serviceName保持完全一致,这个点亿级数据量系统也可能踩到。
最后需要给出一个配置类,把上面这个实现类发布成WebService,这一步极常用错。网上很多老式教程用的是applicationContext.xml配<jaxws:endpoint>标签的方式,但SpringBoot项目现在基本不用XML配置,用@Configuration类更干净。
package com.example.ws.config; import javax.xml.ws.Endpoint; import org.apache.cxf.Bus; import org.apache.cxf.jaxws.EndpointImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.example.ws.service.impl.UserServiceImpl; @Configuration public class WebServiceConfig { @Autowired private Bus bus; @Bean public Endpoint endpoint() { EndpointImpl endpoint = new EndpointImpl(bus, new UserServiceImpl()); endpoint.publish("/userService"); return endpoint; } }注意,endpoint.publish("/userService")里的路径是相对于CXF的Servlet路径的。配置文件中设了cxf.path=/ws,所以最终访问地址是http://localhost:8080/demo/ws/userService?wsdl。
有个非常关键且容易踩的点是:EndpointImpl这个类以下划线跟业务类名称作为构造参数,这里直接用new UserServiceImpl()相当于已经实例化了一个实现类,它会注册到Spring容器里吗?答案是——不会。new UserServiceImpl()创建的是一个脱离Spring容器管理的实例,这意味着实现类里如果用@Autowired注入Service或Mapper,会是null,一调用业务方法就空指针。
正确的做法是让Spring管理实现类,然后从Spring上下文中获取或者通过构造器注入:
@Configuration public class WebServiceConfig { @Autowired private Bus bus; @Autowired private UserServiceImpl userService; @Bean public Endpoint endpoint() { EndpointImpl endpoint = new EndpointImpl(bus, userService); endpoint.publish("/userService"); return endpoint; } }这里有一个容易忽略的细节:EndpointImpl的publish方法会真正把Endpoint注册到Servlet的处理器链上。如果你没有加@Bean标注,而是直接在@PostConstruct里手动发布,也行,但用@Bean的好处是——如果发布失败,SpringBoot启动直接报错,你第一时间就能发现。如果用@PostConstruct,启动日志可能被吞掉,服务起来后你根本不知道WebService到底发布成功没有。
5. 客户端开发:三种调用方式中最稳的方案选择
客户端调用WebService,核心思路是:用CXF提供的代理工厂(JaxWsProxyFactoryBean)动态生成一个接口的代理实例,然后直接调用接口方法。这本质上就是动态代理机制,跟你平时用Feign调用REST接口在体验上非常接近。
package com.example.ws.client; import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; import com.example.ws.service.UserService; public class UserServiceClient { public static void main(String[] args) { JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean(); factory.setServiceClass(UserService.class); factory.setAddress("http://localhost:8080/demo/ws/userService"); UserService userService = (UserService) factory.create(); String result = userService.getUserInfo("1001"); System.out.println(result); } }这种方式的优点是快、直接、少依赖,不需要提前生成客户端代码。因为服务端和客户端往往在同一个项目中(至少在本次模拟项目X中,两部分代码处于一个工程树中),所以客户端直接引用服务端的接口类即可。这是最简方案。
但实际项目中,客户端常常不在同一个服务里,此时有两种做法:
- 方式A:把服务端接口复制到客户端工程,保持包名、注解完全一致。
- 方式B:用
wsimport命令先拉取WSDL,生成客户端代理代码再调用。
方式A坑比较多,因为接口类的targetNamespace、参数类、方法签名必须和服务端的完全一致,稍有一个不对,SOAP消息解析就会失败。方式B虽然生成的代码冗余度大,但是通用性好,不依赖服务端是否开放源码。
如果你想用wsimport生成代码,命令大致是:
wsimport -keep -p com.example.ws.client.generated http://localhost:8080/demo/ws/userService?wsdl生成之后你会得到一堆Java文件,里面有一套完整的JDK JAX-WS风格的代理类,直接实例化UserServiceService再拿到端口调用即可。这种方式生成的代码适合跨团队、跨语言的场景,因为它不依赖任何CXF的客户端类库,纯JDK标准。
如果你希望调用的时候打印SOAP请求和响应报文方便排查问题,可以在创建代理前开启CXF日志拦截器:
factory.getInInterceptors().add(new LoggingInInterceptor()); factory.getOutInterceptors().add(new LoggingOutInterceptor());导入类是:
import org.apache.cxf.interceptor.LoggingInInterceptor; import org.apache.cxf.interceptor.LoggingOutInterceptor;实际使用中,LoggingInInterceptor打印的是对方返回的SOAP响应报文,LoggingOutInterceptor打印的是你发出去的SOAP请求报文。这两个拦截器在联调初期非常救命,能看到实际的XML消息内容,而不是只看一脸懵的状态码。
6. 高级配置:WebService超时设置与地址重构
提到客户端调用,新手很容易忽略的就是超时时间。CXF默认的超时时间比较长(有人实测甚至超过好几分钟),一旦对方服务假死或者网络不通,你的线程会长时间挂起,直到很晚才报ConnectException。这在联调时非常影响排查效率。
正确的做法是在客户端设置连接超时和接收超时:
factory.getClient().getOutInterceptors().add(new org.apache.cxf.interceptor.LoggingOutInterceptor()); // 设置连接超时 org.apache.cxf.transport.Conduit conduit = org.apache.cxf.frontend.ClientProxy.getClient(userService).getConduit(); conduit.getClient().setConnectionTimeout(3000); conduit.getClient().setReceiveTimeout(10000);注意,在CXF 3.3.0以上版本中,conduit.getClient()已经改成了conduit.getClient()或者直接在Conduit中操作。这里不同版本API略有差异,如果你是CXF 3.5.x版本,直接用上面代码第5行的getClient()即可。
有一个非常容易遇到的细节:SocketTimeout和ConnectTimeout的作用范围不一样。连接超时只管TCP握手建立连接的时间,接收超时管的是从发出请求到收到响应的整个等待时间。如果你接的是业务的报表查询接口,底层可能执行了几分钟,此时接收超时不能设置太短,否则会误报超时;而连接超时长度一般很短,因为网络不通往往几秒内就能判断出来。
再来说说服务端WSDL地址重构问题。我在本地测试的时候发现,通过localhost访问正常,但把地址换成内网IP后,再去访问WSDL,里面显示的soap:address地址还是localhost。这就导致客户端拿WSDL生成代码后调用失败。解决办法是在配置文件中使用CXF提供的CXFServlet初始化参数:
cxf: path: /ws servlet: load-on-startup: 1不过这只能解决Servlet映射,不能解决WSDL内嵌地址问题。真正能解决地址重构的方式是配置一个System.setProperty或者使用自定义AddressRewriteInterceptor,但说实话,配置复杂,一般情况下你直接用客户端调用方式,不依赖WSDL的自动生成的话,完全不需要纠结这个问题。
我不建议在生产环境依赖客户端去拉WSDL的方式动态生成代理,因为拉WSDL生成代理的方式常常必须在服务启动时执行,而服务启动时服务端可能没起来,就会打乱你的部署顺序。所以,把接口类和服务端定义保持一致,用JaxWsProxyFactoryBean直接调用,才是日常项目中最稳的做法。
7. 报文调试与追踪:好看懂SOAP才是真正的掌握
WebService联调时,最大的困惑不是写代码,而是看不懂SOAP报文。尤其当参数嵌套复杂或者返回值结构庞大时,一眼看过去全是尖括号标签,难免头皮发麻。
实际调试时,我的建议是先抓报文再猜问题。加日志拦截器之后的输出大概长这样:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <ns2:getUserInfo xmlns:ns2="http://service.ws.example.com/"> <arg0>1001</arg0> </ns2:getUserInfo> </soap:Body> </soap:Envelope>注意看arg0这个标签名。如果你接口定义的参数名是userId,CXF默认生成SOAP消息时会把参数标签命名为arg0,这是CXF的一个“传统艺能”。很多同学一看到arg0就慌了,以为参数名丢了。实际上CXF默认不保留Java方法参数名,如果你想在SOAP报文中显示有意义的参数名,需要在接口或实现类上加上@WebParam(name = "userId")注解:
@WebMethod String getUserInfo(@WebParam(name = "userId") String userId);这个细节在对接一些严格校验参数的第三方系统时非常重要,对方可能在WSDL或者接口文档里定义了固定的SOAP标签名,比如<userCode>,如果你生成的是arg0,对方直接拒绝请求。
看返回报文时,常见场景是返回值里面嵌套了XML结构。比如接口返回一个字符串,内容是JSON格式,那么在SOAP响应里其实就是一个普通的String文本,外层被包在一个形如<return>{"userId":"1001"}</return>的标签里。此时你只需要做字符串解析,问题不大。但如果你的返回值不是简单字符串,而是一个对象,例如UserInfoDTO,那么CXF会把整个DTO序列化成一段XML,标签层级跟DTO的属性是一一映射的。理解了这一点,排查时就不会在报文里迷路。
调试工具的使用上,我除了用代码日志拦截器,偶尔也会用SoapUI工具做冒烟验证。在SoapUI里粘贴WSDL地址,它会自动生成请求模板,你可以直接修改参数后发送请求。不过需要注意的是,SoapUI生成的请求模板默认带上SOAPAction头,而一些服务端对SOAPAction校验很严格,如果报文发出去报SOAPAction缺失或错误,大概率是服务端接口的@WebMethod(action = "...")没配好。
8. 常见问题与排查技巧实录
两天实战下来,把碰到的问题整理成了一张速查表,排查问题的时候按表索骥能省不少时间。
| 现象 | 根本原因 | 解决办法 |
|---|---|---|
启动报org.apache.cxf.wsdl11.WSDLManagerImpl异常 | CXF与SpringBoot版本不匹配 | 统一升级CXF版本或降低SpringBoot版本 |
调用时返回No such operation | 接口方法没有加@WebMethod或者operationName配置错 | 确认接口所有需要暴露的方法都加了@WebMethod注解 |
返回Could not find conduit | 客户端地址配错,或者WebService未发布成功 | 先用浏览器访问?wsdl确认地址存在 |
| 返回401或403 | 服务端有权限拦截或CXF拦截器校验失败 | 检查服务端@WebService配置、拦截器是否加了认证逻辑 |
| WSDL地址是localhost但外部访问需要内网IP | CXF自动生成地址时使用的本机解析名不对 | 忽略WSDL自动生成地址,直接用JaxWsProxyFactoryBean指定地址 |
XML解析异常unexpected element | 客户端接口targetNamespace与服务端不一致 | 检查两端的targetNamespace、portType名称是否一致 |
| 对象返回序列化失败 | 返回类型的DTO没有@XmlRootElement注解 | 给参宿类型或者返回类型DTO添加JAXB注解 |
| 客户端调用超时 | 接收超时时间设置过短或者对方服务响应慢 | 调大setReceiveTimeout或者优化服务端耗时逻辑 |
| 日志打印不出SOAP报文 | 没有加LoggingInInterceptor/LoggingOutInterceptor | 在factory或者endpoint上添加日志拦截器 |
还有一个非常经典的坑,是SpringBoot项目里同时引入了spring-boot-starter-web和CXF后,CXF的Servlet路径如果设置的跟SpringMVC的DispatcherServlet路径冲突,接口会出现404。比如你的项目里既有普通REST接口,又有WebService接口,路径设计时一定要给WebService单独分一段(比如/ws开头),不要跟/api之类的REST路径混在一起。这个点在规划阶段就要想好,不然上生产后改路径会影响已有的调用方。
另外,WebService服务端发布后,你可以在浏览器直接访问http://localhost:8080/demo/ws/userService?wsdl查看WSDL。需要注意的是,CXF默认会生成WSDL但页面可能渲染得比较慢,且第一次访问时CXF才会真正初始化该Endpoint的WSDL信息。如果浏览器直接报404,第一个确认的点是CXF的Servlet路径到底配没配成功,第二个确认点是endpoint.publish("/userService")有没有执行成功,第三个确认点才是你访问的URL路径是否有拼写错误。我当时调试时花了半小时反复检查代码,最后才发现是自己把路径拼成了/userService/多了一个斜杠。
拼接地址时,注意一个关键但容易被忽略的点:endpoint.publish("/userService")内部会做字符串处理,如果在路径末尾加上一个斜杠/userService/,CXF会把这个/userService/连同斜杠当作服务名的一部分,最后访问的地址会变成http://localhost:8080/demo/ws/userService/?wsdl,而你按习惯访问http://localhost:8080/demo/ws/userService?wsdl时就找不到。服务发布路径末尾的斜杠,严格来说是敏感字符,我不建议你乱加。
为了稳妥,我通常会在发布路径前后都做一次Trim检查,并且在自测时用一个封装方法统一拼接地址,避免不同模块拼接层级产生多余斜杠。这个细节在真实项目中非常有价值,尤其当服务端和客户端不是由同一人开发时,光对齐路径就要耗掉半天,能直接用工具类收敛的地方就提前收敛。
9. 另一种服务端发布方式:用CXFServlet灵活植入任意路径
前面讲的是用EndpointImpl的标准做法,其实CXF还有另一种发布方式,也值得掌握,尤其在你想完全掌控URL映射的时候——手动注入CXFServlet并通过ServletRegistrationBean注册。
如果你不想使用cxf.path配置,而是想让WebService在一个完全自定义的URL上生效,可以自己定义配置类:
@Configuration public class CxfServletConfig { @Bean public ServletRegistrationBean<CXFServlet> cxfServlet() { CXFServlet cxfServlet = new CXFServlet(); ServletRegistrationBean<CXFServlet> registration = new ServletRegistrationBean<>(cxfServlet, "/customWs/*"); registration.setLoadOnStartup(1); return registration; } @Bean public Endpoint userEndpoint() { EndpointImpl endpoint = new EndpointImpl(bus, userService); endpoint.publish("/userService"); return endpoint; } }这种方式在多模块项目里特别实用。如果你的系统是一个平台,里面挂了好几个业务模块,每个模块都想暴露自己的WebService接口,你可以给每个模块注册一个独立路径的CXFServlet,让它们各自负责各自的接口,避免所有接口都挤在同一个/services前缀下。
不过需要注意,多个CXFServlet注册时,每个Servlet内部维护的Bus可能独立,也可能共享,这取决于你是否为每个Servlet单独配置了Bus。共享Bus的话,发布的Endpoint要保证路径唯一,否则CXF会报Endpoints冲突,表现是启动时一切正常,但运行时一个地址访问两个方法直接乱套。这个场景虽然少见,但一旦出现排查起来非常痛苦。
对于绝大多数单模块项目,我还是默认推荐使用cxf.path+EndpointImpl.publish()的方式,简单直接,代码量最少,出问题的点也最少。
10. 最后分享点个人经验
WebService这套东西放在今天看确实有点“复古”,但它依旧活跃在大量企业级系统的对接场景中。SpringBoot做WebService服务端和客户端,核心思路跟做REST接口是相通的——定义好接口契约,让框架帮你完成协议转换和序列化。你需要重点控制的是接口契约的准确性和一致性,简单说就是:包名、注解、targetNamespace、参数标签名这些,必须跟对方保持一致,才不会在联调阶段一头雾水。
我个人实际操作中比较深的体会是:不要在联调阶段才想到去核对SOAP报文格式。一开始就该把LoggingInInterceptor和LoggingOutInterceptor打开,把请求和响应报文打印出来,先自己比对一遍,再跟对方联调。这一点跟写REST接口时先看一下Swagger里的JSON结构是一个道理。
另一个建议是:如果业务有选择余地,优先考虑RESTful接口;如果没得选,必须用WebService,那就老老实实把CXF这套标准链路走熟,别东拼西抄网上几年前的代码。CXF版本和SpringBoot版本匹配、路径统一规划、参数注解写清楚,这三大问题处理好了,WebService的开发其实非常顺利。
最后,如果你初次接触JAXB注解对XML结构的影响,建议花半小时跑一个简单的对象序列化测试,感受一下嵌套字段怎么映射成XML标签层级,这对排查那些“字段对不上”“结构不对”的怪问题非常有用。