在SOAP中使用MTOM来有效地传输二进制内容


基于JSON的REST服务正在流行,但在整合企业服务时,SOAP仍然被广泛使用。在最近的一个项目中,我不得不编写一个基于spring boot的微服务,它是第三方 基于SOAP 的网络服务的一种网关,建立在 微软WCF 上。当被调用时,该微服务从其他微服务中收集一些数据,从存储系统中加载一个PDF文档,并在一次SOAP调用中把数据和PDF传输到SOAP服务中。当第一个实现完成并部署后,我检查了访问日志:哇,一些请求的大小是巨大的。好吧,PDF 的大小从几 kB 到几 MB 不等,但请求似乎要大得多,大得多。这是因为 SOAP 使用 Base64 来编码二进制内容。而Base64对一个字符进行6位编码,意味着3个字节被编码成4个字符。这就比原始数据多了33%的内容。对于小的内容来说,这不是一个问题,但如果你必须传输4MB而不是3MB......这就是一个问题。下面是一个简单的带有二进制内容的SOAP请求的例子。该内容只有30个字节,被编码为40个字符(为了便于阅读,XML已经被格式化)。

POST /ws/documents HTTP/1.1
...
Content-Type: text/xml; charset=utf-8
Content-Length: 400


  
30 Herbert zenpS60mUBfHXln3tAbCds82IK1NKhAjAMoI9Czw

MTOM

为了克服这个问题, MTOM - 消息传输优化机制 - 已经被W3C发明并标准化了。与其将二进制内容作为Base64编码的字符提供--作为元素文本包含在SOAP消息中--不如使用多部分的mime格式,将二进制内容从SOAP消息中分离出来。这意味着:SOAP消息本身包含在一个部分中,而二进制内容则在另一个部分中。SOAP消息中的内容元素只是对二进制部分的一个引用。听起来很奇怪?看看就知道了(同样,为了便于阅读,XML是有格式的)。

POST /ws/documents HTTP/1.1
...
Content-Type: Multipart/Related; start-info="text/xml"; type="application/xop+xml"; boundary="----=_Part_0_2494886.1441553075493"
Content-Length: 842

------=_Part_0_2494886.1441553075493
Content-Type: application/xop+xml; charset=utf-8; type="text/xml"


  
30 Herbert
------=_Part_0_2494886.1441553075493 Content-Type: application/octet-stream Content-ID: Content-Transfer-Encoding: binary !�öâ6[ê�ĨŷνªÖÓ$+yò'½ni ------=_Part_0_2494886.1441553075493--

正如你所看到的,包含二进制数据的部分只包含30个字节,没有更多。但是对于多部分元数据,你肯定要付出一些开销。作为一个经验法则,MTOM只对内容> 1 kB有意义。如果你看一下内容元素,你会注意到xop元素。


  

虽然MTOM描述了SOAP中优化传输的抽象特征,但使用MIME多部分的具体实现则保留在一个单独的规范中。 XOP ,XML二进制优化包装。通过这种方式,它可以独立于SOAP用于XML文档中的任何二进制内容。因此,你经常会看到MTOM/XOP这样的字眼。

应用实例

为了给你一个例子,我在github 上准备了一个用spring boot实现的 s SOAP服务器和客户端。它们是用 STS 构建的,但你也可以用普通的Java来构建和运行它们;只需查看自述文件。克隆仓库并切换到分支 base64 ,它提供了两个spring项目 mtom-servermtom-client 的初始设置。按照自述文件中的描述,启动服务器和客户端即可。客户端会问你要上传的文件大小。如果你输入一个尺寸,客户端将生成一个该尺寸的文件(包含一些随机的二进制数据),并将其上传到服务器。客户端和服务器都会跟踪请求和响应,所以你可以检查它们。

客户端控制台输出。

enter size of document to upload, or just press enter to exit: 30

Storing document of size 30
2015-09-07 17:27:22.645 TRACE 13988 --- [main] o.s.ws.client.MessageTracing.received: Received response [
true
] for request [
30BertUZrGmb6QfI78BHRezp2VvzCvtyzRkTYwXhP0FmM/
] success: true

服务器控制台输出。

received 30 bytes
[2015-09-07 17:27:22.592] boot - 13712 TRACE [http-nio-9090-exec-1] --- sent: Sent response [
true
] for request [
30BertUZrGmb6QfI78BHRezp2VvzCvtyzRkTYwXhP0FmM/
]

SOAP服务的基础通常是一个定义了类型和操作的WSDL。在我们的案例中,我们只是在服务器项目中提供了一个描述类型的模式 documents.xsd ,并使用JAXB来创建Java类。Spring为我们即时创建了WSDL。只要启动服务器并浏览 http://localhost:9090/ws/documents.wsdl 。WSDL也包含在客户端的wsdl/documents.wsdl下的资源中。下面是WSDL的一个摘录:


  
    
      
    
  



  
    
      
    
  



  
    
    
    
  



  
    
    
  

所以我们只是在操作 storeDocument 中加入一个 document ,在 storeDocumentRequest 中加入一个布尔值。 document 本身包含一些元数据,如 nameauthor ,以及 - 最后 - 二进制 content

使用MTOM

为了使用MTOM,我们必须对我们的例子做一些修改。(你可以在你的例子上执行这些步骤,或者直接查看分支 mtom 中准备好的解决方案)。首先,让我们在客户端启用MTOM,这只是在JAXB marshaller上启用它。

@Bean
public Jaxb2Marshaller marshaller() {
  Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
  ...
  marshaller.setMtomEnabled(true);
  return marshaller;
}

基本上,这和我们在服务器上要做的一样。但我们还必须告诉spring在端点上使用该marshaller。

@Bean
public Jaxb2Marshaller marshaller() {
  Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
  marshaller.setContextPath("rst.sample.mtom.jaxb");
  marshaller.setMtomEnabled(true);
  return marshaller;
}

@Bean
@Override
public DefaultMethodEndpointAdapter defaultMethodEndpointAdapter() {
  List argumentResolvers =
  new ArrayList();
  argumentResolvers.add(methodProcessor());

  List returnValueHandlers =
  new ArrayList();
  returnValueHandlers.add(methodProcessor());

  DefaultMethodEndpointAdapter adapter = new DefaultMethodEndpointAdapter();
  adapter.setMethodArgumentResolvers(argumentResolvers);
  adapter.setMethodReturnValueHandlers(returnValueHandlers);

  return adapter;
}

@Bean
public MarshallingPayloadMethodProcessor methodProcessor() {
  return new MarshallingPayloadMethodProcessor(marshaller());
}

我们还必须通过提供一个适当的多部分解析器来增加对多部分的支持。

@Configuration
public class MultipartResolverConfig {

  @Bean
  public CommonsMultipartResolver multipartResolver() {
    return new CommonsMultipartResolver();
  }

  @Bean
  public CommonsMultipartResolver filterMultipartResolver() {
    final CommonsMultipartResolver resolver = new CommonsMultipartResolver();
    return resolver;
  }
}

就这样,如果你运行它,你会在客户端控制台看到以下输出。

enter size of document to upload, or just press enter to exit: 30

Storing document of size 30
2015-09-07 17:36:37.740 TRACE 11644 --- [main] o.s.ws.client.MessageTracing.received : Received response [------=_Part_2_8618207.1441640197738
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"

true
------=_Part_2_8618207.1441640197738--] for request [------=_Part_1_12274722.1441640197737 Content-Type: application/xop+xml; charset=utf-8; type="text/xml"
30Bibo
------=_Part_1_12274722.1441640197737 Content-Type: application/octet-stream Content-ID: <3f9e1eef-fc65-4bda-bcee-c2764a3cbf3a@github.com> Content-Transfer-Encoding: binary &.}pR�4Ѿ��m��B�C�T�yK}>} ------=_Part_1_12274722.1441640197737--]


MTOM和流

如果你看一下从模型中生成的Java类,你会看到二进制内容被保存在一个字节数组中。

public class Document {

   @XmlElement(required = true)
   protected String name;

   @XmlElement(required = true)
   protected String author;

   @XmlElement(required = true)
   protected byte[] content;
...

当你处理大型二进制数据时,这是一个问题,因为完整的数据必须保存在内存中。 OutOfMemoryException 在等着你。这个问题的解决方案是流:你不把数据保存在内存中,而是在一个流中提供数据,或者从一个流中读取数据。这是很自然的,因为大多数数据存储,如文件系统、数据库等,都提供了流式接口。即使MTOM规范中没有提到流媒体,XOP规范也说--即使不是强制性的--大多数实现都提供了流数据的可能性。现在让我们在我们的例子中这样做。同样,你可以按照步骤将你的MTOM应用转换为流媒体,或者查看项目的 master 分支。主站为你提供最终版本。首先,我们需要一种方法,在我们的Java类中提供一个流媒体接口。做到这一点的方法是稍微改变一下模式。只要在内容元素中加入属性 xmime:expectedContentTypes="application/octet-stream" 就可以了。


  
    
    
    
  

现在JAXB为字段内容生成一个 DataHandler 而不是 byte[]

public class Document {
  ...
  @XmlElement(required = true)
  @XmlMimeType("application/octet-stream")
  protected DataHandler content;

DataHandler是基于流的,所以你可以使用Input- 和OutputStreams来传递数据。这就是我们所做的;我们不是先将我们的内容读入一个字节数组,而是直接将我们的输入流传递给客户端的DataHandler。

public class DocumentsClient extends WebServiceGatewaySupport {

  public boolean storeDocument(int size) {
    Document document = new Document();
    document.setContent(getContentAsDataHandler(size));
    ...
  }

  private DataHandler getContentAsDataHandler(final int size) {
    InputStream input = getContentAsStream(size);
    DataSource source = new InputStreamDataSource(input, ...
    return new DataHandler(source);
  }

在服务器端我们也得到一个DataHandler,我们用它来直接从InputStream中读取数据。就这样了?试试吧......嗯,还是没有内存了。客户端似乎还是要先把内容读到内存中。这个问题的答案是HTTP协议,让我们回顾一下我们的MTOM请求。

POST /ws/documents HTTP/1.1
...
Content-Type: Multipart/Related; start-info="text/xml"; type="application/xop+xml"; boundary="----=_Part_0_2494886.1441553075493"
Content-Length: 842

HTTP希望提前得到内容长度,所以为了计算内容长度,内容被完全读入内存。为了避免这种情况,我们必须使用 分块传输编码

  public DocumentsClient() {
    setMessageSender(new ChunkedEncodingMessageSender());
  }
  ...

public class ChunkedEncodingMessageSender extends HttpUrlConnectionMessageSender {
  protected void prepareConnection(final HttpURLConnection connection) throws IOException {
    super.prepareConnection(connection);
    connection.setChunkedStreamingMode(-1);
  }
}

现在我们已经明白了吗?让我们重试一下... 神圣的吉娃娃,现在服务器了 。但现在应该马上就能工作了吧!?那是SAAJ实现中的一个错误,见 SAAJ-31 。正如你在那里看到的,我们必须设置一个开关来强制SAAJ使用mimepull,所以我们这样做了。

@SpringBootApplication
public class Application {

  public static void main(String[] args) {
    // needed for streaming, see https://java.net/jira/browse/SAAJ-31
    System.setProperty("saaj.use.mimepull", "true");

    SpringApplication.run(Application.class, args);
  }
}

现在可以了...真的。用1.000.000.000.000字节试试吧(SOAP跟踪在主站是不平衡的,所以不用担心)。这将需要一些时间,因为我们的随机数据InputStream生成了每一个字节。

enter size of document to upload, or just press enter to exit: 1000000000

Storing document of size 1000000000
success: true


WCF中的MTOM和流

在开始的时候,我告诉你我的项目,我必须与一个建立在.NET和WCF上的第三方服务器产品通信。这个产品起初没有使用MTOM,所以我也不得不对这个软件进行修改。幸运的是,在WCF中,这只是你必须做的一些配置 ......而我有机会接触到那个配置文件;-) 在HTTP绑定中,你必须将属性 messageEncoding 设置为Mtom。要使用流媒体,只需将属性 transferMode 设置为流媒体。


  
    

这很容易,是吗?是的,WCF为你处理所有这些有趣的小细节。

结论

MTOM允许你有效地传输大型二进制数据,甚至允许流式传输,以避免内存问题。是的,还有其他的流媒体机制可用;但如果你想让你的SOAP服务与其他服务互通,MTOM标准是你的选择。

疑惑?看完这集《肥皂》你就不会了!
《肥皂》的播音员 《肥皂》,70年代末的一部情景喜剧 我喜欢看 :-)