如何让 Wildfly 10 / Resteasy 向客户端指示序列化异常

Posted

技术标签:

【中文标题】如何让 Wildfly 10 / Resteasy 向客户端指示序列化异常【英文标题】:How do I make Wildfly 10 / Resteasy indicate a serialization exception to the client 【发布时间】:2016-07-10 13:08:02 【问题描述】:

当在 Wildfly 10 中对 jax-rs 响应进行 json 序列化期间发生异常时,响应将被提交,并且无法再更改 HTTP 返回代码。在服务器端记录了一个例外:

10:19:56,148 ERROR [io.undertow.request] (default task-21) UT005023: Exception handling request to /ca00cefe-efd8-496c-a874-de0af20cad42/rest/: org.jboss.resteasy.spi.UnhandledException: RESTEASY003770: Response is committed, can't handle exception
    at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:167)
    at org.jboss.resteasy.core.SynchronousDispatcher.writeResponse(SynchronousDispatcher.java:471)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:415)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:202)
    at org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:221)
    at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:56)
    at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:51)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
    at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:85)
    at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
    at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
    at org.wildfly.extension.undertow.security.SecurityContextAssociationHandler.handleRequest(SecurityContextAssociationHandler.java:78)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:131)
    at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
    at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
    at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)
    at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)
    at io.undertow.security.handlers.NotificationReceiverHandler.handleRequest(NotificationReceiverHandler.java:50)
    at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:284)
    at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:263)
    at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:81)
    at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:174)
    at io.undertow.server.Connectors.executeRootHandler(Connectors.java:202)
    at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:793)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

json 序列化器也很好地关闭了所有打开的标签,所以对于客户端来说,响应是带有有效 json 的 http 代码 200。没有任何故障迹象,只是缺少数据。

当异常发生时,我如何说服 Wildfly / Resteasy / Undertow 停止向客户端提供完全有效的响应。 500 响应或不关闭 json 都可以。

在我的测试过程中,我发现当抛出异常时,实际上并没有向客户端发送任何数据,仍然会在响应中添加一个 Content-Length 标头。这可能会留下更改响应代码的机会,但是我无法找到发生这种情况的位置或为什么在尚未发送任何内容时将响应标记为已提交。

HTTP/1.1 200 OK
Connection: keep-alive
X-Powered-By: Undertow/1
Server: WildFly/10
Content-Type: application/json
Content-Length: 10
Date: Wed, 23 Mar 2016 09:37:19 GMT

"a":"ok"

测试用例

TestActivator.java

package test;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/rest")
public class TestActivator extends Application 

TestResource.java

package test;

import java.io.Serializable;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.xml.bind.annotation.XmlRootElement;

@Path("/")
public class TestResource 
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public TestObject test() 
        return new TestObject();
    

    @XmlRootElement
    public class TestObject implements Serializable 

        private static final long serialVersionUID = 1L;

        public TestObject() 
        

        public String getA() 
            return "ok";
        

        public String getB() 
            throw new RuntimeException("error");
        
    

TestCase.java

package test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.extension.rest.client.ArquillianResteasyResource;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(Arquillian.class)
public class TestCase 

    @Deployment(testable = false)
    public static WebArchive createTestArchive() 
        return ShrinkWrap.create(WebArchive.class)
                .addPackage(TestResource.class.getPackage());
    

    /**
     * This test should fail, either the response should not be 200 or a json exception should occur
     */
    @Test
    public void test(@ArquillianResteasyResource WebTarget webTarget) 
        Response response = webTarget.path("/").request().get();
        assertEquals(200, response.getStatus());
        JSONObject object = new JSONObject(new JSONTokener(response.readEntity(String.class)));
        assertEquals("ok", object.get("a"));
        assertTrue(object.isNull("b"));
    

【问题讨论】:

【参考方案1】:

您是否考虑过返回Response 对象?这将具有状态和您的结果。如果状态良好,客户端将在检索结果之前检查状态:

/**
 * Creates a new contact from the values provided and will return a JAX-RS response with either 200 ok, or 400 (BAD REQUEST)
 * in case of errors.
 */
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createContact(Contact contact) 

    Response.ResponseBuilder builder = null;
    Long nextId = contactsRepository.keySet().size() + 1L;
    try 
        // Store the contact
        contact.setId(nextId);
        contactsRepository.put(nextId, contact);

        // Create an "ok" response with the persisted contact
        builder = Response.ok(contact);
     catch (Exception e) 
        // Handle generic exceptions
        builder = Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage());
    

    return builder.build();

客户会这样使用它:

    // 1 - drop all contacts
    log.info("dropping all contacts");
    Response response = ClientBuilder.newClient().target(REST_TARGET_URL).request().delete();
    Assert.assertEquals("All contacts should be dropped", Response.ok().build().getStatus(), response.getStatus());

    // 2 - Create a new contact
    log.info("creating a new contact");
    Contact c = new Contact();
    c.setName(CONTACT_NAME);
    c.setPhoneNumber(CONTACT_PHONE);
    Contact persistedContact = ClientBuilder.newClient().target(REST_TARGET_URL).request().post(Entity.entity(c, MediaType.APPLICATION_JSON), Contact.class);
    Assert.assertEquals("A book should be persisted with Id=1!", (Long) 1L, (Long) persistedContact.getId());

这是来自 WildFly 快速入门原型:

jaxrs-client: JAX-RS Client API example

【讨论】:

不幸的是,这给出了相同的结果,用“return Response.ok(new TestObject()).build();”测试在 TestResource.test()【参考方案2】:

我们找到了实现这一目标的方法。

在大多数情况下,禁用 AUTO_CLOSE_TARGET 会在失败时导致 http 500 错误。我发现的唯一例外是当响应超过一定大小时,在这种情况下,某些东西会开始刷新输出。对于这种情况,禁用 AUTO_CLOSE_JSON_CONTENT 将确保 json 文档在出错时未正确关闭。

package test;

import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;

@Provider
public class MyObjectContextResolver implements ContextResolver<ObjectMapper> 
    private ObjectMapper objectMapper;

    public MyObjectContextResolver() 
        objectMapper = new ObjectMapper();

        objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
        objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_JSON_CONTENT, false);
    

    @Override
    public ObjectMapper getContext(final Class<?> arg0) 
        return objectMapper;
    

【讨论】:

以上是关于如何让 Wildfly 10 / Resteasy 向客户端指示序列化异常的主要内容,如果未能解决你的问题,请参考以下文章

使用 RESTEasy、Weld 和 Wildfly 失败的 @Inject 对象

Wildfly14 的 Resteasy:并非所有字段都返回

如何找出 Wildfly 1x.x.x 中使用的 RESTEasy 版本

我如何找出Wildfly 1x.x.x中使用的RESTEasy版本

Hadoop Jersey 与 Wildfly resteasy 发生冲突

CORS:AngularJS + Resteasy 3 + Wildfly