Почему CXF/JAXB считывает весь InputStream в память перед сортировкой по SOAP-сообщению

INFO - Пример кода

Я установил образец кода (SSCCE), чтобы вы могли отслеживать проблему:

https://github.com/ljader/test-cxf-base64-marshall

Проблема

Я интегрируюсь со сторонним JAX-WS-сервисом, поэтому я не могу изменить WSDL.

Сторонний вебсервис ожидает, что закодированные байты Base64 будут выполнять некоторую операцию над ними - они ожидают, что клиент отправит все байты в сообщении SOAP. Они не хотят, чтобы менялся на MTOM/XOP, поэтому я придерживался текущих требований.

Я решил использовать CXF для простой настройки образца клиента, и он работал нормально для небольших файлов.

Но когда я пытаюсь отправить BIG-данные, т.е. 200 МБ, CXF/JAXB выдает исключение:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.sun.xml.bind.v2.util.ByteArrayOutputStreamEx.readFrom(ByteArrayOutputStreamEx.java:75)
at com.sun.xml.bind.v2.runtime.unmarshaller.Base64Data.get(Base64Data.java:196)
at com.sun.xml.bind.v2.runtime.unmarshaller.Base64Data.writeTo(Base64Data.java:312)
at com.sun.xml.bind.v2.runtime.output.UTF8XmlOutput.text(UTF8XmlOutput.java:312)
at com.sun.xml.bind.v2.runtime.XMLSerializer.leafElement(XMLSerializer.java:356)
at com.sun.xml.bind.v2.model.impl.RuntimeBuiltinLeafInfoImpl$PcdataImpl.writeLeafElement(RuntimeBuiltinLeafInfoImpl.java:191)
at com.sun.xml.bind.v2.runtime.MimeTypedTransducer.writeLeafElement(MimeTypedTransducer.java:96)
at com.sun.xml.bind.v2.runtime.reflect.TransducedAccessor$CompositeTransducedAccessorImpl.writeLeafElement(TransducedAccessor.java:254)
at com.sun.xml.bind.v2.runtime.property.SingleElementLeafProperty.serializeBody(SingleElementLeafProperty.java:130)
at com.sun.xml.bind.v2.runtime.ClassBeanInfoImpl.serializeBody(ClassBeanInfoImpl.java:360)
at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsXsiType(XMLSerializer.java:696)
at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl$1.serializeBody(ElementBeanInfoImpl.java:155)
at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl$1.serializeBody(ElementBeanInfoImpl.java:130)
at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl.serializeBody(ElementBeanInfoImpl.java:332)
at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl.serializeRoot(ElementBeanInfoImpl.java:339)
at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl.serializeRoot(ElementBeanInfoImpl.java:75)
at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsRoot(XMLSerializer.java:494)
at com.sun.xml.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:323)
at com.sun.xml.bind.v2.runtime.MarshallerImpl.marshal(MarshallerImpl.java:251)
at javax.xml.bind.helpers.AbstractMarshallerImpl.marshal(AbstractMarshallerImpl.java:95)
at org.apache.cxf.jaxb.JAXBEncoderDecoder.writeObject(JAXBEncoderDecoder.java:617)
at org.apache.cxf.jaxb.JAXBEncoderDecoder.marshall(JAXBEncoderDecoder.java:241)
at org.apache.cxf.jaxb.io.DataWriterImpl.write(DataWriterImpl.java:237)
at org.apache.cxf.interceptor.AbstractOutDatabindingInterceptor.writeParts(AbstractOutDatabindingInterceptor.java:117)
at org.apache.cxf.wsdl.interceptors.BareOutInterceptor.handleMessage(BareOutInterceptor.java:68)
at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308)
at org.apache.cxf.endpoint.ClientImpl.doInvoke(ClientImpl.java:514)
at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:423)
at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:324)
at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:277)
at org.apache.cxf.frontend.ClientProxy.invokeSync(ClientProxy.java:96)
at org.apache.cxf.jaxws.JaxWsClientProxy.invoke(JaxWsClientProxy.java:139)

Мои результаты

Я отслеживал ошибку, которая на основе xsd типа "base64Binary",

com.sun.xml.bind.v2.model.impl.RuntimeBuiltinLeafInfoImpl

решает, что

com.sun.xml.bind.v2.runtime.unmarshaller.Base64Data​​p >

должен обрабатывать сортировку данных из

javax.activation.DataHandler

Во время сортировки данные WHOLE из базового InputStream пытаются читать http://grepcode.com/file/repo1.maven.org/maven2/com.sun.xml.bind/jaxb-impl/2.2.11/com/sun/xml/bind/v2/runtime/unmarshaller/Base64Data.java/#311, что вызывает исключение OOME.

Проблема

CXF использует JAXB во время сортировки объектов Java в сообщения SOAP - при сортировке InputStream входной поток WHOLE считывается в память перед преобразованием в двоичный файл Base64.

Итак, я хочу отправлять данные с клиента на сервер в фрагменты (так как OutputSteam в marshaller обернута напрямую HttpURLConnection), поэтому мой клиент может обрабатывать отправку любого количества данных.

Особенно, когда многие потоки будут использовать мой клиент, потоковая передача IMHO очень желательна.

У меня нет хорошего знания JAX-WS/CXF/JAXB, поэтому вопрос.

Единственными материалами, которые я нашел и которые могут быть полезны, являются:

Может ли JAXB анализировать большие XML файлы в кусках

http://rezarahim.blogspot.com/2010/05/chunking-out-big-xml-with-stax-and-jaxb.html

Вопросы

  • Почему CXF/JAXB загружает весь InputStream в память - не является ли чистая память DataHandler для предотвращения такой реализации?

  • Знаете ли вы какой-либо способ изменить поведение JAXB по-разному, маршаллируйте InputStream?

  • Знаете ли вы разных маршаллеров, которые могут обрабатывать такие большие сортировки данных?

  • Как последнее средство, возможно, у вас есть ссылки на некоторые материалы, как создать собственный маршаллер, который будет передавать данные непосредственно на сервер?

Ответ 1

Вам не нужны никакие пользовательские маршаллеры или изменение поведения JAXB для достижения того, что вам нужно - DataHandler - ваш друг здесь.

Отвечая на ваш первый вопрос: JAXB должен хранить все данные в памяти, потому что он должен разрешать ссылки.

Я знаю, что вы не можете изменить ссылки WSDL и т.д. Но у вас есть ваш клиент WSDL в вашем проекте, чтобы генерировать классы клиентов, не так ли? Итак, что вы можете сделать (я не тестировал этот с сторонним WSDL, но, возможно, стоит попробовать) заключается в добавлении xmime:expectedContentTypes="application/octet-stream" в элемент XSD ответа, который возвращает данные, закодированные Base64. Например, например:

<xsd:element name="generateBigDataResponse">
    <xsd:complexType>
        <xsd:sequence>
            <xsd:element name="result"
                         type="xsd:base64Binary"
                         minOccurs="0"
                         maxOccurs="1"
                         xmime:expectedContentTypes="application/octet-stream"/>
        </xsd:sequence>
    </xsd:complexType>
</xsd:element>

Также не забудьте добавить пространство имен: xmlns:xmime="http://www.w3.org/2005/05/xmlmime" в элементе xsd:schema.

Что вы здесь делаете - это не изменение ссылок на WSDL, а просто указание JAXB вместо генерации byte[] для генерации DataHandler. Итак, что происходит, когда вы создаете свои клиентские классы следующим образом:

@Override
public DataHandler generateBigData() {
    try {
        final PipedOutputStream pipedOutputStream = new PipedOutputStream();
        PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream);
        InputStreamDataSource dataSource = new InputStreamDataSource(pipedInputStream, "application/octet-stream");

        executor.execute(new Runnable() {

            @Override
            public void run() {
                //write your stuff here into pipedOutputStream
            }
        });

        return new DataHandler(dataSource);
    } catch (IOException e) {
        //handle exception if any
    }
}

Вы получаете DataHandler как тип ответа благодаря xmime. Я предлагаю вам использовать PipedOutputStream, но обязательно сделайте запись в другом потоке:

Канальный выходной поток может быть подключен к входному потоку с каналами создать канал связи. Потоковый поток - это отправка конец трубы. Как правило, данные записываются в PipedOutputStream объект одним потоком, и данные считываются из подключенного PipedInputStream через какой-то другой поток. Попытка использования обоих объектов из одного потока не рекомендуется, так как он может затормозить нить. Труба называется сломанной, если поток, который считывал байты данных из подключенного входного потока с каналами больше не будет.

Затем вы соединяете его с PipedInputStream, этот экземпляр входит в конструктор InputStreamDataSource, который затем передается в DataHandler и возвращает экземпляр DataHandler. Таким образом, ваш файл будет записан в виде кусков, и вы не получите этого исключения, больше - клиент никогда не получит таймаут.

Надеюсь, что это поможет.