Tuesday, July 17, 2012

RESTEasy Spring Integration - Tutorial Part 1 - Introduction

This is step by step tutorial explaining how to integrate RESTEasy with Spring.
First tutorial part covers setting up simple REST Service, and request processing by Spring components.

Spring implementation is based version 3 with dependency injection. I do not use Spring annotations, but javax.inject instead. I find this approach more flexible, since is not tightly coupled with particular technology.

RESTEasy can be deployed in a various ways - I will concentrate on the most popular one - which is the web application with component scan. At the begining we will start with web 2.4 and later on update to 3.0.

Stand alone deployment, without spring would consists of two main parts - servlet for request processing, bootstrap listener which handles configuration, and finally REST Services itself. Bootstrap listener scans classpath and looks for classes annotated with @Path - those are potential candidates for REST Service. They will be registered in request dispatcher in order to handle incoming HTTP calls. Since this is stand alone deployment, RESTEasy will simply create instances of found REST Services - this means, that those instances and not deployed in any particular container (except tomcat). For example they cannot access Spring context - especially because there is none ;). But if developer would like to access Spring context within such setup, he would need to manually load Spring context from REST Service class. This is typical approach when accessing Spring from servlet, but there is much better solution:

Let Spring manage services and their dependencies, and RESTEasy handles REST/HTTP related stuff.

In the stand alone example, the REST Services (annotated with @Path) were created by RESTEasy classpath scanner. Now we will use Spring to instantiate REST Services and all dependent components.
When the web application starts, as first step the SpringContextLoaderListener is being triggered - it initializes Spring context and all declared components.
In the second step, the RESTEasy bootstrap is being triggered - but this is special one, designed for usage with spring. It does not scan classpath for @Path annotated classes - instead it queries Spring for all beans with @Path annotation, and Spring returns references to those beans. This is a big difference, when compared to standalone setup. RESTEasy will not create new instance of REST Service, it will reference beans created by Spring. Basically this is the main difference between standalone deployment and one based on Spring - REST Service instances are created by Spring, so they are running within Spring container, and have access to all Spring components, like any other Spring bean. RESTEasy only forwards incoming calls to those Spring beans.

Hints:
  • Service, component, spring managed bean, REST Service - all are synonyms, and technically they are Plain Java Objects Objects managed by Spring
  • Do not instantiate services with new operator. This would create new class outside of Spring context, and such class cannot access Spring manged beans, and therefore injections will not work (will remain null).
  • Do not register REST servlet on root context ("/*"), always use some path ("/rest/*"). You might need to add another servlet in the future, and this new servlet will conflict with one registered on root context. You can remove extra path with proper Apache configuration.

Tutorial Application

This is a web application which exposes simple REST Service. Its implementation forwards incoming calls to Spring component, which handles business logic - in our case it is answer to traditional "Hello World" message.

REST Service exposes two methods:
  • GET - /Hello/text -  request/response attributes are passed as text in URL. Request has single msg path parameter.  Response contains message from request with appended "Hello" text.
  • POST - /Hello/javabean - request/response attributes are passed in body as JSON/XML documents. Request contains msg and additionally gender attribute. Response contains message from request with appended "Hello Sir" or "Hello Madam" - depending on provided gender. Response includes also server time, it's not formatted - this will be covered later.

Curl examples:
>> GET REQUEST <<
curl -i http://localhost:8080/resteasy_spring_p1/rest/Hello/text?msg=Hi%20There
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 17

>> RESPONSE <<
Hi There--> Hello

>> POST REQUEST <<
curl -i -X POST -H "Content-Type: application/json" -d '{"msg":"Hi There","gender":"MALE"}' 
  http://localhost:8080/resteasy_spring_p1/rest/Hello/javabean
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/json
Transfer-Encoding: chunked

>> RESPONSE <<
{"response":"Hi There--> Hello Sir","serverTime":1341305935291}

Project Structure (Maven)













  • org.mmiklas.resttutorial.model - POJOs / transfer objects for REST Service

    • org.mmiklas.resttutorial.rest - REST Service (Resource) implementation
    • org.mmiklas.resttutorial.server - Spring component containing backed implementation
    • resources - XML configuration files for logger and Spring
    • webapp/WEB-INF - deployment descriptor










    Spring Configuration


    Spring configuration consists of single XML file which enables classpath component scan. Dependencies between Spring components are declared by annotations - @Named declares Spring component, and @Inject references existing component.

    src/main/resources/tutorial-spring-context.xml 
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="..">
        <context:component-scan base-package="org.mmiklas.resttutorial" />
    </beans>

    Deployment Descriptor


    web.xml has following functions (later on we will update it to 3.0, now its 2.4):
    • initializes Spring context from XML file
    • initializes RESTEasy and enables Spring integration
    • registers RESTEasy dispatcher servlet on "/rest/*" context

    src/main/webapp/WEB-INF/web.xml
    <?xml version="1.0" encoding="UTF-8" ?>
    <web-app xmlns="...">
    
        <display-name>Spring REST Easy Tutorial</display-name>
    
        <!-- Custom name for main spring configuration -->
        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
                classpath:tutorial-spring-context.xml
            </param-value>
        </context-param>
    
        <!-- RESTEasy configuration -->
        <listener>
            <listener-class>org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap
            </listener-class>
        </listener>
    
        <!-- RESTEasy <-> Spring connector (RESTEasy can access Spring beans) -->
        <listener>
            <listener-class>org.jboss.resteasy.plugins.spring.SpringContextLoaderListener
            </listener-class>
        </listener>
    
        <!-- RESTEasy HTTP Request processor -->
        <context-param>
            <param-name>resteasy.servlet.mapping.prefix</param-name>
            <param-value>/rest</param-value>
        </context-param>
        <servlet>
            <servlet-name>restservlet</servlet-name>
            <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher
            </servlet-class>
        </servlet>
    
        <!-- NEVER map servlet to root context "/*" ! -->
        <servlet-mapping>
            <servlet-name>restservlet</servlet-name>
            <url-pattern>/rest/*</url-pattern>
        </servlet-mapping>
    </web-app>
    

    Transfer Objects (POJOs)

    Transfer objects will be serialized and deserialized from/to JSON. The configuration in both cases is made with JAXB annotations, new JSON parsers are supporting those - but make sure that you have latest version, because it was not always the case.


    package org.mmiklas.resttutorial.model;
    
    public enum Gender {
        MALE, FEMALE;
    }


    package org.mmiklas.resttutorial.model;
    
    import javax.xml.bind.annotation.XmlAccessType;
    import javax.xml.bind.annotation.XmlAccessorType;
    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlRootElement
    @XmlAccessorType(XmlAccessType.FIELD)
    public class HelloMessage {
    
        private String msg;
    
        private Gender gender;
    
        public String getMsg() {
            return msg;
        }
    
        public void setMsg(String msg) {
            this.msg = msg;
        }
    
        public Gender getGender() {
            return gender;
        }
    
        public void setGender(Gender gender) {
            this.gender = gender;
        }
    }
    


    package org.mmiklas.resttutorial.model;
    
    import java.util.Date;
    
    import javax.xml.bind.annotation.XmlAccessType;
    import javax.xml.bind.annotation.XmlAccessorType;
    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlRootElement
    @XmlAccessorType(XmlAccessType.FIELD)
    public class HelloResponse {
    
        private String response;
    
        private Date serverTime;
    
        public HelloResponse(String response, Date serverTime) {
            this.response = response;
            this.serverTime = serverTime;
        }
    
        public String getResponse() {
            return response;
        }
    
        public Date getServerTime() {
            return serverTime;
        }
    
    }
    

    REST Service


    REST implementation is based only on standard Java API - it does not have Spring or RESTEasy dependencies. This is really good approach to use standardized API as far as possible - it does not matter whenever you are planning to stick with particular framework forever - clean standardized code is easier to read.

    @Named annotation defines HelloRestService class as Spring component, and @Path declares this class additionally as REST Service provider. Spring will create instance of our class, and RESTEasy will expose its methods over HTTP.

    sayTextHello accepts GET messages on ..../Hallo/text and reads query parameter msg.

    sayJavaBeanHello accepts POST messages on ..../Hallo/javabean and supports XML and JSON documents. XML is supported by default, since Java contains required providers (marshaller/unmarshaller). JSON must be enabled - since it's not JDK part - I will describe that in next chapter.
     
    package org.mmiklas.resttutorial.rest;
    
    import javax.inject.Inject;
    import javax.inject.Named;
    import javax.ws.rs.Consumes;
    import javax.ws.rs.GET;
    import javax.ws.rs.POST;
    import javax.ws.rs.Path;
    import javax.ws.rs.Produces;
    import javax.ws.rs.QueryParam;
    import javax.ws.rs.core.MediaType;
    import javax.ws.rs.core.Response;
    
    import org.mmiklas.resttutorial.model.HelloMessage;
    import org.mmiklas.resttutorial.model.HelloResponse;
    import org.mmiklas.resttutorial.server.HelloSpringService;
    
    @Named
    @Path("/Hello")
    public class HelloRestService {
    
        @Inject
        private HelloSpringService halloService;
    
        // curl http://localhost:8080/resteasy_spring_p1/rest/Hello/text?msg=Hi%20There
        @GET
        @Path("text")
        @Produces(MediaType.APPLICATION_FORM_URLENCODED)
        public Response sayTextHello(@QueryParam("msg") String msg) {
            String resp = halloService.sayTextHello(msg);
            return Response.ok(resp).build();
        }
    
        // curl -X POST -H "Content-Type: application/json" -d '{"msg":"Hi There","gender":"MALE"}'
        //   http://localhost:8080/resteasy_spring_p1/rest/Hello/javabean
        @POST
        @Path("javabean")
        @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
        @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
        public Response sayJavaBeanHello(HelloMessage msg) {
            HelloResponse resp = halloService.sayJavaBeanHello(msg);
            return Response.ok(resp).build();
        }
    }
    

    Spring Component (Backend)


    Spring implementation is self explanatory - service is declared with @Named. @Component would work as well, but it's not standarized annotation.
    @Named
    public class HelloSpringService {
    
        public String sayTextHello(String msg) {
            return msg + "--> Hello";
        }
    
        public HelloResponse sayJavaBeanHello(HelloMessage msg) {
            return new HelloResponse(msg.getMsg() + "--> Hello " + 
        (msg.getGender() == Gender.MALE ? "Sir" : "Madam"), new Date());
        }
    }

    JSON Provider


    sayJavaBeanHello REST method supports JSON media type. In order to enable it, we only need to have proper jar in classpath:
    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-jackson-provider</artifactId>
        <version>2.3.2.Final</version>
    </dependency>

    RESTEasy scans during bootstrap phase classpath and searches for content providers - they are divided into two groups - marshaller and unmarshaller:
    • unmarshaller class in annotated with @Provider, @Consumes and implements MessageBodyReader interface
    • marshaller class in annotated with @Provider, @Produces and implements MessageBodyWriter interface
    @Provider annotation is only marker declaring, that given class has some interesting functionality - first the implementing interface specifies it - annotation alone is not sufficient. @Produces and @Consumes are defining mime type which is supported by our provider - the same annotations can be found on rest methods.

    Both interfaces above have generic type (T), which is currently set to Object - it provides possibility to register dedicated provider for the same mime type, but different class type. For example we can register different provider for User and Group and both for mime type JSON.

    @Provider
    @Produces(MediaType.APPLICATION_JSON)
    public class MyMarshaller implements MessageBodyWriter<Object> {
    
        @Override
        public boolean isWriteable(Class<?> type, Type genericType, 
                Annotation[] annotations, MediaType mediaType) {
            return false;
        }
    
        @Override
        public long getSize(Object t, Class<?> type, Type genericType, 
                Annotation[] annotations, MediaType mediaType) {
            return 0;
        }
    
        @Override
        public void writeTo(Object t, Class<?> type, Type genericType, 
                Annotation[] annotations, MediaType mediaType,
                MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) 
                        throws IOException, WebApplicationException {
        }
    }

    @Provider
    @Consumes(MediaType.APPLICATION_JSON)
    public class MyUnmarshaller implements MessageBodyReader<Object> {
    
        @Override
        public boolean isReadable(Class<?> type, Type genericType, 
                Annotation[] annotations, MediaType mediaType) {
            return false;
        }
    
        @Override
        public Object readFrom(Class<Object> type, Type genericType, 
                Annotation[] annotations, MediaType mediaType,
                MultivaluedMap<String, String> httpHeaders, InputStream entityStream) 
                        throws IOException, WebApplicationException {
            return null;
        }
    }

    Maven build - pom.xml


    <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/maven-v4_0_0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>org.mmiklas.resteasy</groupId>
        <artifactId>resteasy_spring_p1</artifactId>
        <packaging>war</packaging>
        <version>1.0</version>
        <name>RESTEasy Spring - Tutorial - Part 1</name>
    
        <dependencies>
            <!-- Logger -->
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>1.2.16</version>
            </dependency>
    
            <!-- Spring -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
                <version>3.1.0.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-web</artifactId>
                <version>3.1.0.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-beans</artifactId>
                <version>3.1.0.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>javax.inject</groupId>
                <artifactId>javax.inject</artifactId>
                <version>1</version>
            </dependency>
    
            <!-- RESTEasy -->
            <dependency>
                <groupId>org.jboss.resteasy</groupId>
                <artifactId>jaxrs-api</artifactId>
                <version>2.3.2.Final</version>
            </dependency>
            <dependency>
                <groupId>org.jboss.resteasy</groupId>
                <artifactId>resteasy-jaxrs</artifactId>
                <version>2.3.2.Final</version>
            </dependency>
            <dependency>
                <groupId>org.jboss.resteasy</groupId>
                <artifactId>resteasy-jackson-provider</artifactId>
                <version>2.3.2.Final</version>
            </dependency>
            <dependency>
                <groupId>org.jboss.resteasy</groupId>
                <artifactId>resteasy-spring</artifactId>
                <version>2.3.2.Final</version>
            </dependency>
            <dependency>
                <groupId>org.jboss.resteasy</groupId>
                <artifactId>resteasy-jaxb-provider</artifactId>
                <version>2.3.2.Final</version>
            </dependency>
        </dependencies>
    </project>
    

    Project Source Download


    resteasy_spring_p1_src.zip
    https://github.com/maciejmiklas/resteasy_spring_p1.git

    1 comment: