天天看點

JTF的Unable to invoke request異常或Unable to find a MessageBodyReader of content-type application..異常詳解

基于Jersey開發的一個操作OpenStack的REST服務,利用Jersey的Test Framework編寫單元測試類如下:

public class RestAddressTest extends JerseyTest {

    Integer autoId = 1;

    @BeforeClass
    public void before() throws Exception {
        super.setUp();
    }

    @AfterClass
    public void after() throws Exception {
        super.tearDown();
    }

    @Override
    protected Application configure() {
        return new ResourceConfig(RestAddress.class);
    }

    @Test(priority = 0)
    public void testAdd() {
        Address ac = new Address();
        ac.setGateway("1.1.1.1");
        ac.setName("CLOUD_TEST_BJ");
        ac.setNicName("ipv4");

        Response res = target("address").request(MediaType.APPLICATION_JSON).post(Entity.entity(ac, MediaType.APPLICATION_JSON), Response.class);
        Assert.assertEquals(200, res.getStatus());
    }

    @Test(priority = 1)
    public void testGet() {
        Response res = target("address").request(MediaType.APPLICATION_JSON).get();
        List<Address> acl = res.readEntity(new GenericType<List<Address>>() {});
        this.autoId = acl.get(0).getAutoId();
        Assert.assertEquals(1, acl.size());
    }

    @Test(priority = 2)
    public void testDelete() {
        Response response = target("address").path(String.valueOf(autoId)).request(MediaType.APPLICATION_JSON).delete();
        Assert.assertEquals(200, response.getStatus());
    }
}           

但是在測試時,卻始終遇到如下異常:

...
java.lang.IllegalArgumentException: attempt to create delete event with null entity
	at org.hibernate.event.spi.DeleteEvent.<init>(DeleteEvent.java:31)
	at org.hibernate.internal.SessionImpl.delete(SessionImpl.java:911)
	...
[03:39:54,116 ERROR] rollbackTxn - Transaction.rollback
[03:39:54,143 INFO ] Stopped [email protected]{HTTP/1.1}{0.0.0.0:9998}
[03:39:54,146 WARN ] /address/1
java.lang.RuntimeException: java.lang.IllegalArgumentException: Status code of the supplied response [500] is not from the required status code family "CLIENT_ERROR".
	at org.glassfish.jersey.jetty.JettyHttpContainer.handle(JettyHttpContainer.java:197)
	...
[03:39:54,147 WARN ] Could not send response error 500: java.lang.RuntimeException: java.lang.IllegalArgumentException: Status code of the supplied response [500] is not from the required status code family "CLIENT_ERROR".
Oct 24, 2018 5:39:54 AM org.glassfish.jersey.test.jetty.JettyTestContainerFactory$JettyTestContainer <init>
INFO: Creating JettyTestContainer configured at the base URI http://localhost:9998/
[03:39:54,275 INFO ] jetty-9.2.14.v20151106
[03:39:54,279 INFO ] Started [email protected]{HTTP/1.1}{0.0.0.0:9998}
[03:39:54,280 INFO ] Started @5991ms
[03:39:54,312 INFO ] Stopped [email protected]{HTTP/1.1}{0.0.0.0:9998}
...
Tests run: 4, Failures: 3, Errors: 0, Skipped: 0, Time elapsed: 4.734 sec <<< FAILURE!

Results :

Failed tests:   testAdd(com.mycompany.myapp.rest.RestAddressTest): RESTEASY004655: Unable to invoke request
  testGet(com.mycompany.myapp.rest.RestAddressTest): RESTEASY003145: Unable to find a MessageBodyReader of content-type application/json and type interface java.util.List
  testDelete(com.mycompany.myapp.rest.RestAddressTest): expected [500] but found [200]
...           

分析異常,可以發現在執行測試用例testDelete()時異常,這裡的資訊有很強的迷惑性。事實上,在測試用例testDelete()之前執行的testAdd()和testGet()都已經發生了異常,但是卻沒有輸出錯誤資訊。

繼續分析發現這應該是個“CLIENT_ERROR”。

在最後的測試結果中,終于暴露了錯誤的根源。原來,發生錯誤的是RESTEASY,等等,我們利用Jersey的測試架構進行單元測試,哪裡來的RestEasy呢?

檢查項目依賴,發現果然同時存在Jersey-Client和RestEasy-Client。突然想到,存在OpenStack的OpenStack4j就是依賴了RestEasy。檢視OpenStack4j 3.1.0果然是其引入了RestEasy。

那麼RestEasy為什麼會在這裡被調用呢?感謝StackOverflow,我很快找到了答案。

原來,同樣作為JAX-RS的實作,Jersey和RestEasy都擴充了javax.ws.rs.client.ClientBuilder類,并實作了javax.ws.rs.client.Client接口。

其中,Jersey提供的是org.glassfish.jersey.client.JerseyClientBuilder和org.glassfish.jersey.client.JerseyClient,

而RestEasy提供的是org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder和org.jboss.resteasy.client.jaxrs.ResteasyClient。

在同時存在兩個JAX-RS實作的時候,由于JAX-RS采用了Java SPI的服務實作注入機制。RestEasy響應了這種機制,聲明了ResteasyClientBuilder實作如下:

JTF的Unable to invoke request異常或Unable to find a MessageBodyReader of content-type application..異常詳解

而Jersey對此機制卻無動于衷,實作中根本沒有給出JerseyClientBuilder的實作,如下所示:

JTF的Unable to invoke request異常或Unable to find a MessageBodyReader of content-type application..異常詳解

是以,ResteasyClientBuilder的優先級高于JerseyClientBuilder而被采用。這樣,在測試過程中,用戶端事實上使用的是RestEasy的ResteasyClient,而非Jersey測試架構期望的JerseyClient。

找到問題的根源,解決起來也很容易了,隻要在測試類中重寫JerseyTest的getClient()方法,明确指定JerseyClient即可,修改測試類如下:

@Override
public Client getClient() {
    return JerseyClientBuilder.createClient();
}           

參考連結:

https://jersey.github.io/documentation/latest/test-framework.html

https://github.com/jersey/jersey/tree/master/test-framework

https://stackoverflow.com/questions/36348675/unable-to-test-jax-rs-with-json-entity

https://stackoverflow.com/questions/48337023/eclipse-jerseytest-getclient-returns-resteasyclient

https://github.com/ContainX/openstack4j/blob/3.1.0/connectors/resteasy/pom.xml

https://github.com/resteasy/Resteasy/blob/master/resteasy-client/src/main/resources/META-INF/services/javax.ws.rs.client.ClientBuilder

https://github.com/jersey/jersey/tree/master/core-client/src/main