【问题标题】:Why CXF / JAXB read whole InputStream into memory before marshalling to SOAP message为什么 CXF / JAXB 在编组到 SOAP 消息之前将整个 InputStream 读入内存
【发布时间】:2015-11-02 20:07:06
【问题描述】:

INFO - 示例代码

我已经为您设置了示例代码 (SSCCE) 以帮助您跟踪问题:

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

问题

我正在与第 3 方 JAX-WS 服务集成,因此我无法更改 WSDL。

第 3 方网络服务期望 Base64 编码字节对它们执行一些操作 - 他们期望客户端在 SOAP 消息中发送整个字节。 他们不想改用 MTOM / XOP,所以我坚持目前的要求。

我决定使用 CXF 轻松设置示例客户端,它适用于小文件。

但是当我尝试发送 BIG 数据(即 200MB)时,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

应该处理来自

的数据编组

javax.activation.DataHandler

在编组期间,来自底层 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 在将 Java 对象编组为 SOAP 消息期间使用 JAXB - 编组 InputStream 时,WHOLE 输入流在被转换为 Base64 二进制文件之前被读取到内存中。

所以我想在 chunks 中从客户端向服务器发送(“流”)数据(因为 marshaller 中的 OutputSteam 被直接包装为 HttpURLConnection),所以我的客户端可以处理发送任意数量的数据.

特别是当许多线程将使用我的客户端时,恕我直言,流式传输是非常可取的。

我没有很好的 JAX-WS/CXF/JAXB 知识,因此提出了这个问题。

我发现并且可能有用的唯一材料是:

Can JAXB parse large XML files in chunks

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

问题

  1. 为什么 CXF/JAXB 将整个 InputStream 加载到内存中 - DataHandler 的目的不是阻止此类实现?

  2. 您知道有什么方法可以将 JAXB 行为更改为以不同方式编组 InputStream 吗?

  3. 你知道不同的编组器,它们可以处理这样的大数据编组吗?

  4. 作为最后的手段,也许您有一些材料的链接,如何创建将数据直接流式传输到服务器的自定义编组器?

【问题讨论】:

    标签: jaxb cxf jax-ws marshalling jaxb2


    【解决方案1】:

    您不需要任何自定义编组器或更改 JAXB 行为即可实现所需 - DataHandler 是您的朋友。

    回答您的第一个问题:JAXB 需要将所有数据保存在内存中,因为它必须解析引用。

    我知道您不能更改 WSDL 引用等。但是您的项目中仍然有您客户的 WSDL 以便生成客户端类,不是吗?所以你可以做的(我还没有测试过这个与第三方的WSDL,但可能值得一试)是将xmime:expectedContentTypes="application/octet-stream" 添加到返回Base64 编码数据的响应XSD 元素中。例如:

    <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>
    

    另外不要忘记在xsd:schema 元素中添加命名空间:xmlns:xmime="http://www.w3.org/2005/05/xmlmime"

    您在这里所做的 - 没有更改任何 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
        }
    }
    

    感谢xmime,您可以得到DataHandler 作为响应类型。我建议你使用PipedOutputStream,但请确保在不同的线程中编写:

    管道输出流可以连接到管道输入流以 创建通信管道。管道输出流是发送 管道的末端。通常,数据被写入 PipedOutputStream 一个线程的对象和从连接的数据读取 PipedInputStream 来自其他线程。尝试同时使用这两个对象 不建议使用单个线程,因为它可能会使线程死锁。 如果线程正在读取数据字节,则称管道已损坏 来自连接的管道输入流不再存在。

    然后将它与PipedInputStream 连接,该实例进入InputStreamDataSource 的构造函数,然后您将其传递到DataHandler 并返回DataHandler 的实例。这样,您的文件将被分块写入,并且您不会收到该异常,而且 - 客户端永远不会超时。

    希望这会有所帮助。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多