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

2022-09-01 01:32:20

我正在使用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 问题管理系统的功能改进发布在此处。我只能希望这能得到关注。


答案 1

它并不是开箱即用,但CXF确实支持JSON绑定到休息服务。请参阅 cxf jax-rs json docs here.您仍然需要执行一些最小配置才能使提供程序可用,并且如果要更好地控制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 提供程序进行代码配置的位置。特别是,您会看到命名空间映射。这需要与服务器端配置上的内容相匹配。我对抛弃选项知之甚少,因此我对操纵所有用于控制编组过程的各种旋钮的帮助不大。
  2. CXF 中的 Jettison 的工作原理是将 XML 从 JAXB 提供程序编组到 JSON 中。因此,您必须确保有效负载对象全部标记(或以其他方式配置为)以编组为应用程序/xml,然后才能将它们编组为 JSON。如果你知道解决这个问题的方法(除了写你自己的消息正文作者),我很想听听。
  3. 我在服务器上使用弹簧,所以我的配置都是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 - 示例服务器端配置正如我之前所说,我的服务器端是弹簧配置的。下面是一个用于在提供程序中进行连线的示例配置:

<?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消息正文读取器/写入器的示例需要快速修改,其中isWriteable/isReadable更改为:

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

编辑 4 - 非弹簧配置使用您选择的 servlet 容器,配置

org.apache.cxf.jaxrs.servlet.CXFNonSpringJaxrsServlet

具有至少 2 个初始化参数:

jaxrs.serviceClasses
jaxrs.providers

其中,serviceClasses 是要绑定的服务实现的空格分隔列表,例如上面提到的 TestApi,而提供程序是消息正文提供程序的空格分隔列表,例如上面提到的 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>

这几乎是在没有弹簧的情况下运行它的最快方法。如果不使用 servlet 容器,则需要使用 XstreamJsonProvider 实例配置 JAXRSServerFactoryBean.setProviders,并通过 JAXRSServerFactoryBean.setResourceProvider 方法设置服务实现。检查 CXFNonSpringJaxrsServlet.init 方法,了解它们在 servlet 容器中设置时是如何做到的。

无论你的情况如何,这都应该让你继续前进。


答案 2

我从 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>

推荐