基于 JSON 的 REST 服务很流行,但是在集成企业服务时,SOAP 仍然被广泛使用。在最近的一个项目中,我不得不编写一个基于 spring boot 的微服务,它是一种基于 Microsoft WCF 的 第三方基于 SOAP 的 Web 服务的网关。调用时,微服务从其他微服务收集一些数据,从存储系统加载 PDF 文档,并在一次 SOAP 调用中将数据和 PDF 传输到 SOAP 服务。当第一个实现完成并部署后,我检查了访问日志:哇,某些请求的请求大小非常大。好的,PDF 的大小从几 kB 到几 MB 不等,但请求似乎要大得多。这是因为 SOAP 使用 Base64 对二进制内容进行编码。而Base64将6个Bits编码成一个字符,即3个Bytes被编码成4个字符。这比原始数据多 33% 的内容。对于小内容,这不是问题,但如果您必须传输 4MB 而不是 3MB……那就是问题了。下面是一个包含二进制内容的简单 SOAP 请求示例。内容只有 30 个字节,编码为 40 个字符(为了便于阅读,XML 已经格式化):
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
MTOM
为了克服这个问题,W3C 发明并标准化了 MTOM (消息传输优化机制)。不是以 Base64 编码的字符形式提供二进制内容(作为元素文本包含在 SOAP 消息中),而是使用多部分 mime 格式来从 SOAP 消息中分离二进制内容。意思是:SOAP消息本身包含在一个部分中,二进制内容在一个单独的部分中。 SOAP 消息中的内容元素仅具有对二进制部分的引用。听起来怪怪的?看看它(同样,XML 的格式是为了便于阅读):
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
如您所见,包含二进制数据的部分仅包含 30 个字节,而不是更多。但是对于您必须支付的多部分元数据肯定会有一些开销。根据经验,MTOM 仅对 > 1 kB 的内容有意义。如果您查看内容元素,您会注意到 xop 元素:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
虽然 MTOM 描述了 SOAP 中优化传输的抽象特征,但使用 MIME 多部分的具体实现保留在单独的规范中: XOP ,即 XML-binary Optimized Packaging。这样,它就可以独立于 SOAP 用于 XML 文档中的任何二进制内容。因此,您经常会发现 MTOM/XOP 的措辞。
示例应用程序
为了给你一个简单的例子,我
在 github 上准备了一个简单的 SOAP 服务器和客户端,
用 spring boot 实现。它们是使用
STS
构建的,但您可以仅使用纯 Java 构建和运行它们;只需检查自述文件。克隆存储库并切换到分支
base64
,它提供了两个 spring 项目
mtom-server
和
mtom-client
初始设置。只需按照自述文件中的描述启动服务器和客户端。客户要求您提供要上传的文档大小。如果您输入一个大小,客户端将生成一个该大小的文档(包含一些随机二进制数据),并将其上传到服务器。客户端和服务器也会跟踪请求和响应,因此您可以检查它们。
客户端控制台输出:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
服务器控制台输出:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
SOAP 服务的基础通常是定义类型和操作的 WSDL。在我们的例子中,我们只是在服务器项目中提供一个描述类型的模式
documents.xsd
并使用 JAXB 来创建 Java 类。 Spring 为我们创建了 WSDL 而不是动态的。只需启动服务器并浏览到
http://localhost:9090/ws/documents.wsdl
。 WSDL 也包含在 wsdl/documents.wsdl 下的资源中的客户端中。以下是 WSDL 的摘录:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
所以我们只需要在
storeDocument
中有一个
document
,在
storeDocumentResponse
中
storeDocumentRequest
一个布尔值。
document
本身包含一些元数据,如
name
和
author
,最后是二进制
content
。
使用 MTOM
为了使用 MTOM,我们必须对示例进行一些更改。 (您可以在示例中执行这些步骤,也可以直接在分支
mtom
中查看准备好的解决方案)。首先让我们在客户端启用 MTOM,这只是在 JAXB 编组器上启用它:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
基本上,这与我们在服务器上也必须做的一样。但我们还必须告诉 spring 为端点使用该编组器:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
我们还必须通过提供适当的多部分解析器来添加对多部分的支持:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
就是这样,如果你运行它,你将在客户端控制台上输出以下内容:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
MTOM 和流媒体
如果查看从模型生成的 Java 类,您会看到二进制内容保存在字节数组中:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
当你处理大的二进制数据时,这是一个问题,因为完整的数据必须保存在内存中。
OutOfMemoryException
正在等待您。这个问题的解决方案是流式传输:不是将数据保存在内存中,而是以流的形式提供数据。从流中读取数据。这是很自然的,因为大多数数据存储(例如文件系统、数据库等)都提供流接口。即使 MTOM 规范只字未提流式传输,XOP 规范也指出——即使它不是强制性的——大多数实现都提供了流式传输数据的可能性。让我们现在在我们的例子中这样做。同样,您可以按照这些步骤将您的 MTOM 应用程序转换为流,或检查项目的
master
分支。大师为您提供最终版本。首先,我们需要一种方法在我们的 Java 类中提供流接口。这样做的方法是稍微更改架构。只需将属性
xmime:expectedContentTypes="application/octet-stream"
添加到内容元素:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
现在 JAXB 为字段内容生成一个
DataHandler
而不是
byte[]
:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
DataHandler 基于流,因此您可以使用 Input- 和 OutputStreams 来传递数据。这就是我们所做的;我们没有先将内容读入字节数组,而是直接将输入流传递给客户端的 DataHandler:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
在服务器端,我们还有一个 DataHandler,我们用它来直接从 InputStream 读取数据。就是这样?试一试……嗯,还是内存不足。客户端似乎仍然先将内容读入内存。这个问题的答案是 HTTP 协议,让我们回顾一下我们的 MTOM 请求:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
HTTP 需要预先知道content-length,所以为了计算content-length,将content 全部读入内存。为了避免这种情况,我们必须使用 分块传输编码 :
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
现在我们得到了吗?让我们重试... 神圣的吉娃娃,现在服务器发出呻吟声 。但它现在应该马上工作?!?这是 SAAJ 实现中的错误,请参阅 SAAJ-31 。你可以在那里读到,我们必须设置一个开关来强制 SAAJ 使用 mimepull,所以我们这样做:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
现在它起作用了……真的。尝试使用 1.000.000.000 字节(SOAP 跟踪在 master 中被禁用,所以不用担心)。这需要一些时间,因为我们的随机数据 InputStream 会生成每个字节:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
WCF 中的 MTOM 和流式处理
一开始我告诉过你我的项目,在这个项目中我必须与构建在 .NET 和 WCF 上的第三方服务器产品进行通信。该产品最初没有使用 MTOM,因此我也不得不对该软件进行更改。幸运的是,在 WCF 中,这只是您必须做的
一些配置
……而且我可以访问该配置文件;-) 在 HTTP 绑定中,您必须将属性
messageEncoding
设置为 Mtom。要使用流式传输,只需将属性
transferMode
设置为 Streamed:
POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns2:storeDocumentRequest xmlns:ns2="https://github.com/ralfstuckert/mtom">
<ns2:document>
<ns2:name>30</ns2:name>
<ns2:author>Herbert</ns2:author>
<ns2:content>zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw</ns2:content>
</ns2:document>
</ns2:storeDocumentRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
那很容易,是吗?是的,WCF 会为您处理所有这些漂亮的小细节。
结论
MTOM 允许您有效地传输大型二进制数据,甚至允许流式传输它以避免内存问题。是的,还有其他可用的流媒体机制;但是如果您希望您的 SOAP 服务与其他服务互操作,则 MTOM 标准是您的选择。
使困惑?你不会在这一集 Soap 之后!
Soap 的播音员,我喜欢看的 70 年代后期情景喜剧
:-)