SpringBoot项目里调用老系统WebService接口,我踩过的那些坑(附完整代码)
SpringBoot集成遗留系统WebService的血泪史:从WSDL解析到异常处理全指南
第一次接手SpringBoot与老掉牙的C#系统对接任务时,我天真地以为这不过是简单的API调用。直到看见对方发来的WSDL文件里那些错综复杂的XML命名空间和SOAP 1.1规范,才意识到自己即将开启一段"填坑"之旅。本文将还原我如何用Java 11+SpringBoot 2.7啃下这块硬骨头的全过程,重点分享那些文档里永远不会写的实战细节。
1. 环境准备:当现代Java遇上古董级WebService
在开始编码前,先准备这些"生存物资":
<!-- 基础依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web-services</artifactId> </dependency> <dependency> <groupId>com.sun.xml.ws</groupId> <artifactId>jaxws-rt</artifactId> <version>2.3.3</version> </dependency> <!-- XML处理三件套 --> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> </dependency> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.13</version> </dependency>注意:如果遇到JAXB相关报错,可能需要添加
--add-modules java.xml.bindJVM参数
2. WSDL解析的三大陷阱
2.1 命名空间的地狱嵌套
老系统的WSDL里经常出现这样的结构:
<xsd:schema targetNamespace="http://tempuri.org/"> <xsd:import namespace="http://schemas.microsoft.com/2003/10/Serialization/"/> <xsd:element name="GetData"> <xsd:complexType> <xsd:sequence> <xsd:element minOccurs="0" name="key" nillable="true" type="xsd:string"/> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:schema>解决方案是使用wsimport生成代码时指定包映射:
wsimport -keep -p com.example.ws.client \ -b custom.xml http://old-system/Service.svc?wsdl2.2 缺失的XSD依赖
当遇到Unable to locate imported document at...错误时,需要手动下载缺失的XSD:
@Configuration public class WsConfig extends WsConfigurerAdapter { @Override public void addInterceptors(List<EndpointInterceptor> interceptors) { interceptors.add(new PayloadValidatingInterceptor() {{ setSchema(new ClassPathResource("schemas/missing.xsd")); }}); } }2.3 日期格式的世纪之争
.NET的DateTime与Java的XMLGregorianCalendar转换:
public class DateAdapter { public static XMLGregorianCalendar toXmlDate(LocalDateTime date) { try { return DatatypeFactory.newInstance() .newXMLGregorianCalendar(date.toString()); } catch (Exception e) { throw new RuntimeException(e); } } }3. 请求构建的黑暗艺术
3.1 SOAP Header的认证难题
老系统常用的Basic Auth实现方式:
public class SecurityHeaderBuilder implements WebServiceMessageCallback { private final String username; private final String password; @Override public void doWithMessage(WebServiceMessage message) { SoapMessage soapMessage = (SoapMessage) message; SoapHeader header = soapMessage.getSoapHeader(); StringCredentials credentials = new StringCredentials() {{ setUsername(username); setPassword(password); }}; Marshaller marshaller = new Jaxb2Marshaller(); marshaller.marshal(credentials, header.getResult()); } }3.2 二进制附件传输
处理老系统用MTOM发送的附件:
@Bean public Jaxb2Marshaller marshaller() { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setMtomEnabled(true); marshaller.setContextPath("com.example.ws.model"); return marshaller; }4. 响应处理的九死一生
4.1 动态XML解析策略
当返回结构不确定时,采用XPath解析:
public class DynamicResponseParser { private static final NamespaceContext NS_CONTEXT = new NamespaceContext() { public String getNamespaceURI(String prefix) { return "http://tempuri.org/"; } // 其他方法实现... }; public String parseResponse(String xml, String xpathExpr) throws XPathExpressionException { XPath xpath = XPathFactory.newInstance().newXPath(); xpath.setNamespaceContext(NS_CONTEXT); InputSource source = new InputSource(new StringReader(xml)); return xpath.evaluate(xpathExpr, source); } }4.2 异常处理的俄罗斯套娃
典型的老系统错误响应结构:
<soap:Fault> <faultcode>soap:Server</faultcode> <faultstring>Exception occurred</faultstring> <detail> <ExceptionDetail> <StackTrace>...</StackTrace> <InnerException> <Message>真正的错误信息在这里</Message> </InnerException> </ExceptionDetail> </detail> </soap:Fault>对应的异常处理器:
@ControllerAdvice public class SoapFaultTranslator { @ExceptionHandler(SoapFaultClientException.class) public ResponseEntity<String> handleFault(SoapFaultClientException ex) { String detail = extractDetail(ex.getSoapFault()); return ResponseEntity.status(500).body(detail); } private String extractDetail(SoapFault fault) { // 使用DOM或XPath解析detail节点 } }5. 性能优化的秘密武器
5.1 连接池配置
避免每次创建新连接:
@Bean public WebServiceTemplate webServiceTemplate() { WebServiceTemplate template = new WebServiceTemplate(); template.setMessageSender(new HttpComponentsMessageSender() {{ setHttpClient(HttpClients.custom() .setMaxConnTotal(20) .setMaxConnPerRoute(10) .build()); }}); return template; }5.2 缓存策略
对静态WSDL启用缓存:
# application.properties spring.webservices.wsdl-locations=classpath:/wsdl/ spring.webservices.cache=true6. 调试技巧:没有文档时的生存指南
6.1 流量镜像工具
使用SoapUI或Postman录制请求:
# 用mitmproxy抓包 mitmproxy -p 8080 --mode reverse:http://old-system:806.2 动态日志调整
临时开启SOAP消息日志:
@Configuration @EnableWs public class WebServiceConfig extends WsConfigurerAdapter { @Override public void addInterceptors(List<EndpointInterceptor> interceptors) { interceptors.add(new PayloadLoggingInterceptor()); } }在项目上线三个月后,我们终于将调用成功率从最初的62%提升到99.9%。最关键的教训是:永远要对老系统保持敬畏之心,它的每个看似古怪的设计背后,可能都藏着一段血泪史。
