背景
SOAP(Simple Object Access Protocol)是上世纪的远程调用协议,基于 XML,通常走 HTTP 传输。虽然现在 RESTful API 和 gRPC 已经是主流,但一些政府、金融、国企的老系统仍然只提供 SOAP / Web Service 接口。
对接这类接口时,传统做法是引入 CXF、Axis2 等 SOAP 框架,通过 WSDL 生成客户端代码。但如果你只是偶尔调一两个接口,引入几百 MB 的依赖、增加部署体积,实在没必要。
实际上 SOAP 本质上就是一个 带特定 Content-Type 的 HTTP POST 请求,body 是一段 XML。完全可以用任何 HTTP 客户端(OkHttp、HttpClient、RestTemplate 等)手动构造。
SOAP 协议要点
1. SOAP 消息结构
一个 SOAP 请求本质上是这样一段 XML:
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="http://your.namespace.com/">
<soapenv:Header/>
<soapenv:Body>
<ns:methodName>
<param1>value1</param1>
<param2>value2</param2>
</ns:methodName>
</soapenv:Body>
</soapenv:Envelope>
核心结构:
- Envelope:根元素,固定命名空间
http://schemas.xmlsoap.org/soap/envelope/(SOAP 1.1) - Header:可选,放认证/路由等信息,可为空
<soapenv:Header/> - Body:实际请求内容,包含你要调用的方法和参数
2. HTTP 层面的三个关键点
要让服务端正确识别为 SOAP 请求,HTTP 层面必须满足:
| 要点 | 说明 |
|---|---|
| Content-Type | SOAP 1.1 用 text/xml; charset=UTF-8;SOAP 1.2 用 application/soap+xml; charset=UTF-8 |
| SOAPAction 头 | SOAP 1.1 必须带 SOAPAction 头,即使值为空字符串 "";SOAP 1.2 不需要 |
| 请求 URL | 去掉 ?wsdl,?wsdl 是获取接口描述文档的地址,不是实际调用端点 |
3. 命名空间
WSDL 中会定义 targetNamespace,这就是 Body 中方法名和参数的命名空间。必须与服务端一致,否则服务端无法匹配到对应的方法。
从服务端的 响应报文 可以反推命名空间:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns1:methodNameResponse xmlns:ns1="http://instru.server.ws.com/">
<return>200</return>
</ns1:methodNameResponse>
</soap:Body>
</soap:Envelope>
上面响应中 xmlns:ns1="http://instru.server.ws.com/" 就是请求时应该使用的命名空间。
代码实现
完整示例
public int callSoapService(String url, String namespace, String method,
Map<String, String> params) {
// 1. 构造 SOAP XML 报文
StringBuilder paramXml = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
paramXml.append(" <")
.append(entry.getKey()).append(">")
.append(entry.getValue())
.append("</").append(entry.getKey()).append(">\n");
}
String soapRequest = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"" +
" xmlns:ns=\"" + namespace + "\">\n" +
" <soapenv:Header/>\n" +
" <soapenv:Body>\n" +
" <ns:" + method + ">\n" +
paramXml +
" </ns:" + method + ">\n" +
" </soapenv:Body>\n" +
"</soapenv:Envelope>";
// 2. 去掉 URL 中的 ?wsdl
String endpoint = url.replace("?wsdl", "");
try {
OkHttpClient client = new OkHttpClient().newBuilder()
.connectTimeout(300, TimeUnit.SECONDS)
.readTimeout(300, TimeUnit.SECONDS)
.writeTimeout(300, TimeUnit.SECONDS)
.build();
// 3. Content-Type 用 text/xml(SOAP 1.1)
RequestBody requestBody = RequestBody.create(
MediaType.parse("text/xml; charset=UTF-8"), soapRequest);
// 4. 添加 SOAPAction 头
Request request = new Request.Builder()
.url(endpoint)
.post(requestBody)
.addHeader("Content-Type", "text/xml; charset=UTF-8")
.addHeader("SOAPAction", "\"\"")
.build();
Response response = client.newCall(request).execute();
String responseBody = response.body().string();
// 5. 解析返回值
Pattern p = Pattern.compile(".+<return>(\\d+)</return>.+");
Matcher m = p.matcher(responseBody);
if (m.find()) {
return Integer.parseInt(m.group(1));
}
return 0;
} catch (Exception e) {
log.error("SOAP 调用失败", e);
return -1;
}
}
调用示例
Map<String, String> params = new HashMap<>();
params.put("insCode", "your-code-here");
params.put("instruType", "4");
int result = callSoapService(
"http://example.com:8080/services/cxf/instru?wsdl", // 配置中的 URL 带 ?wsdl
"http://instru.server.ws.com/", // 从响应报文反推的命名空间
"instruInfo", // WSDL 中定义的操作名
params
);
常见坑
坑 1:URL 带 ?wsdl
对方给你的地址通常长这样:
http://xxx.com/services/cxf/instru?wsdl
在浏览器打开会返回一大段 XML(WSDL 描述文档),这是给你看接口定义的,不是给你发请求的。实际 SOAP 调用的端点是去掉 ?wsdl 后的地址:
http://xxx.com/services/cxf/instru
坑 2:Content-Type 用错
最容易犯的错误是用了 application/xop+xml。这是 MTOM(Message Transmission Optimization Mechanism)的 Content-Type,用于 SOAP 带二进制附件传输的场景。如果你只是发送纯 XML 文本,用这个类型,服务端可能无法正确解析。
| Content-Type | 用途 |
|---|---|
text/xml; charset=UTF-8 |
SOAP 1.1 标准 |
application/soap+xml; charset=UTF-8 |
SOAP 1.2 标准 |
application/xop+xml; charset=UTF-8 |
MTOM(带二进制附件) |
坑 3:缺少 SOAPAction 头
SOAP 1.1 规范要求 HTTP 请求必须包含 SOAPAction 头。即使没有具体的 action 值,也要传空字符串 ""(注意是带引号的空字符串)。很多服务端框架(如 Apache CXF)会先检查这个头来判断是否为 SOAP 请求。
.addHeader("SOAPAction", "\"\"")
SOAP 1.2 不需要此头,action 通过 Content-Type 中的 action 参数传递。
坑 4:命名空间不匹配
WSDL 中的 targetNamespace 必须与请求 XML 中的命名空间一致。如果用了错误的命名空间(比如从网上复制的示例代码里带的占位符 http://my.server.ws.com/),服务端会找不到对应的方法。
如何确认正确的命名空间:
- 打开
?wsdl地址,搜索targetNamespace - 或者看服务端返回的响应报文,
xxxResponse元素上的xmlns:ns1="..."就是
坑 5:CDATA 包裹 JSON 字符串
如果 SOAP 参数的值本身是 JSON 字符串,需要用 <![CDATA[...]]> 包裹,避免 XML 解析器把 JSON 中的特殊字符(<、&、> 等)当作 XML 标记:
String cdataJson = "<param><![CDATA[" + jsonString + "]]></param>\n";
坑 6:手动设置 Content-Length
OkHttp / HttpClient 等库会自动根据请求体字节数设置 Content-Length。手动设置容易因为编码问题导致长度不一致(string.length() 返回的是字符数,不是字节数)。不要手动设置。
如何调试
1. 先用 SoapUI 验证
在写代码之前,先用 SoapUI 导入 WSDL 测试接口。SoapUI 会自动生成正确的 SOAP 请求模板,你可以参考它的 Content-Type、SOAPAction 和 XML 结构。
2. 打印请求和响应
log.info("SOAP Request:\n{}", soapRequest);
log.info("SOAP Response:\n{}", responseBody);
3. 用 curl 验证
curl -X POST "http://example.com/services/cxf/instru" \
-H "Content-Type: text/xml; charset=UTF-8" \
-H "SOAPAction: \"\"" \
-d '<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns="http://your.namespace.com/">
<soapenv:Header/>
<soapenv:Body>
<ns:methodName>
<param>value</param>
</ns:methodName>
</soapenv:Body>
</soapenv:Envelope>'
curl 能通,代码就能通。如果 curl 不通,说明协议本身有问题,先调通 curl 再写代码。
总结
| 项目 | 传统做法 | 手动构造 |
|---|---|---|
| 依赖 | CXF / Axis2(几十 MB) | OkHttp(几 MB,项目通常已有) |
| 部署体积 | 大 | 无额外增加 |
| 学习成本 | 需了解 WSDL 生成、JAXB 绑定等 | 只需理解 SOAP 报文结构 |
| 适用场景 | 大量 SOAP 接口、复杂类型 | 偶尔对接一两个接口 |
SOAP 本质就是 特定格式 XML + 特定 HTTP 头的 POST 请求,理解了这一点,就不需要被框架绑架。
文章评论