开发者

CXF Restful service complex object unmarshalling does not work

开发者 https://www.devze.com 2023-03-31 00:33 出处:网络
I am new at restful services, and had a relatively good start, until I decided to play with some complex objects. The problem I am hitting is about unmarshalling an object coming to the server (creati

I am new at restful services, and had a relatively good start, until I decided to play with some complex objects. The problem I am hitting is about unmarshalling an object coming to the server (creating object from XML on the server side).

Below is my sample (representative) implementation of the service.

Here is my "complex object" data type.

package data;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class ComplexType {
    private long id;
    private String name;
    private Boolean isRegistered;


    public ComplexType() {
        super();
    }
    public ComplexType(long id, String name, Boolean isRegistered) {
        super();
        this.id = id;
        this.name = name;
        this.isRegistered = isRegistered;
    }
    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Boolean getIsRegistered() {
        return isRegistered;
    }
    public void setIsRegistered(Boolean isRegistered) {
        this.isRegistered = isRegistered;
    }
}

This is my service API

package api;

import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;

import data.ComplexType;


public interface Service {
    @GET
    @Path("/nummembers")
    int getNumElements();

    @GET
    @Path("/member/{id}")
    ComplexType getMember(@PathParam("id") long id);

    @POST
    @Path("/member")
    boolean addMember(@FormParam("member") ComplexType member);
}

And this is the implementation of the service:

package impl;

import java.util.HashMap;
import java.util.Map;

import data.ComplexType;
import api.Service;

public class ServiceImpl implements Service {
    Map<Long, ComplexType> data;

    public ServiceImpl() {
        System.out.println("TestApp Starting");
        data = new HashMap<Long, ComplexType>();
    }

    @Override
    public int getNumElements() {
        return data.size();
    }

    @Override
    public ComplexType getMember(long id) {
        if (data.containsKey(id)) {
            return data.get(id);
        }
        ComplexType ct =
            new ComplexType(id, "NAME" + new Long(id).toString(), (id % 2 == 1));
        data.put(id, ct);
        return ct;
    }

    @Override
    public boolean addMember(ComplexType member) {
        int preSize = data.size();
        data.put(member.getId(), member);
        return preSize < data.size(); // True if added
    }
}

So, when I call getNumElements() there is no problem. When I call getMember(long id) I get a serialized "ComplexType" just fine. When I serialize a complex type and pass it as FormParam to addMember(ComplexType member), I always get Parameter Class data.ComplexType has no constructor with single String parameter, static valueOf(String) or fromString(String) methods

I tried to provide my custom provider, by writing the following class:

package impl;

import javax.ws.rs.ext.ContextResolver;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;

import data.ComplexType;

public class JaxbXmlContextResolver implements ContextResolver<Object> {
    private static final Class<?>[] classes = new Class[] {ComplexType.class};
    private static final JAXBContext context = initContext();

    public static JAXBContext initContext() {
        JAXBContext context = null;
        try {
            context = JAXBContext.newInstance(classes);
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
        return conte开发者_StackOverflow社区xt;
    }

    @Override
    public Object getContext(Class<?> arg0) {
        return context;
    }
}

And for the rest of the configurations, here is my web.xml:

<?xml version="1.0"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
  <display-name>TestApp</display-name>

  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:cxf.xml</param-value>
  </context-param>

  <context-param>
    <param-name>log4jConfigLocation</param-name>
    <param-value>classpath:log4j.properties</param-value>
  </context-param>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <servlet>
    <servlet-name>CXFServlet</servlet-name>
    <display-name>CXF Servlet</display-name>
    <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>CXFServlet</servlet-name>
    <url-pattern>/services/*</url-pattern>
  </servlet-mapping>
</web-app>

And the cxf.xml it refers to:

<?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:util="http://www.springframework.org/schema/util"
       xmlns:jaxrs="http://cxf.apache.org/jaxrs"
       xmlns:jaxws="http://cxf.apache.org/jaxws"
       xmlns:cxf="http://cxf.apache.org/core"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/util
                           http://www.springframework.org/schema/util/spring-util-2.0.xsd
                           http://cxf.apache.org/jaxrs
                           http://cxf.apache.org/schemas/jaxrs.xsd">

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

  <bean id="myservice" class="impl.ServiceImpl" />
  <bean id="jaxbXmlProvider" class="impl.JaxbXmlContextResolver" />

  <jaxrs:server id="connectionService" address="/" >
    <jaxrs:serviceBeans>
      <ref bean="myservice" />
    </jaxrs:serviceBeans>
    <jaxrs:extensionMappings>
      <entry key="xml" value="application/xml" />
    </jaxrs:extensionMappings>
    <jaxrs:providers>
      <ref bean="jaxbXmlProvider" />
    </jaxrs:providers>
  </jaxrs:server>
</beans>

And for completeness, here is the pom.xml I am using to build the app.

<project xmlns="http://maven.apache.org/POM/4.0.0"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <modelVersion>4.0.0</modelVersion>
  <groupId>TESTAPP</groupId>
  <artifactId>testApp</artifactId>
  <packaging>war</packaging>
  <version>1.0</version>
  <name>Test Application</name>
  <url>http://www.mycompany.com</url>

  <dependencies>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.16</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.0.5.RELEASE</version>
    </dependency>

    <dependency>
      <groupId>org.apache.cxf</groupId>
      <artifactId>cxf-rt-frontend-jaxrs</artifactId>
      <version>2.4.1</version>
      <exclusions>
        <exclusion>
          <groupId>wsdl4j</groupId>
          <artifactId>wsdl4j</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>com.thoughtworks.xstream</groupId>
      <artifactId>xstream</artifactId>
      <version>1.3.1</version>
    </dependency>
    <dependency>
      <groupId>net.sf.kxml</groupId>
      <artifactId>kxml2</artifactId>
      <version>2.2.2</version>
    </dependency>
    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.2.1</version>
    </dependency>

  </dependencies>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Any help (solutions, pointers, or any kind of direction) will be greatly appreciated.

Oh, I am using cxf 2.4.1 and Spring 3.0.5.RELEASE. This is exact copy of my deployed application.

Thanks.


As it was stated at http://cxf.547215.n5.nabble.com/MessageBodyReader-not-picked-up-td564496.html, it turned out I needed a ParameterHandler. Since I had a large set of objects in my application, I did not want to create separate ParameterHandler<> for each, so I made a small change:

Using the technique described at JAXB inheritance, unmarshal to subclass of marshaled class, I created a base type "BaseType" which all the API data objects (TypeA, TypeB, ...) were inherited.

public class XmlParamHandler implements ParameterHandler<BaseType> {
    private static final Logger log = LoggerFactory.getLogger(XmlParamHandler.class);
    private static final JAXBContext jaxbContext = initContext();


    private static JAXBContext initContext() throws JAXBException {
        Class<?>[] classes = new Class[] {
            com.mycompany.BaseType.class,
            com.mycompany.TypeA.class,
            com.mycompany.TypeB.class,
            com.mycompany.TypeC.class,
            com.mycompany.TypeD.class,
            com.mycompany.TypeE.class,
            com.mycompany.TypeF.class,
        };
        JAXBContext context = JAXBContext.newInstance(classes);
        return context;
    }

    public static <T> T valueOf(String str) throws JAXBException {
        if (str == null) {
            return null;
        }
        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
        StringReader sr = new StringReader(str);
        @SuppressWarnings("unchecked")
        T request = (T) unmarshaller.unmarshal(sr);
        return request;
    }

    @Override
    public BaseType fromString(String s) {
        BaseType ct = null;
        try {
            return valueOf(s);
        } catch (JAXBException e) {
            return null;
        }
    }
}


Implement a default constructor with no arguments and one with an argument for each of the three properties. That should make JAXB happy.


It's a little odd to be passing a complex "XML-marshallable" object as a @FormParam. @FormParam are intended mostly for simple or primitive types like strings, integers, etc.

One of the nice things of JAX-RS is that it will automatically marshall / unmarshall objects for you is you tell it which media type to consume. So you may want to define your endpoints as (adding @Consumes and no annotation on member):

@POST
@Path("/member")
@Consumes({ MediaType.APPLICATION_XML })
boolean addMember(ComplexType member);

And POST the XML representation of a member as the request body rather than a form parameter and set the Content-Type header of your request to application/xml. CXF will then automatically read and parse the request body to an instance of ComplexType and pass it to your method.

You can use curl for that. Create an XML file with the serialized object (member.xml) and run:

curl -k --request POST --header "Content-Type: application/xml" --data @member.xml https://localhost:8080/path/to/service
0

精彩评论

暂无评论...
验证码 换一张
取 消