【问题标题】:RESTEasy loses object during serializationRESTEasy 在序列化期间丢失对象
【发布时间】:2014-01-13 15:05:57
【问题描述】:

试图将对象发送到端点,RESTEasy 正在丢弃一些对象,将它们替换为 Ns 空值,然后导致类转换异常。

这是一个简化的测试用例,显示了最新版本的 RESTEasy 和早期版本(例如 2.3.7)的问题:

请求类(注意是根元素):

package org.problem;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Request implements Serializable {
    private static final long serialVersionUID = 4057201224391819116L;

    public Request() {
        super();
    }

    // Yes, I could use an adapter to serialize a map, but that's not germane to the problem.
    public List<String> names = new ArrayList<String>();
    public List<Object> values = new ArrayList<Object>();

    public Map<String, Object> getArgs() {
        int size = names.size();
        Map<String, Object> args = new HashMap<String, Object>(size);
        for (int i = 0; i < size; i++) {
            args.put(names.get(i), values.get(i));
        }
        return args;
    }
}

附加信息类(也是根元素):

package org.problem;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class AdditionalInfo implements Serializable {
    private static final long serialVersionUID = 4286757723357099359L;

    public AdditionalInfo() {
        super();
    }

    public String value;
}

端点:

package org.problem;

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/problem/")
public class Endpoint {
    @POST
    @Path("example/v1")
    @Consumes(MediaType.APPLICATION_XML)
    @Produces(MediaType.APPLICATION_XML)
    public String doProblem(Request request) {
        Object obj = null;
        try {
            obj = request.getArgs().get("info");
            AdditionalInfo info = (AdditionalInfo)obj;
            return info.value;
        } catch (ClassCastException e) {
            throw new RuntimeException("Expected " + AdditionalInfo.class.getCanonicalName() + " but instead got " + obj.getClass().getCanonicalName());
        }
    }
}

重现问题:

package org.problem;

import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.jboss.resteasy.plugins.server.resourcefactory.POJOResourceFactory;

import javax.ws.rs.core.MediaType;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;

public class TestProblem {

    public static void main(String[] args) throws Exception {
        AdditionalInfo info = new AdditionalInfo();
        info.value = "Got The Info";
        Request request = new Request();
        request.names.add("info");
        request.values.add(info);
        // To see that AdditionalInfo has to be specifically added to JAXB context, try running without it:
        // JAXBContext requestContext = JAXBContext.newInstance(Request.class);
        JAXBContext requestContext = JAXBContext.newInstance(Request.class, AdditionalInfo.class);
        StringWriter writer = new StringWriter();
        Marshaller marshaller = requestContext.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(request, writer);
        String marshalledRequestString = writer.toString();
        System.out.println(marshalledRequestString);
        InputStream xmlStream = new ByteArrayInputStream(marshalledRequestString.getBytes("UTF-8"));
        MockHttpRequest mockRequest = MockHttpRequest.post("/problem/example/v1");
        mockRequest.contentType(MediaType.APPLICATION_XML);
        mockRequest.content(xmlStream);
        MockHttpResponse mockResponse = new MockHttpResponse();
        Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
        dispatcher.getRegistry().addResourceFactory(new POJOResourceFactory(Endpoint.class));
        dispatcher.invoke(mockRequest, mockResponse);
        System.out.println(mockResponse.getStatus());
        String response = mockResponse.getContentAsString();
        System.out.println(response);
    }
}

当我运行该代码时,我得到以下输出:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<request>
    <names>info</names>
    <values xsi:type="additionalInfo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <value>Got The Info</value>
    </values>
</request>

log4j:WARN No appenders could be found for logger (org.jboss.resteasy.plugins.providers.DocumentProvider).
log4j:WARN Please initialize the log4j system properly.
Exception in thread "main" org.jboss.resteasy.spi.UnhandledException: java.lang.RuntimeException: Expected org.problem.AdditionalInfo but instead got com.sun.org.apache.xerces.internal.dom.ElementNSImpl
    at org.jboss.resteasy.core.ExceptionHandler.handleApplicationException(ExceptionHandler.java:76)
    at org.jboss.resteasy.core.ExceptionHandler.handleException(ExceptionHandler.java:212)
    at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:149)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:372)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:179)
    at org.problem.TestProblem.main(TestProblem.java:41)
Caused by: java.lang.RuntimeException: Expected org.problem.AdditionalInfo but instead got com.sun.org.apache.xerces.internal.dom.ElementNSImpl
    at org.problem.Endpoint.doProblem(Endpoint.java:22)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:137)
    at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:280)
    at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:234)
    at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:221)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:356)
    ... 2 more

如果我从用于创建请求正文的 JAXBContext 初始化中删除 AdditionalInfo,那么它会更早地爆炸:

Exception in thread "main" javax.xml.bind.MarshalException
 - with linked exception:
[com.sun.istack.SAXException2: class org.problem.AdditionalInfo nor any of its super class is known to this context.
javax.xml.bind.JAXBException: class org.problem.AdditionalInfo nor any of its super class is known to this context.]
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:326)
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.marshal(MarshallerImpl.java:251)
    at javax.xml.bind.helpers.AbstractMarshallerImpl.marshal(AbstractMarshallerImpl.java:116)
    at org.problem.TestProblem.main(TestProblem.java:31)
Caused by: com.sun.istack.SAXException2: class org.problem.AdditionalInfo nor any of its super class is known to this context.
javax.xml.bind.JAXBException: class org.problem.AdditionalInfo nor any of its super class is known to this context.
    at com.sun.xml.bind.v2.runtime.XMLSerializer.reportError(XMLSerializer.java:247)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.reportError(XMLSerializer.java:262)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsXsiType(XMLSerializer.java:653)
    at com.sun.xml.bind.v2.runtime.property.ArrayElementNodeProperty.serializeItem(ArrayElementNodeProperty.java:69)
    at com.sun.xml.bind.v2.runtime.property.ArrayElementProperty.serializeListBody(ArrayElementProperty.java:172)
    at com.sun.xml.bind.v2.runtime.property.ArrayERProperty.serializeBody(ArrayERProperty.java:159)
    at com.sun.xml.bind.v2.runtime.ClassBeanInfoImpl.serializeBody(ClassBeanInfoImpl.java:361)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsSoleContent(XMLSerializer.java:593)
    at com.sun.xml.bind.v2.runtime.ClassBeanInfoImpl.serializeRoot(ClassBeanInfoImpl.java:342)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsRoot(XMLSerializer.java:494)
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:323)
    ... 3 more
Caused by: javax.xml.bind.JAXBException: class org.problem.AdditionalInfo nor any of its super class is known to this context.
    at com.sun.xml.bind.v2.runtime.JAXBContextImpl.getBeanInfo(JAXBContextImpl.java:593)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsXsiType(XMLSerializer.java:648)
    ... 11 more

看起来问题可能是 RESTEasy 没有正确初始化 JAXBContext。

文档说“基于您正在编组/解组的类,RESTEasy 将默认为每个类类型创建和缓存 JAXBContext 实例”。

这似乎是一个错误的说法。

假设它不正确,解决方法是什么?我需要编写自己的 ContextResolver 吗?有没有办法告诉 RESTEasy 它需要能够序列化哪些根元素?

即使我告诉它,名称/值映射的重点是能够序列化任何 arg(前提是它是根元素),因此必须通知 RESTEasy 每个新的 arg 类型是一个维护难题,并且破坏了名称/值对的要点。

【问题讨论】:

    标签: jaxb resteasy


    【解决方案1】:

    不,您不需要创建自定义 ContextResolver。 JAXB 根​​据预期的端点数据类型自动确定解析器的匹配上下文。您的问题是您的 REST 端点需要 org.problem.Request 的实例,但在您的主要方法中,您使用 org.problem.AdditionalInfo 类型的对象作为有效负载。您可以尝试使用以下命令注释 org.problem.Request 类:

    @XmlSeeAlso({AdditionalInfo.class})
    

    它告诉 JAXB 框架将类型 org.problem.AdditionInfo 也视为预期类型 org.problem.Request 的可能上下文类型

    但是为什么你使用Request 类型和AdditionalInfo。他们有什么共同点吗?您可以将端点简化如下:

    @Path("/problem/")
    public class Endpoint {
        @POST
        @Path("example/v1")
        @Consumes(MediaType.APPLICATION_XML)
        @Produces(MediaType.APPLICATION_XML)
        public String doProblem(AdditionalInfo request) {
        ...
    }
    

    这会更直接一些。还要考虑 JAXB 注释的用法。您可以使用以下注释标记您的 XML 标记并将其绑定到特定命名空间:

    @XmlRootElement(name = "RootElement", namespace = "SomeNameSpace")
    

    进一步尝试使用如下注释:

    @XmlElement(namespace = "SomeNameSpace")
    @XmlAttriubte
    

    为您的数据实体的成员。


    JUnit 测试用例,与其余端点通信。

    @Test
    public void a100_insertCustomerOrderTest() throws Exception {
        Customer cust = new Customer(1, "CUST", "OMER", "sb@gmail.com", "sb",
                lastLoginTime);
        List<Product> products = new Vector<Product>();
        products.add(new Book(5, "asdf", "description", 0, currency, isbn, new Date(),
                "asdf"));
        CustomerOrder custOrder = new CustomerOrder(cust, bookingOrder,
                products);
    
        ClientRequest request = new ClientRequest(BASE_URL, sslExecutor_schusb);
        request.body(MediaType.APPLICATION_XML, custOrder);
        ClientResponse<String> response = request.post(String.class);
        Assert.assertEquals(201, response.getStatus());
    
        String expectedLink = BasicServiceTest.BASE_URL+"booking/6";
        insertedResourceLink = response.getHeaders().getFirst("Location");
        assertEquals(expectedLink, insertedResourceLink);
    
        response.releaseConnection();
        request.clear();
    }
    

    下面是数据实体ProductBook

    @XmlRootElement
    @XmlSeeAlso({Book.class})
    public class Product implements Serializable {
    
        /**
         * Constructor needed by JAXB, in order to marshal a {@link Product} into
         * {@link Book}
         * 
         * @param product
         *            {@link Product}
         */
        protected Product(Product product) {
            setId(new Integer(product.getId()));
            setName(new String(product.getName()));
            setDescription(new String(product.getDescription()));
            setPrice(new Integer(product.getPrice()));
            setCurrency(new String(product.getCurrency()));
        }
    }
    
    
    @XmlRootElement
    public class Book extends Product implements Serializable {
    
        /**
         * Constructor needed by JAXB in order to marshal a {@link Product} into a
         * {@link Book} instance
         * 
         * @param product
         *            {@link Product}
         */
        public Book(Product product) {
            super(product);
            Book book = (Book) product;
    
            setIsbn(new String(book.isbn));
            setPublicationDate(new Date(book.getPublicationDate().getTime()));
            setAuthor(new String(book.getAuthor()));
        }
    }
    

    【讨论】:

    • 我没有使用 AdditionalInfo 作为有效负载。我正在发送一个请求,这也是端点所期望的类型。 AdditionalInfo 位于请求的名称/值“映射”中。关于您关于 Request 和 AdditionalInfo 是否相关的问题,不,它们不相关。这才是重点。由于过于乏味而无法进入此处,该请求包含在调用端点之前收集的任意数量的名称/值对。
    • 我明白你关于在请求中添加 see-also 的观点,这很有效,但目标是不必为可以进入 args 的每种类型都这样做。
    • @user1461450 您正在使用JAXBContext.newInstance(Request.class, AdditionalInfo.class); 并期望上下文可以从Request 切换到AdditionalInfo。但这不是它的完成方式。 JAXBFramework 正在执行以下操作:JAXBContext jaxbContext = jaxbResolver.getContext(type);,其中类型为Request.classAdditionalInfo.class。解析 JaxbContext 后,您将无法在它们之间切换。将一个数据实体封装到另一个数据实体中并不是预期的通信方式。
    • @user1461450 例如,您也没有声明类似void doSth(Request request){...} 的方法并尝试使用List&lt;AdditionalInfo&gt; request 类型的参数调用它。即使AdditionalInfo 扩展Request,这也不起作用!解决此问题的方法是创建数据实体类并将它们用作其余端点方法中的参数类型,而不是 public String doProblem(Request request) {...}
    • @user1461450 不,如果你按照我上一篇文章的建议,你根本不需要指定任何类型!我将从我的一个项目中添加一个测试用例,这表明您无需添加任何类型信息。
    【解决方案2】:

    开始讨论并进行聊天,并将问题细化为足够通用,以突出问题。重申一下,问题是,给定这个端点:

    @POST 
    @Path("example/v1") 
    @Consumes(MediaType.APPLICATION_XML) 
    @Produces(MediaType.APPLICATION_XML) 
    public String foo(List<Object> bars) throws Exception {...
    

    我们不提前知道列表中对象的类型,或者有多少,只知道它们都是正确的 JAXB 注释的根元素,并且知道我们不能假设任何对象之间的继承或组合关系,我们如何通过 RESTEasy 调用该端点方法。

    由用户 My-Name-Is 提供的答案是编写一个自定义的消息正文编写器和读取器。

    我设想 RESTEasy 的工作方式(真的,我设想 JAXB 的工作方式):获取每个对象的类。如果已经看到,则获取缓存的 JAXBContext。如果还没有看到,请从对象的类中获取注释,查看它是否是 JAXB 注释的,如果是,则为其创建一个 JAXBContext 并将其添加到缓存中,否则抛出异常。

    但 JAXB 似乎没有这样做。

    顺便说一句,我必须处理这些奇怪的不相关对象的原因是该请求正在传递有关记录在客户端上的一组函数调用的信息,然后在服务器上“回放”。所以请求只是一个调用列表的外壳,每个调用可以有 N 个参数,请求不一定知道参数的类型。

    一个类比是,请求是一辆公共汽车,将具有各种技能的人从一个地方,他们被交给工具来完成他们的工作,到另一个地方,他们应该在那里完成他们的工作。公共汽车对技能或工具一无所知。它只知道如何将任意人从一个地方运送到另一个地方。

    【讨论】:

      猜你喜欢
      • 2015-09-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-12-07
      • 2023-03-25
      • 1970-01-01
      • 2011-09-18
      • 1970-01-01
      相关资源
      最近更新 更多