Почему 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.Base64Datap >
должен обрабатывать сортировку данных из
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
. Таким образом, ваш файл будет записан в виде кусков, и вы не получите этого исключения, больше - клиент никогда не получит таймаут.
Надеюсь, что это поможет.