Статьи

Используйте MTOM для эффективной передачи двоичного содержимого в SOAP

Сервисы REST на основе JSON популярны, но когда дело доходит до интеграции корпоративных сервисов, SOAP все еще широко используется. В недавнем проекте мне пришлось написать микросервис на основе весенней загрузки, который был своего рода шлюзом для стороннего веб-сервиса на основе SOAP, построенного на Microsoft WCF . При вызове микросервис собирал некоторые данные из других микросервисов, загружал документ PDF из системы хранения и передавал данные и PDF в службу SOAP за один вызов SOAP. Когда первая реализация была закончена и развернута, я проверил журналы доступа: вау, размер запроса был огромным для некоторых запросов. Хорошо, размер PDF-файлов варьировался от нескольких килобайт до нескольких мегабайт, но запрос, казалось, был намного больше. Это потому, что SOAP использует Base64кодировать двоичный контент. И Base64 кодирует 6 битов на символ, значит 3 байта кодируются в 4 символа. Это на 33% больше контента, чем необработанных данных. Для небольшого контента это не проблема, но если вам нужно передать 4 МБ вместо 3 МБ … это проблема. Вот пример простого 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: 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"

<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>
          <xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include"
            href="cid:ecc6eb7a-ce73-4ab7-8266-3bc2869cb0ae%40github.com" />
        </ns2:content>
      </ns2:document>
    </ns2:storeDocumentRequest>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>
------=_Part_0_2494886.1441553075493
Content-Type: application/octet-stream
Content-ID: <ecc6eb7a-ce73-4ab7-8266-3bc2869cb0ae@github.com>
Content-Transfer-Encoding: binary

 ! öâ6[ê  Ä ¨Å  ·Î½ªÖÓ$+yò'½ni
------=_Part_0_2494886.1441553075493--

Как видите, часть, содержащая двоичные данные, содержит только 30 байтов, не более. Но наверняка есть некоторые издержки для составных метаданных, которые вы должны заплатить. Как правило, MTOM имеет смысл только для контента> 1 кБ. Если вы посмотрите на элемент содержимого, вы заметите элемент xop:

<ns2:content>
  <xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include"
    href="cid:ecc6eb7a-ce73-4ab7-8266-3bc2869cb0ae%40github.com" />
</ns2:content>

В то время как MTOM описывает абстрактную функцию оптимизации передачи в SOAP, конкретная реализация, использующая мультипартии MIME, хранится в отдельной спецификации: XOP , XML-двоичная оптимизированная упаковка. Тем самым он может использоваться независимо от SOAP для любого двоичного содержимого в документах XML. Поэтому вы часто найдете формулировку MTOM / XOP.

Пример приложения

Чтобы дать вам пример, с которым можно поиграться, я подготовил простой SOAP-сервер и клиент на github, реализованный с использованием Spring Boot. Они построены с помощью STS , но вы можете создавать и запускать их с помощью простой Java; просто проверьте файл Readme. Клонируйте репозиторий и переключитесь на ветку base64, он обеспечивает начальную настройку с двумя весенними проектами mtom-serverи mtom-client. Просто запустите сервер и клиент, как описано в файле Readme. Клиент запрашивает размер документа для загрузки. Если вы введете размер, клиент сгенерирует документ такого размера (содержащий некоторые случайные двоичные данные) и загрузит его на сервер. И клиент, и сервер также отслеживают запрос и ответ, так что вы можете проверить их.

Вывод клиентской консоли:

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 [<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/><SOAP-ENV:Body><ns2:storeDocumentResponse xmlns:ns2="https://github.com/ralfstuckert/mtom"><ns2:success>true</ns2:success></ns2:storeDocumentResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>] for request [<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>Bert</ns2:author><ns2:content>UZrGmb6QfI78BHRezp2VvzCvtyzRkTYwXhP0FmM/</ns2:content></ns2:document></ns2:storeDocumentRequest></SOAP-ENV:Body></SOAP-ENV:Envelope>]
success: true

Вывод консоли сервера:

received 30 bytes
[2015-09-07 17:27:22.592] boot - 13712 TRACE [http-nio-9090-exec-1] --- sent: Sent response [<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/><SOAP-ENV:Body><ns2:storeDocumentResponse xmlns:ns2="https://github.com/ralfstuckert/mtom"><ns2:success>true</ns2:success></ns2:storeDocumentResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>] for request [<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>Bert</ns2:author><ns2:content>UZrGmb6QfI78BHRezp2VvzCvtyzRkTYwXhP0FmM/</ns2:content></ns2:document></ns2:storeDocumentRequest></SOAP-ENV:Body></SOAP-ENV:Envelope>]

Основой службы SOAP обычно является WSDL, который определяет типы и операции. В нашем случае мы просто предоставляем схему documents.xsdв проекте сервера с описанием типов и используем JAXB для создания классов Java. Весна, чем создает WSDL, чем на лету для нас. Просто запустите сервер и перейдите по адресу http: // localhost: 9090 / ws / documents.wsdl . WSDL также содержится в клиенте в ресурсах в wsdl / documents.wsdl. Вот выдержка из WSDL:

<xs:element name="storeDocumentRequest">
  <xs:complexType>
    <xs:sequence>
      <xs:element name="document" type="tns:document" />
    </xs:sequence>
  </xs:complexType>
</xs:element>

<xs:element name="storeDocumentResponse">
  <xs:complexType>
    <xs:sequence>
      <xs:element name="success" type="xs:boolean" />
    </xs:sequence>
  </xs:complexType>
</xs:element>

<xs:complexType name="document">
  <xs:sequence>
    <xs:element name="name" type="xs:string" />
    <xs:element name="author" type="xs:string" />
    <xs:element name="content" type="xs:base64Binary" />
  </xs:sequence>
</xs:complexType>

<wsdl:portType name="DocumentsPort">
  <wsdl:operation name="storeDocument">
    <wsdl:input message="tns:storeDocumentRequest" name="storeDocumentRequest"></wsdl:input>
    <wsdl:output message="tns:storeDocumentResponse" name="storeDocumentResponse"></wsdl:output>
  </wsdl:operation>
</wsdl:portType>

Таким образом, мы имеем только операции storeDocumentс a documentв storeDocumentRequestи логическое в storeDocumentResponse. documentСама содержит некоторые метаданные , как nameи author, и — наконец — двоичный код content.

Использование MTOM

Чтобы использовать MTOM, мы должны применить некоторые изменения в нашем примере. (Вы можете выполнить эти шаги на своем примере или напрямую проверить готовое решение в филиале mtom). Сначала давайте включим MTOM на клиенте, который просто включает его на маршаллере JAXB:

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

По сути, это то же самое, что и на сервере. Но мы также должны сказать Spring, чтобы использовать этого маршаллера для конечных точек:

@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<MethodArgumentResolver> argumentResolvers =
  new ArrayList<MethodArgumentResolver>();
  argumentResolvers.add(methodProcessor());

  List<MethodReturnValueHandler> returnValueHandlers =
  new ArrayList<MethodReturnValueHandler>();
  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"

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/><SOAP-ENV:Body><ns2:storeDocumentResponse xmlns:ns2="https://github.com/ralfstuckert/mtom"><ns2:success>true</ns2:success></ns2:storeDocumentResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>
------=_Part_2_8618207.1441640197738--] for request [------=_Part_1_12274722.1441640197737
Content-Type: application/xop+xml; charset=utf-8; type="text/xml"

<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>Bibo</ns2:author><ns2:content><xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="cid:3f9e1eef-fc65-4bda-bcee-c2764a3cbf3a%40github.com"/></ns2:content></ns2:document></ns2:storeDocumentRequest></SOAP-ENV:Body></SOAP-ENV:Envelope>
------=_Part_1_12274722.1441640197737
Content-Type: application/octet-stream
Content-ID: <3f9e1eef-fc65-4bda-bcee-c2764a3cbf3a@github.com>
Content-Transfer-Encoding: binary

&.}p R 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"к элементу контента:

<xs:complexType name="document">
  <xs:sequence>
    <xs:element name="name" type="xs:string" />
    <xs:element name="author" type="xs:string" />
    <xs:element name="content" type="xs:base64Binary" xmime:expectedContentTypes="application/octet-stream" />
  </xs:sequence>
</xs:complexType>

Теперь 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 байтов (трассировка SOAP отключена в master, так что не беспокойтесь). Это займет некоторое время, так как наши случайные данные InputStream генерируют каждый байт:

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

Storing document of size 1000000000
success: true

MTOM и потоковая передача в WCF

Вначале я рассказывал вам о своем проекте, в котором мне приходилось общаться со сторонним серверным продуктом, построенным на .NET и WCF. Сначала этот продукт не использовал MTOM, поэтому мне пришлось внести изменения и в это программное обеспечение. К счастью, в WCF это всего лишь какая-то конфигурация, которую вы должны сделать … и у меня был доступ к этому файлу конфигурации 😉 В привязке HTTP вы должны установить атрибут messageEncodingMtom. Чтобы использовать потоковую передачу, просто установите атрибут transferModeStreamed:

<bindings>
  <basicHttpBinding>
    <binding name="...." messageEncoding="Mtom" transferMode="Streamed" >

Это было легко, а? Да, WCF обрабатывает все эти мелкие мелочи для вас.

Заключение

MTOM позволяет эффективно передавать большие двоичные данные, и даже позволяет потоковую передачу во избежание проблем с памятью. Да, есть и другие механизмы потокового вещания; но если вы хотите, чтобы ваш SOAP-сервис взаимодействовал с другими, то стандарт MTOM — ваш выбор.

Смущенный? Вы не будете после этого эпизода мыла!
Диктор Soap, комедия конца 70-х, которую я любил смотреть 🙂