Saturday, September 19, 2009

RESTful Web Services with Spring 3 Experience

Arjen Poutsma introduced REST support in Spring 3 through his two blogs REST in Spring 3: @MVC and REST in Spring 3: RestTemplate. Follow his instructions and Spring 3 PetClinic sample application, I got RESTful Web Service working in my Open Toast Project.

Just to provide a very simple service "get member object by member id", I created a new Web bundle project "org.opentoast.rest" by copying "org.opentoast.web" project. There is only one Java class in new project, MemebrController:

@Controller
public class MemberController
{
    protected final Log log = LogFactory.getLog(getClass());
    
    private MemberManager memberManager;
    
    @Autowired
    public MemberController(MemberManager memberManager){
        this.memberManager = memberManager;
    }
    
    @RequestMapping(value="/members/{memberId}", method = RequestMethod.GET)
    public ModelAndView getMember(@PathVariable("memberId") Long id) {
        Member m = memberManager.getMemberById(id);
        ModelAndView mav = new ModelAndView("member");
        mav.addObject("member", m);
        return mav;
    }
}

It provides one RESTful service in the method getMember(). It is annotated by @RequestMapping with URI template /members/{memberId}. Using @PathVariable("memberId"), the request member id is passed to parameter id. So with the injected MemberManager, we can get the Member object by id and pass to returned ModelAndView object. The view name is "member", and result member object is put into model with key "member" as well.

To setup REST support, there are few changes in config file module-context.xml:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop"
  xmlns:context="http://www.springframework.org/schema/context" xmlns:jee="http://www.springframework.org/schema/jee"
  xmlns:tx="http://www.springframework.org/schema/tx"
  xmlns:oxm="http://www.springframework.org/schema/oxm"
  xsi:schemaLocation="
   http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
   http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
   http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
   http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.5.xsd
   http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
   http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-3.0.xsd">

 <context:component-scan base-package="org.opentoast.rest.controller"/>

 <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>
   
 <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>
 
 <bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
 
 <bean id="member" class="org.springframework.web.servlet.view.xml.MarshallingView">
  <property name="contentType" value="application/vnd.opentoast.rest+xml"/>
  <property name="marshaller" ref="marshaller"/>
  <property name="modelKey" value="member"/>
 </bean>

 <oxm:jaxb2-marshaller id="marshaller">
  <oxm:class-to-be-bound name="org.opentoast.domain.Member"/>
 </oxm:jaxb2-marshaller>

</beans>

Since we only want to support REST service with customized content type, we don’t need ContentNegotiatingViewResolver , so only one view resolver is set in the context: org.springframework.web.servlet.view.BeanNameViewResolver. It will resolve view named "member" to bean "member", which is an instance of org.springframework.web.servlet.view.xml.MarshallingView. The content type is "application/vnd.opentoast.rest+xml", modelKey is "member" and reference to jaxb2 marshaller bean "marshaller".

The web.xml sets servlet with org.springframework.web.servlet.DispatcherServlet, named opentoastrest, and maps to /*:

<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
 version="2.5">

 <display-name>Open Toast RESTful</display-name>

 <context-param>
  <param-name>contextClass</param-name>
  <param-value>com.springsource.server.web.dm.ServerOsgiBundleXmlWebApplicationContext</param-value>
 </context-param>
 <context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>
   /META-INF/spring/module-context.xml
   /META-INF/spring/osgi-context.xml
  </param-value>
 </context-param>

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

 <servlet>
  <servlet-name>opentoastrest</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <init-param>
   <param-name>contextConfigLocation</param-name>
   <param-value></param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
 </servlet>

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

You can check out the code from Google Code by running

svn checkout http://opentoastproject.googlecode.com/svn/tags/opentoast_20090919 opentoast

Then inside opentoast folder, run

mvn package

to build the whole application. Copy the par file at opentoast/org.opentoast.par/target/OpenToast.par to springsource dm Server pickup folder, and start dm Server.

To test the RESTful Web Service, here is a client example code:

package org.opentoast.client;

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

import org.opentoast.domain.Member;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.web.client.RestTemplate;

public class RestClient
{
    RestTemplate restTemplate;
    
    public RestClient(RestTemplate template)
    {
        this.restTemplate = template;
    }
    
    public void testMember()
    {
        Map<String, String> vars = new HashMap<String, String>();
        vars.put("memberId", "1");
        Member result = restTemplate.getForObject(
                "http://localhost:8080/opentoastrest/members/{memberId}",
                Member.class, vars);
        System.out.println(result);
    }
    
    public static void main(String[] args)
    {
        ApplicationContext ac = new ClassPathXmlApplicationContext(
                "/org/opentoast/client/appContext.xml");
        RestClient client = (RestClient)ac.getBean("restClient");
        client.testMember();
    }
}
With xml config file /org/opentoast/client/appContext.xml:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:oxm="http://www.springframework.org/schema/oxm"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-3.0.xsd">

 <bean id="restClient" class="org.opentoast.client.RestClient">
  <constructor-arg ref="restTemplate"/>
 </bean>

 <bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
  <property name="messageConverters">
   <list>
    <bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
     <constructor-arg ref="marshaller"/>
     <property name="supportedMediaTypes">
      <list>
       <bean class="org.springframework.http.MediaType">
        <constructor-arg value="application"/>
        <constructor-arg value="vnd.opentoast.rest+xml"/>
       </bean>
      </list>
     </property>
    </bean>
   </list>
  </property>
 </bean>

 <oxm:jaxb2-marshaller id="marshaller">
  <oxm:class-to-be-bound name="org.opentoast.domain.Member"/>
 </oxm:jaxb2-marshaller>
 
</beans>
Run this test client program and you will get print out:

[Member 1: Jane Smith]

One big question I have now is "Do I need to expose my rich domain model as REST object model?"

JAXB2 marshaller requires Member class to be annotated with @XmlRootElement. And my domain model class Member is already annotated with persistent API, so now it looks like:

@XmlRootElement
@Entity
@Table(name = "MEMBER")
public class Member extends BaseEntity
{
...

If I don't want REST client to see my rich domain model, I have to use DTO model, and map between those two models. That will defeat the purpose of Domain Driven Design. If I expose my rich domain model classes to client, it will be more problematic when I try to use GWT as client application running inside browser. So far I haven't found a good solution for GWT + REST.

No comments: