RESTEasy在序列化期间丢失对象

时间:2013-12-25 20:56:23

标签: jaxb resteasy

尝试将对象发送到端点,RESTEasy正在删除一些对象,用Ns null替换它们,然后导致类转换异常。

这是一个简化的测试用例,它显示了最新版本的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类型是一个维护问题,并且击败名称/价值对的点。

2 个答案:

答案 0 :(得分:1)

不,您不需要创建自定义ContextResolver。 JAXB根据预期的端点数据类型自动确定解析程序的匹配上下文。这里的问题是你的REST-端点需要org.problem.Request的实例,但在你的main方法中,你使用类型为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注释的usagae。您可以使用以下注释标记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()));
    }
}

答案 1 :(得分:0)

进行讨论以进行聊天,并将问题细化为足够通用,以突出显示问题。重申,问题是,鉴于此终点:

@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是如何工作的):获取每个Object的类。如果已经看过,那么获取缓存的JAXBContext。如果还没有看到,请从Object的类中获取Annotations,看看它是否是JAXB注释,如果是,为它创建一个JAXBContext并将其添加到缓存中,否则抛出异常。

但似乎JAXB没有这样做。

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

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