接手过几个老系统对接项目的同学应该都有同感:系统之间要交换数据,对方给了个 WSDL 地址,说“你调一下这个 WebService 接口就行”。你看着 SpringBoot 项目里清一色的 RESTful 风格代码,一时竟不知道该从哪儿下手。这篇文章就是写给这类场景的。我会用一套完整的 SpringBoot 集成 WebService 调用方案,把 WSDL 解析、代理生成、Marshaller 配置、超时控制、日志排查、安全认证这些环节全部走一遍,顺便把我在实际项目里踩过的坑都交代清楚。适合正在对接第三方 SOAP 接口,或者打算在手头项目里封装一套通用 WebService 客户端的后端开发同学阅读。
1. WebService 接口为什么现在还躲不掉
1.1 十几年老协议依然活跃的核心场景
很多人觉得 SOAP 是上个时代的东西,但现实是,银行支付网关、航空公司票务系统、政务数据交换平台、传统企业内部 ERP,这些体量巨大的系统之间,大量核心接口仍然是 WebService 形态。原因并不复杂:SOAP 有严格的 WSDL 契约描述、内建 WS-Security 标准,跨语言、跨平台能力极强,一套接口定义出来后,Java、C#、Python、PHP 的客户端都能按协议生成自己的代码。双方只要守住 WSDL 这份“合同”,细节不容易扯皮。
这种背景下,一旦新系统要和老系统做数据打通,SpringBoot 团队就得承担起调 WebService 的活儿。它不像调 REST 那么简单——没有 JSON 一把梭,要处理 SOAP Envelope、命名空间、SOAPAction、XML 序列化,步骤天然多。好在 Spring 生态里有 Spring WS 这套方案,配合 JAXB 和 JAX-WS,把调用成本降到了可接受范围。
1.2 先聊清楚 SOAP 和 REST 的差异
我刚入行的时候也迷糊:REST 是“资源 + 动词”,GET、POST 加 JSON 就能搞定;SOAP 则是“动作 + 消息”,客户端要构造一整套 XML 报文,塞进 Envelope 里发给服务端。SOAP 的请求体长这样:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wea="http://www.example.com/weather"> <soapenv:Header/> <soapenv:Body> <wea:queryWeather> <city>Beijing</city> </wea:queryWeather> </soapenv:Body> </soapenv:Envelope>对比 REST 的GET /weather/Beijing,SOAP 确实显得笨重。但它的好处是严谨:WSDL 里会写明方法名、参数类型、返回结构、异常定义,甚至约束枚举值和服务端口。对接老系统时,这份契约就是双方共同的语言。
所以,SpringBoot 调 WebService 这件事,本质上要解决三件事:如何根据 WSDL 生成客户端代码或对象模型、如何完成 XML 与 Java 对象的相互转换、如何在 HTTP 层正确发送和接收 SOAP 报文。理清这条主线,后面不管用静态方式还是动态方式,心里都有底。
2. 大方向选型:静态调用还是动态调用
2.1 静态调用:wsimport 一把梭生成代理类
静态调用的核心思路是:利用工具读取 WSDL,自动生成一套 Java 客户端代码,包括服务接口、服务实现类和请求/响应的数据模型。生成完毕后,你只需要拿到里面的 Service 类,通过它获取 port,直接调用接口方法即可。
我以前第一次做 WebService 对接,就是先把 WSDL 下载下来,用wsimport命令生成代码,然后丢进 SpringBoot 项目里,注入后调用。整个过程像拼积木,不需要手写 XML 映射,理解成本极低。
这套方案最大的优势是省心:所有请求/响应对象都已经带好了 JAXB 注解,Marshaller 直接扫包,方法签名也跟 WSDL 一一对应,基本不会写错字段名。
缺点也明确:服务端 WSDL 一更新,你本地生成的代码就失效了,得重新生成、重新编译、重新发版。而且生成代码里的类名、包名带有的第三方风格,跟项目现有命名规范不一定搭,需要做一层防腐隔离。
2.2 动态调用:WebServiceTemplate 直接发消息
动态调用的核心是 Spring WS 提供的WebServiceTemplate。你不需要生成任何客户端代理类,只需要手工定义 JAXB 注解的请求/响应对象,然后调用marshalSendAndReceive,把请求对象丢进去,让 Marshaller 负责序列化,再把响应对象换算回来。
它和静态调用的本质区别在于:静态方案是“代码即契约”,动态方案是“数据即契约”。动态方案里,请求和响应的结构是通过 JAXB 注解手动维护的,你对报文有完全控制权。SOAP Header 里要加什么内容、命名空间要调整成什么值、要不要自定义 SOAPAction,都能插手。
灵活性更高,代价是:所有类型定义要自己写,WSDL 复杂时工作量不小。我一般建议,接口数量少、变更新、或者服务端由第三方强管控、WSDL 不稳定时,优先考虑动态方案。
2.3 选型对比与我的建议
| 对比维度 | 静态调用 | 动态调用 |
|---|---|---|
| 上手难度 | 低,自动生成代码 | 中高,需手工写类型 |
| 与 WSDL 同步 | 紧耦合,改 WSDL 需重生成 | 松耦合,按需手工调整 |
| 自定义 SOAP Header | 需要额外操作 | 非常方便 |
| 代码风格控制 | 弱,类名受第三方约束 | 强,完全由你定义 |
| 适合场景 | 接口稳定、数量多 | 接口变更频繁、报文需定制 |
做选型时,如果项目里只需要对接一两个老接口,接口几个月不变一次,直接静态调用。如果你们要做一套统一的外部服务对接模块,未来要接很多供应商,且各家报文格式五花八门,我会偏向动态方案,把 WSDL 相关的逻辑收敛到配置里,而不是散落在生成的代码里。
3. 实操:静态调用完整跑通一次
3.1 拿到 WSDL 先别急着生成代码
对接开始,第一件事不是写代码,而是拿 WSDL 地址去浏览器里看一遍。重点看这几个地方:
wsdl:service和wsdl:port里面的服务名、端口名;wsdl:binding里面的soap:binding的 transport 和 style;wsdl:portType里面的 operation 名称和消息结构;- 最下面的
wsdl:types里 import 了哪个 XSD,可能是内联在 WSDL 里的,也可能是独立 schema 文件。
有一次我对接一个老接口,WSDL 里 import 了外部 XSD,而 wsimport 不能直接通过网络加载那个 XSD,导致构建失败。后来把 XSD 下载到本地,改掉引用路径才通过。所以建议一开始就下载完整 WSDL 和关联的 XSD,放在项目里做本地化解析,既快又稳。
3.2 wsimport 生成客户端代码
假设我们接到的是一个天气查询服务,WSDL 地址是http://www.example.com/ws/weather?wsdl,接口方法是queryWeather,请求参数是城市名。执行命令:
wsimport -keep -s ./src/main/java -p com.example.weather.ws http://www.example.com/ws/weather?wsdl参数说明:
-keep:保留生成的 Java 源文件,不只要 .class;-s ./src/main/java:指定源码输出目录;-p com.example.weather.ws:指定包名,避免生成的类名跟项目现有类冲突;- 最后一个参数是 WSDL 地址或本地文件路径。
生成后,项目里会出现几个核心类:WeatherServiceImplService是 Service 入口;WeatherService是接口;请求/响应对象如QueryWeatherRequest、QueryWeatherResponse;还有一些 JAXB 生成的支持类。
这里需要特别注意一个环境问题:JDK 11 开始,JDK 移除了自带的 wsimport 工具。如果你用的是 JDK 11 及以上,命令行直接敲 wsimport 会提示找不到命令。解决思路有两个:一是下载独立的 JAX-WS RI 工具包,把 bin 目录加进 PATH;二是在 Maven 里引入org.codehaus.mojo:jaxws-maven-plugin,用wsimport插件目标在构建期生成代码。第二种方案能让生成流程进入构建管线,更适合团队协作。
以 Maven 插件的方式,pom.xml 配置类似:
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>jaxws-maven-plugin</artifactId> <version>2.6</version> <executions> <execution> <goals> <goal>wsimport</goal> </goals> <configuration> <wsdlFiles> <wsdlFile>weather.wsdl</wsdlFile> </wsdlFiles> <packageName>com.example.weather.ws</packageName> <sourceDestDir>${project.build.directory}/generated-sources/ws</sourceDestDir> <keep>true</keep> </configuration> </execution> </executions> </plugin>我一直建议生成代码固定放一个目录,并且纳入版本管理,这样接口调用的实现代码始终处于可控状态。WSDL 即便更新,你可以 diff 生成代码,判断哪些接口变了,比一刀切全部重新生成更安全。
3.3 在 SpringBoot 里管理生成代码
生成代码之后,SpringBoot 项目里要引入 Spring WS 依赖。虽然静态调用不依赖 WebServiceTemplate,但 JAXB 相关的运行时支持还是要的:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web-services</artifactId> </dependency>随后配置一个 Jaxb2Marshaller Bean,让 JAXB 知道扫描哪个包下的注解类:
@Configuration public class WebServiceConfig { @Bean public Jaxb2Marshaller marshaller() { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath("com.example.weather.ws"); return marshaller; } }注意setContextPath填的包名,就是 wsimport-p指定的那个包。JAXB 会扫描这个包下面所有带@XmlRootElement或@XmlType注解的类,构建上下文。
真正的调用服务用一个 Service 类封装。在这个例子里,wsimport 生成的 Service 类是WeatherServiceImplService,通过它获取端口WeatherService,然后调方法:
@Service public class WeatherClient { public WeatherQueryResult query(String city) { WeatherServiceImplService service = new WeatherServiceImplService(); WeatherService port = service.getWeatherServicePort(); QueryWeatherRequest request = new QueryWeatherRequest(); request.setCity(city); QueryWeatherResponse response = port.queryWeather(request); return new WeatherQueryResult(response.getTemperature(), response.getCondition()); } }如果你是 Spring 风格,也可以把WeatherServiceImplService定义成 Bean,因为 JAX-WS 客户端的 Service 对象是线程安全的,多个请求可以共享同一个代理。
@Bean public WeatherServiceImplService weatherServiceFactory() { return new WeatherServiceImplService(); } @Bean public WeatherService weatherService(WeatherServiceImplService factory) { return factory.getWeatherServicePort(); }这样业务代码里直接注入WeatherService接口,干净很多。
3.4 业务调用与异常处理细节
调用外部 WebService 是典型的不可靠操作,必须设置超时时间。静态方式下,超时配置要走到BindingProvider层:
BindingProvider bp = (BindingProvider) port; Map<String, Object> ctx = bp.getRequestContext(); ctx.put("com.sun.xml.ws.connect.timeout", 3000); ctx.put("com.sun.xml.ws.request.timeout", 15000);这里connect.timeout是建立连接的超时,request.timeout是等待服务端响应的超时。单位是毫秒,这两个参数名字来自 JAX-WS RI 的实现,如果你用的是别的 WebService 框架,参数名可能不同。
调用过程中可能抛出的异常主要分两类:传输层异常(连接超时、Socket 异常)和 SOAP Fault(服务端业务异常)。SOAP Fault 会被 JAX-WS 包装成SoapFaultException,异常消息里能看到 fault code 和 fault string,尽量把这些信息透传给上层,方便排查。
我建议在封装层加统一异常转换,把 WebService 调用异常翻译成业务异常码,避免上游服务被无关的 SoapFault 细节淹没:
try { return port.queryWeather(request); } catch (SoapFaultException e) { throw new BusinessException("WEATHER_SERVICE_ERROR", "天气服务返回错误: " + e.getMessage()); }4. 动态调用更通用的封装思路
4.1 手工定义 JAXB 请求响应对象
动态方案不生成代理类,而是自己定义请求和响应对象。以天气服务举例:
@XmlRootElement(name = "queryWeather", namespace = "http://www.example.com/weather") @XmlAccessorType(XmlAccessType.FIELD) public class QueryWeatherRequest { @XmlElement(name = "city", namespace = "http://www.example.com/weather") private String city; public String getCity() { return city; } public void setCity(String city) { this.city = city; } }响应对象同理,把服务端返回的 XML 字段映射进来。写的时候多对着 WSDL 检查命名空间和元素名,一个字母拼错,序列化后服务端就找不到方法。
这里分享一个技巧:先把 WSDL 对应的 XSD 打开,找到目标元素的完整限定名,再照着写注解。XSD 的targetNamespace和element name基本决定了一切。我曾经遇到过命名空间写错导致的诡异报错,服务端一直返回“找不到匹配的操作”,排查一小时后才发现,原来 XSD 里 namespace 末尾是有斜杠的,我少写了一层路径,这种细节靠肉眼扫代码很难发现。
4.2 WebServiceTemplate 的配置与发送
动态调用的核心是 Spring WS 命名空间,也就是spring-boot-starter-web-services里带的org.springframework.ws.client.core.WebServiceTemplate。它需要一个 Marshaller 来把 Java 对象序列化成 XML,配置如下:
@Configuration public class WebServiceClientConfig { @Bean public Jaxb2Marshaller jaxb2Marshaller() { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath("com.example.weather.dto"); return marshaller; } @Bean public WebServiceTemplate webServiceTemplate(Jaxb2Marshaller jaxb2Marshaller) { WebServiceTemplate template = new WebServiceTemplate(jaxb2Marshaller); template.setDefaultUri("http://www.example.com/ws/weather"); template.setMarshaller(jaxb2Marshaller); template.setUnmarshaller(jaxb2Marshaller); return template; } }我没把默认 URI 写死成直接带?wsdl,因为生产环境调用时,实际的 SOAP 地址是 WSDL 里<soap:address location="...">指定的那个,它可能跟 WSDL 地址不同。这也是一个容易踩的坑:有人直接拿http://www.example.com/ws/weather?wsdl当作调用地址,服务端直接报错。调用地址应该取自 WSDL 里的 service port location。
接下来定义调用逻辑:
@Component public class WeatherServiceClient { private final WebServiceTemplate webServiceTemplate; public WeatherServiceClient(WebServiceTemplate webServiceTemplate) { this.webServiceTemplate = webServiceTemplate; } public QueryWeatherResponse queryWeather(String city) { QueryWeatherRequest request = new QueryWeatherRequest(); request.setCity(city); return (QueryWeatherResponse) webServiceTemplate.marshalSendAndReceive(request); } }marshalSendAndReceive内部会完成三步:marshaller 序列化请求对象,HTTP 发请求,unmarshaller 把响应 XML 转成 Java 对象。全程无需手写 XML 报文。
4.3 动态设置 SOAPAction 和自定义 Header
有些服务端严格要求SOAPAction头的值必须等于某个固定字符串,默认情况下 Spring WS 会根据命名空间和方法名推导,但不一定跟服务端要求一致。解决方式是用WebServiceMessageCallback在发送前手动设置:
QueryWeatherResponse response = (QueryWeatherResponse) webServiceTemplate .marshalSendAndReceive(request, new WebServiceMessageCallback() { @Override public void doWithMessage(WebServiceMessage message) { SoapMessage soapMessage = (SoapMessage) message; soapMessage.setSoapAction("http://www.example.com/weather/queryWeather"); } });同样的回调机制还能加自定义 SOAP Header。比如某些金融接口要求把身份令牌放进 Header,可以这样实现:
SoapHeader header = soapMessage.getSoapHeader(); QName headerQName = new QName("http://www.example.com/auth", "token"); SoapHeaderElement element = header.addHeaderElement(headerQName); element.setText("your-token-value");这套回调机制是动态方案最大的红利。REST 接口你可以在拦截器里统一加认证信息,SOAP 接口同样可以在回调里统一处理,所有第三方服务共享一段逻辑。
4.4 动态方案的适用边界
动态方案代码量不一定比静态少,尤其是 WSDL 里嵌套复杂的 complexType 时,手工定义 JAXB 类会让你怀疑人生。我的建议是,只有当接口数量少、报文结构简单、但又需要频繁微调 Header 或命名空间时,才钦定动态方案。如果面对的是几十个方法上百个类型的重量级 WSDL,静态生成再包一层防腐层,性价比远高于手工建模。
另外,有人会问能不能直接传 XML String,不走 JAXB 对象?技术上是可行的,用StringSource当请求源,但这等于放弃了类型安全和 marshaller 的自动映射能力,只适合调试期临时验证报文,不建议落到生产代码里。我调试时倒是经常这么干,敲一个测试接口,手写 XML 报文直接丢给 WebServiceTemplate,快速定位问题,但正式代码还是要走对象建模。
5. 超时、日志、认证,一个都不能省
5.1 超时设置不生效的真相
动态方案里,超时配置必须作用在 HTTP 客户端上,而 WebServiceTemplate 默认使用的连接工厂未必让你调底层超时参数。推荐用 HttpComponentsClientHttpRequestFactory 替换默认工厂,它就是基于 Apache HttpClient 的封装:
@Bean public WebServiceTemplate webServiceTemplate(Jaxb2Marshaller jaxb2Marshaller) { HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(15000); factory.setConnectionRequestTimeout(3000); WebServiceTemplate template = new WebServiceTemplate(jaxb2Marshaller); template.setRequestFactory(factory); template.setDefaultUri(soapAddress); return template; }这组参数的含义:
setConnectTimeout:建立 TCP 连接的超时,过长会让故障感知变慢;setReadTimeout:服务端响应超时,外部系统挂死时避免线程池被拖垮;setConnectionRequestTimeout:从连接池获取连接的超时,高并发下更关键。
如果用的是 Apache HttpClient,还需要引入依赖:
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency>很多 SpringBoot 2.x 项目本身就带 httpclient,没有就加一个。改了配置发现超时还是不生效,先确认是不是有两个 WebServiceTemplate Bean 相互覆盖,这是我真实遇到过的坑——一个配置类里定义了 Template,另一个自动配置又注册了一个默认的,注入到业务代码里的是默认的那个,调多久都看不到超时日志。
5.2 抓 SOAP 报文的正确打开方式
WebService 联调阶段,最痛苦的是服务端说“你报文有问题”,但你不知道你发的是什么。这时候有两招:
第一招,Spring WS 自带日志,只要在配置里打开:
logging: level: org.springframework.ws.client.MessageTracing: TRACE这个开关会打印发送和接收的 SOAP 报头、报文主体,简单可靠。我用它比抓包工具更频繁,毕竟日志直接和代码执行栈对应。
第二招,写一个 ClientInterceptor 在发送前后截报文,适合把报文存进单独日志文件或表里:
public class LoggingClientInterceptor implements ClientInterceptor { @Override public boolean handleRequest(MessageContext messageContext) { WebServiceMessage request = messageContext.getRequest(); log.debug("request payload: {}", messageToString(request)); return true; } @Override public boolean handleResponse(MessageContext messageContext) { WebServiceMessage response = messageContext.getResponse(); log.debug("response payload: {}", messageToString(response)); return true; } @Override public boolean handleFault(MessageContext messageContext) { // 同样打印 fault 报文 return true; } @Override public void afterCompletion(MessageContext messageContext, Exception ex) { // 收尾处理,比如计时 } private String messageToString(WebServiceMessage message) { try { TransformerFactory transformerFactory = TransformerFactory.newInstance(); Transformer transformer = transformerFactory.newTransformer(); StringWriter writer = new StringWriter(); transformer.transform(message.getPayloadSource(), new StreamResult(writer)); return writer.toString(); } catch (TransformerException e) { return "failed to serialize message"; } } }注册拦截器:
template.setInterceptors(new ClientInterceptor[]{new LoggingClientInterceptor()});生产环境建议把原始报文写在单独的日志文件里,方便对账和问题回溯。
5.3 认证:HTTP Basic 与 WS-Security 处理
第三方 WebService 最常见的安全控制是 HTTP Basic 认证,JAX-WS 静态调用里直接设置请求上下文属性:
Map<String, Object> requestContext = ((BindingProvider) port).getRequestContext(); requestContext.put(BindingProvider.USERNAME_PROPERTY, "serviceUser"); requestContext.put(BindingProvider.PASSWORD_PROPERTY, "servicePass");动态调用时,则需要改造底层的 Http 请求头。最简单的方式是给连接工厂加一个拦截器,或者在doWithMessage回调里直接操作消息头,如果服务端用的是 HTTP Header 这种验证方式,也可以在发送前设置 HttpHeaders:
((WebServiceTemplate) template).setMessageSender(new HttpUrlConnectionMessageSender() { @Override protected void prepareConnection(HttpURLConnection connection) { String auth = "Basic " + Base64.getEncoder().encodeToString(("user:pass").getBytes(StandardCharsets.UTF_8)); connection.setRequestProperty("Authorization", auth); } });注意,上面这个写法依赖标准 HttpURLConnection,如果你切换了 Apache HttpClient,也许要换一个实现类。一个更通用的做法是使用 ClientHttpRequestFactory 的拦截器扩展,例如定义一个拦截器的抽象层,统一注入认证头,具体细节根据项目使用的基础库来定。
对于安全性要求更高的接口,用的是 WS-Security 标准,需要在 SOAP Header 里嵌入 Security 节点。这种情况一般引入 WSS4J 体系来做报文签名与加密,直接用 Spring WS 的XwsSecurityInterceptor来支持。配置比较繁琐,但原理上就是在发送前拦截报文,往 Header 里塞 UsernameToken 或签名。这部分我建议独立一个章节写,因为涉及密钥库、证书等额外配置,几句话说不清。
无论哪种认证,核心原则都一样:不要在业务代码里散落认证逻辑,统一沉淀在客户端封装层。我之前接手过一个项目,每个调用 WebService 的地方都写着独立的认证代码,后来证书轮换的时候,改了十几处才改完,整得人差点想跑路。
6. 高频故障排查清单
6.1 启动期 JAXB 类找不到或 marshaller 上下文为空
SpringBoot 2.x 在不同版本下启动时,可能报ClassNotFoundException: javax.xml.bind.JAXBException或javax.xml.bind.UnmarshalException。原因通常是 JDK 9+ 移除了 JAXB 模块,而项目依赖没有显式引入 JAXB API 与实现。解决方式是加依赖:
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.1</version> </dependency>SpringBoot 3.x 环境里,javax.* 变成了 jakarta.*,如果你还在用旧的 wsimport 生成代码,基本上会碰到命名空间不兼容的问题。这种情况下优先考虑 Spring WS 3.x 方案,或者直接运行动态调用,避免老代码带来的适配工作量。
6.2 调用返回 200,但解析出来的对象全是 null
这种坑最隐蔽,命中的概率极高。HTTP 层面一切正常,服务端确实返回了完整 XML,但响应对象字段全是空。常见原因有三个:
- 响应对象里的
@XmlElement命名空间写得不对,JAXB 找不到对应元素; - 服务端返回的是复合结构,比如字段包在某个
wrapper节点下,而你的对象没定义对应嵌套类; - 响应对象用了全局 XML 解析配置,某个字段类型不匹配,报文被静默忽略了。
排查方式很简单:把响应原始报文打出来,逐段跟响应对象的注解对照,重点看 namespace、element name、类型是否一致。我通常直接把报文存进测试用例,先手动解析一遍确认结构,再重建对象模型,效率很高。
6.3 HTTPS 证书异常
现在很多 WebService 都迁到了 HTTPS 协议,联调时常见的报错包括PKIX path building failed、SSLHandshakeException、unable to find valid certification path等。测试环境最省事的处理方式是先临时信任该服务端证书,写一个测试专用证书信任管理器,但别把它带到生产代码里。
生产环境正确做法是:拿到第三方提供的服务器证书或根证书,导入本地 JVM 的cacerts信任库,或者做成独立 truststore 给连接器使用。我之前对接一个政务平台时,就专门维护了一个 truststore 文件,通过启动参数-Djavax.net.ssl.trustStore=指定,这样换环境换证书时,不用重新发版。
需要留意的是,WebServiceTemplate 使用 Apache HttpClient 时,它的 SSL 配置和 JVM 默认行为可能不完全一致,所以还是建议直接参考 HttpClient 的 SSLContext 定制方式去做。
6.4 SOAP Fault 怎么读
服务端返回的业务异常会以 SOAP Fault 形式出现。静态调用和动态调用都可能在客户端抛出异常,但要看清楚 fault code、fault reason、detail 三个部分。fault code 通常是soapenv:Client或soapenv:Server,fault reason 是给人看的信息,detail 里可能包含服务端错误码。
建议在项目封装层写一个统一的异常解析函数,把这三个信息拆开记录日志。遇到“服务端明明报错,你这边日志只有 SoapFaultException 一行”的情况,基本就是没有单独记录 detail,调试时少了很多上下文。
6.5 一个容易被忽略的 URL 细节
WSDL 地址、调用地址、XSD import 地址三个概念不要混为一谈。有人直接用 WSDL 地址当调用地址,有人把 XSD import 的地址映射成本地路径却忘了改 WSDL 引用,还有人在服务端切换了域名后,WSDL 没变但调用地址已经变了,这些问题排查起来都挺费劲。建议封装配置时,将wsdlLocation、soapActionUri、soapAddress分开配置,三者各归各,后续改起来也清晰。
WebService 这块说实话没什么魔法,原理熟练了,剩下的就是耐心和细致的报文分析。我个人做过好几个对接项目下来,最深刻的一个体会是:最好的排错手段不是反复改代码重启,而是把原始报文完整抓出来看清楚。很多看上去玄乎的问题,只要把收到的 XML 和 WSDL 定义并排放一起,答案就浮在表面。接这种老协议接口,技术难度未必高,但信息不对等造成的摩擦一定少不了,稳扎稳打反而是最快的路径。