不引入 SOAP 类库,用 OkHttp 手动构造 SOAP 请求

2026-09-09 2点热度 0人点赞 0条评论

背景

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/),服务端会找不到对应的方法。

如何确认正确的命名空间:

  1. 打开 ?wsdl 地址,搜索 targetNamespace
  2. 或者看服务端返回的响应报文,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 请求,理解了这一点,就不需要被框架绑架。

admin

这个人很懒,什么都没留下

文章评论

您需要 登录 之后才可以评论