CXF:找不到类的消息体编写器 - 自动映射非简单资源

43

我正在使用CXF REST客户端,对于简单的数据类型(例如:字符串、整数)工作得很好。但是,当我尝试使用自定义对象时,我遇到了这个错误:

Exception in thread "main" org.apache.cxf.interceptor.Fault: .No message body writer found for class : class com.company.datatype.normal.MyObject.
    at org.apache.cxf.jaxrs.client.ClientProxyImpl$BodyWriter.handleMessage(ClientProxyImpl.java:523)
    at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:263)
    at org.apache.cxf.jaxrs.client.ClientProxyImpl.doChainedInvocation(ClientProxyImpl.java:438)
    at org.apache.cxf.jaxrs.client.ClientProxyImpl.invoke(ClientProxyImpl.java:177)
    at $Proxy13.execute(Unknown Source)
    at com.company.JaxTestClient.main(JaxTestClient.java:26)
Caused by: org.apache.cxf.jaxrs.client.ClientWebApplicationException: .No message body writer found for class : class com.company.datatype.normal.MyObject.
    at org.apache.cxf.jaxrs.client.AbstractClient.reportMessageHandlerProblem(AbstractClient.java:491)
    at org.apache.cxf.jaxrs.client.AbstractClient.writeBody(AbstractClient.java:401)
    at org.apache.cxf.jaxrs.client.ClientProxyImpl$BodyWriter.handleMessage(ClientProxyImpl.java:515)
    ... 5 more
我这样调用它:
JaxExample jaxExample = JAXRSClientFactory.create( "http://localhost:8111/", JaxExample.class );
MyObject before = ...
MyObject after = jaxExample.execute( before );

以下是接口中的方法:

@POST
@Path( "execute" )
@Produces( "application/json" )
MyObject execute( MyObject myObject );

Restlet库通过将XStream依赖项添加到您的路径中,非常简单地实现了这一点。CXF是否有类似的功能?

编辑#1:

我已将此发布为CXF问题管理系统的功能改进在此处。我只希望这将得到关注。

11个回答

47
CXF支持将JSON与rest服务绑定,但需要进行一些最小配置才能使用提供程序。如果您想更好地控制JSON的形式,需要熟悉jettison。请参见cxf jax-rs json文档了解详细信息。
编辑:根据评论请求,以下是一些代码示例。我对此没有太多经验,但在一个快速测试系统中,以下代码作为示例运行良好。
//TestApi parts
@GET
@Path ( "test" )
@Produces ( "application/json" )
public Demo getDemo () {
    Demo d = new Demo ();
    d.id = 1;
    d.name = "test";
    return d;
}

//client config for a TestApi interface
List providers = new ArrayList ();
JSONProvider jsonProvider = new JSONProvider ();
Map<String, String> map = new HashMap<String, String> ();
map.put ( "http://www.myserviceapi.com", "myapi" );
jsonProvider.setNamespaceMap ( map );
providers.add ( jsonProvider );
TestApi proxy = JAXRSClientFactory.create ( url, TestApi.class, 
    providers, true );

Demo d = proxy.getDemo ();
if ( d != null ) {
    System.out.println ( d.id + ":" + d.name );
}

//the Demo class
@XmlRootElement ( name = "demo", namespace = "http://www.myserviceapi.com" )
@XmlType ( name = "demo", namespace = "http://www.myserviceapi.com", 
    propOrder = { "name", "id" } )
@XmlAccessorType ( XmlAccessType.FIELD )
public class Demo {

    public String name;
    public int id;
}

备注:

  1. 提供者列表是您在客户端上配置JSON提供程序的地方。特别是,您可以看到名称空间映射。这需要与您服务器端配置中的内容匹配。我不太了解Jettison选项,因此在操纵控制编组过程的各种旋钮方面没有太多帮助。
  2. CXF中的Jettison通过将来自JAXB提供程序的XML编组为JSON来工作。因此,您必须确保有效载荷对象都已标记(或以其他方式配置)为应用程序/ xml才能将它们编组为JSON。如果您知道绕过此问题的方法(而不是编写自己的消息体编写器),我很乐意听听。
  3. 我在服务器上使用spring,因此我的配置都是xml内容。基本上,您需要通过相同的命名空间配置过程将JSONProvider添加到服务中。这方面的代码不方便,但我想它会与客户端方面相当相似。

这个例子有点不完美,但希望能帮助您入门。

编辑2:一个基于xstream的消息体编写器示例,以避免使用jaxb。

@Produces ( "application/json" )
@Consumes ( "application/json" )
@Provider
public class XstreamJsonProvider implements MessageBodyReader<Object>,
    MessageBodyWriter<Object> {

@Override
public boolean isWriteable ( Class<?> type, Type genericType, 
    Annotation[] annotations, MediaType mediaType ) {
    return MediaType.APPLICATION_JSON_TYPE.equals ( mediaType ) 
        && type.equals ( Demo.class );
}

@Override
public long getSize ( Object t, Class<?> type, Type genericType, 
    Annotation[] annotations, MediaType mediaType ) {
    // I'm being lazy - should compute the actual size
    return -1;
}

@Override
public void writeTo ( Object t, Class<?> type, Type genericType, 
    Annotation[] annotations, MediaType mediaType, 
    MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream ) 
    throws IOException, WebApplicationException {
    // deal with thread safe use of xstream, etc.
    XStream xstream = new XStream ( new JettisonMappedXmlDriver () );
    xstream.setMode ( XStream.NO_REFERENCES );
    // add safer encoding, error handling, etc.
    xstream.toXML ( t, entityStream );
}

@Override
public boolean isReadable ( Class<?> type, Type genericType, 
    Annotation[] annotations, MediaType mediaType ) {
    return MediaType.APPLICATION_JSON_TYPE.equals ( mediaType ) 
        && type.equals ( Demo.class );
}

@Override
public Object readFrom ( Class<Object> type, Type genericType, 
    Annotation[] annotations, MediaType mediaType, 
    MultivaluedMap<String, String> httpHeaders, InputStream entityStream ) 
    throws IOException, WebApplicationException {
    // add error handling, etc.
    XStream xstream = new XStream ( new JettisonMappedXmlDriver () );
    return xstream.fromXML ( entityStream );
}
}

//now your client just needs this
List providers = new ArrayList ();
XstreamJsonProvider jsonProvider = new XstreamJsonProvider ();
providers.add ( jsonProvider );
TestApi proxy = JAXRSClientFactory.create ( url, TestApi.class, 
    providers, true );

Demo d = proxy.getDemo ();
if ( d != null ) {
    System.out.println ( d.id + ":" + d.name );
}

样例代码缺少对媒体类型支持、错误处理、线程安全等方面的健壮性支持。但是,它应该可以用最少的代码解决jaxb问题。
编辑3 - 样例服务器端配置 正如我之前所说,我的服务器端是经过spring配置的。这是一个可行的样例配置,可以将提供程序连接起来:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jaxrs="http://cxf.apache.org/jaxrs"
xmlns:cxf="http://cxf.apache.org/core"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd
    http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd">

<import resource="classpath:META-INF/cxf/cxf.xml" />

<jaxrs:server id="TestApi">
    <jaxrs:serviceBeans>
        <ref bean="testApi" />
    </jaxrs:serviceBeans>
    <jaxrs:providers>
        <bean id="xstreamJsonProvider" class="webtests.rest.XstreamJsonProvider" />
    </jaxrs:providers>
</jaxrs:server>

<bean id="testApi" class="webtests.rest.TestApi">
</bean>

</beans>

我注意到我正在使用的最新版本cxf的媒体类型有所不同,因此上述关于xstream消息正文读取器/写入器的示例需要进行快速修改,其中isWritable/isReadable需要更改为:

return MediaType.APPLICATION_JSON_TYPE.getType ().equals ( mediaType.getType () )
    && MediaType.APPLICATION_JSON_TYPE.getSubtype ().equals ( mediaType.getSubtype () )
    && type.equals ( Demo.class );

编辑4 - 非Spring配置 使用您选择的servlet容器进行配置

org.apache.cxf.jaxrs.servlet.CXFNonSpringJaxrsServlet

至少有两个初始参数:

jaxrs.serviceClasses
jaxrs.providers

其中,serviceClasses是您想要绑定的服务实现的以空格分隔的列表,例如上面提到的TestApi;providers是消息体提供程序的以空格分隔的列表,例如上面提到的XstreamJsonProvider。在Tomcat中,您可以将以下内容添加到web.xml文件中:

<servlet>
    <servlet-name>cxfservlet</servlet-name>
    <servlet-class>org.apache.cxf.jaxrs.servlet.CXFNonSpringJaxrsServlet</servlet-class>
    <init-param>
        <param-name>jaxrs.serviceClasses</param-name>
        <param-value>webtests.rest.TestApi</param-value>
    </init-param>
    <init-param>
        <param-name>jaxrs.providers</param-name>
        <param-value>webtests.rest.XstreamJsonProvider</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

那基本上是在没有使用spring的情况下最快的运行它的方法。如果您没有使用servlet容器,则需要使用XstreamJsonProvider实例配置JAXRSServerFactoryBean.setProviders,并通过JAXRSServerFactoryBean.setResourceProvider方法设置服务实现。在servlet容器中设置时,请检查CXFNonSpringJaxrsServlet.init方法。这应该可以让您无论什么情况都能开始运行。

1
我看过这个,但是示例使用了Spring - 而我想避免使用它。你有能让这个工作起来的Java代码吗?+1 - javamonkey79
首先,我感谢您为帮助我解决这个问题所做的努力。其次,我期望像这样的东西在库中“开箱即用” - 但也许我期望过高了。在我看来,对于每个资源都必须实现MessageBody [Reader \ Writer]似乎很繁琐和混乱。除非有人提出更好的解决方案,否则我将接受您的解决方案,并在时间表结束时授予您奖励,但是我仍然认为这不是最佳解决方案。谢谢! - javamonkey79
如果我能帮忙就很高兴。个人而言,我不会对json提供程序抱有太大期望;它的通用性太弱了。在许多情况下,客户端和服务器需要相互了解才能正常工作,这可能使jaxb标记或其他方法更加可行。最后一个要点是,您不需要为每个资源都拥有消息体读取器/写入器。您可以轻松地更改isWritable/Readable以不关心类类型,从而使所有实体都使用xstream/jettison默认编组。代码少于100行,非常便宜。 - philwb
@philwb ... 谢谢,这也对我有帮助,因为我正在使用Spring中的CXF工作于RESTful服务。 - Garry
有没有一种方法可以通过编程方式设置MessageBodyReader/Writer?我没有使用webap,因此我没有xml或启动。 - markthegrea
显示剩余8条评论

9
我在将CXF 2.7.0升级到3.0.2时遇到了这个问题。以下是我解决它的方法:
在我的pom.xml中加入以下内容:
    <dependency>
        <groupId>org.apache.cxf</groupId>
        <artifactId>cxf-rt-rs-extension-providers</artifactId>
        <version>3.0.2</version>
    </dependency>

    <dependency>
        <groupId>org.codehaus.jackson</groupId>
        <artifactId>jackson-jaxrs</artifactId>
        <version>1.9.0</version>
    </dependency>

并添加了以下提供者

    <jaxrs:providers>
        <bean class="org.codehaus.jackson.jaxrs.JacksonJaxbJsonProvider" />
    </jaxrs:providers>

1
你能分享一下在CXF 2.7中注册提供者类的具体步骤吗? - Abhishek Ranjan
jaxrs 的 xmlns 是什么? - MonoThreaded

4

如果您正在使用jaxrs:client路由进行配置,您可以选择使用JacksonJsonProvider来提供

<jaxrs:client id="serviceId"
    serviceClass="classname"
    address="">
    <jaxrs:providers>
        <bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider">
            <property name="mapper" ref="jacksonMapper" />
        </bean>
    </jaxrs:providers>
</jaxrs:client>

<bean id="jacksonMapper" class="org.codehaus.jackson.map.ObjectMapper">
</bean>

您需要在类路径中包含 jackson-mapper-asl 和 jackson-jaxr 工件。

3

当通过编程方式创建服务器时,您可以通过设置提供程序来添加用于 JSON/XML 的消息体编写器。

JAXRSServerFactoryBean bean = new JAXRSServerFactoryBean();
bean.setAddress("http://localhost:9000/");

List<Object> providers = new ArrayList<Object>();
providers.add(new JacksonJaxbJsonProvider());
providers.add(new JacksonJaxbXMLProvider());
bean.setProviders(providers);

List<Class< ? >> resourceClasses = new ArrayList<Class< ? >>();
resourceClasses.add(YourRestServiceImpl.class);
bean.setResourceClasses(resourceClasses);

bean.setResourceProvider(YourRestServiceImpl.class, new SingletonResourceProvider(new YourRestServiceImpl()));

BindingFactoryManager manager = bean.getBus().getExtension(BindingFactoryManager.class);
JAXRSBindingFactory restFactory = new JAXRSBindingFactory();
restFactory.setBus(bean.getBus());
manager.registerBindingFactory(JAXRSBindingFactory.JAXRS_BINDING_ID, restFactory);

bean.create();

1
步骤1:将bean类添加到dataFormat列表中:
<dataFormats>
    <json id="jack" library="Jackson" prettyPrint="true"
          unmarshalTypeName="{ur bean class path}" /> 
</dataFormats>

步骤2:在客户端调用之前对Bean进行编组:

<marchal id="marsh" ref="jack"/>

1

您还可以配置CXFNonSpringJAXRSServlet(假设使用JSONProvider):

<init-param>
  <param-name>jaxrs.providers</param-name>
  <param-value>
      org.apache.cxf.jaxrs.provider.JSONProvider
      (writeXsiType=false)
  </param-value> 
</init-param>

1

以上的更改都对我无效。 请查看下面我的配置:

依赖项:

            <dependency>
                <groupId>org.apache.cxf</groupId>
                <artifactId>cxf-rt-frontend-jaxrs</artifactId>
                <version>3.3.2</version>
            </dependency>
            <dependency>
                <groupId>org.apache.cxf</groupId>
                <artifactId>cxf-rt-rs-extension-providers</artifactId>
                <version>3.3.2</version>
            </dependency>
            <dependency>
                <groupId>org.codehaus.jackson</groupId>
                <artifactId>jackson-jaxrs</artifactId>
                <version>1.9.13</version>
            </dependency>
            <dependency>
                <groupId>org.codehaus.jettison</groupId>
                <artifactId>jettison</artifactId>
                <version>1.4.0</version>
            </dependency>

web.xml:

     <servlet>
        <servlet-name>cfxServlet</servlet-name>
        <servlet-class>org.apache.cxf.jaxrs.servlet.CXFNonSpringJaxrsServlet</servlet-class>
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>com.MyApplication</param-value>
        </init-param>
        <init-param>
            <param-name>jaxrs.providers</param-name>
            <param-value>org.codehaus.jackson.jaxrs.JacksonJsonProvider</param-value>
        </init-param>
        <init-param>
            <param-name>jaxrs.extensions</param-name>
            <param-value> 
            json=application/json 
        </param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>cfxServlet</servlet-name>
        <url-pattern>/v1/*</url-pattern>
    </servlet-mapping>

享受编程.. :)


0
在我的场景中,当没有端口号的rest url没有正确地配置负载均衡时,我遇到了类似的错误。我使用带端口号的rest url进行验证,这个问题就不再出现了。因此,我们必须更新负载均衡配置来解决这个问题。

0

如果你想在响应中以JSON格式返回对象,你可以尝试在REST客户端头部添加 "Accept: application/json"。


这并没有真正帮助@Koushik Das。 - N00b Pr0grammer

0
如果您正在使用“cxf-rt-rs-client”版本3.03或更高版本,请确保以下声明了xml名称空间和schemaLocation。
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:jaxrs="http://cxf.apache.org/jaxrs" 
    xmlns:jaxrs-client="http://cxf.apache.org/jaxrs-client"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd http://cxf.apache.org/jaxrs-client http://cxf.apache.org/schemas/jaxrs-client.xsd">

并确保客户端拥有JacksonJsonProvider或您自定义的JsonProvider

<jaxrs-client:client id="serviceClient" address="${cxf.endpoint.service.address}" serviceClass="serviceClass">
        <jaxrs-client:headers>
            <entry key="Accept" value="application/json"></entry>
        </jaxrs-client:headers>
        <jaxrs-client:providers>
            <bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider">
            <property name="mapper" ref="jacksonMapper" />
        </bean>
        </jaxrs-client:providers>
</jaxrs-client:client>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接