Saturday, October 19, 2013

REST with JAX-RS: Part 1 - Spring Java Config

There are many Java REST frameworks. We can devide these frameworks on three groups:
  1. Spring MVC. It can be used to implement RESTful services. This framework has been widly used, mature, etc. But, Spring, in general, does not support JAX-RS standard.
  2. JAX-RS implementation. I know at least four frameworks:
  3. Non-Standard. I.e. frameworks which do not support JAX-RS, or addition many other features. Please note, it assume that Spring MVC can be called "standard" ;-)
    • Dropwizard very cool frameworks. It supports not only JAX-RS. 
    • RESTX, lightweight framework.
It's logically to ask yourself why don't use Spring MVC for REST services development. There is a  very good article on InfoQ: A Comparison of Spring MVC and JAX-RS. I consider to use JAX-RS frameworks for REST API and Spring MVC for everything else . The most popular are Apache CXF  and Jersey. Also, Apache CXF has SOAP services support. Actually, you can easily switch between JAX-RS frameworks till you use standard approaches.

Let's create simple Spring JAX-RS application with Spring Java Configs (see sample application based on Spring context xml [3])

Create pom.xml file

    4.0.0
    jaxrs-tutorials
    jaxrs-tutorials
    war
    1.0-SNAPSHOT
    jaxrs-tutorials Maven Webapp
    http://maven.apache.org

    
        UTF-8
        1.7

        2.7.6
        3.2.3.RELEASE
        1.7.5
        1.0.13
        3.0.1
        2.0.2

        3.0
        2.0
        2.2
        2.6
    

    

        
            org.apache.cxf
            cxf-rt-frontend-jaxrs
            ${cxf.version}
        

        
        
            org.springframework
            spring-context
            ${org.springframework-version}
            
                
                
                    commons-logging
                    commons-logging
                
            
        
        
            org.springframework
            spring-webmvc
            ${org.springframework-version}
        

        
        
            org.slf4j
            slf4j-api
            ${org.slf4j-version}
        
        
            org.slf4j
            jcl-over-slf4j
            ${org.slf4j-version}
            runtime
        
        
            ch.qos.logback
            logback-classic
            ${ch.qos.logback-version}
        

        
        
            javax.servlet
            javax.servlet-api
            ${servlet-version}
            provided
        

        
        
            com.fasterxml.jackson.jaxrs
            jackson-jaxrs-json-provider
            ${jackson-version}
        

    

    

        jaxrs-tutorials

        
            
                org.apache.tomcat.maven
                tomcat7-maven-plugin
                2.0
                
                    /
                    8080
                
            
            
                org.apache.maven.plugins
                maven-compiler-plugin
                ${maven-compiler-plugin-version}
                
                    ${java-version}
                    ${java-version}
                
            
        
    

Create some sample entity
package com.halyph.entity;

public class User {

    private Integer id;
    private String name;

    public User() {
    }

    public User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return String.format("{id=%s,name=%s}", id, name);
    }
}
We should implement Service(s) which manages this entity:
package com.halyph.service;

import com.halyph.entity.User;

import javax.ws.rs.core.Response;
import java.util.Collection;

public interface UserService {

    Collection<User> getUsers();

    User getUser(Integer id);

    Response add(User user);

}
package com.halyph.service;

import com.halyph.entity.User;
import org.springframework.stereotype.Service;

import javax.ws.rs.core.Response;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

@Service("userService")
public class UserServiceImpl implements UserService {

     private static Map<Integer, User> users = new HashMap<Integer, User>();

    static {
        users.put(1, new User(1, "foo"));
        users.put(2, new User(2, "bar"));
        users.put(3, new User(3, "baz"));
    }

    public UserServiceImpl() {
    }

    @Override
    public Collection<User> getUsers() {
        return users.values();
    }

    @Override
    public User getUser(Integer id) {
        return users.get(id);
    }

    @Override
    public Response add(User user) {
        user.setId(users.size()+1);
        users.put(user.getId(), user);

        //do more stuff to add user to the system..
        return Response.status(Response.Status.OK).build();
    }

}
It's time to introduce REST services. with the next endpoints /api/users and /api/exception. So, we  have bunch of REST URIs:
  • GET /api/users - get all users
  • GET /api/users/{id} - get user with id
  • POST /api/users - accept "user" json and create the specified user on back-end
  • GET /api/exception - throw exception
 
package com.halyph.rest;

import com.halyph.entity.User;
import com.halyph.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Collection;

@Path("/users")
@Produces({MediaType.APPLICATION_JSON})
@Consumes({MediaType.APPLICATION_JSON})
public class UserResource {

    private static Logger log = LoggerFactory.getLogger(UserResource.class);

    @Autowired
    UserService service;

    public UserResource() {
    }

    @GET
    public Collection<User> getUsers() {
        return service.getUsers();
    }

    @GET
    @Path("/{id}")
    public User getUser(@PathParam("id") Integer id) {
        return service.getUser(id);
    }

    @POST
    public Response add(User user) {
        log.info("Adding user {}", user.getName());
        service.add(user);
        return Response.status(Response.Status.OK).build();
    }
}
Also we added /api/exception REST url to demonstrate how CXF deals with exceptions:
package com.halyph.rest;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/exception")
public class ExceptionResource {

    public ExceptionResource() { }

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String generateException() throws Exception {
        throw new Exception("generateException from ExceptionResource");
    }
}

So, what's left? In general we are creating some web.xml where we configure Apache CXF, etc. But, we will use Spring feature and implement all our configuration in Spring Java Configs.
Our web.xml willl be empty, some App Servers still require it:

Next, we should create some class which does the same work which had been done by web.xml:
package com.halyph.config;

import org.apache.cxf.transport.servlet.CXFServlet;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import java.util.Set;

public class WebAppInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        servletContext.addListener(new ContextLoaderListener(createWebAppContext()));
        addApacheCxfServlet(servletContext);
    }

    private void addApacheCxfServlet(ServletContext servletContext) {
        CXFServlet cxfServlet = new CXFServlet();

        ServletRegistration.Dynamic appServlet = servletContext.addServlet("CXFServlet", cxfServlet);
        appServlet.setLoadOnStartup(1);

        Set<String> mappingConflicts = appServlet.addMapping("/api/*");
    }

    private WebApplicationContext createWebAppContext() {
        AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
        appContext.register(AppConfig.class);
        return appContext;
    }

}
So, "web.xml" is configured, now we should configure Spring context:
package com.halyph.config;

import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
import com.halyph.rest.UserResource;
import org.apache.cxf.bus.spring.SpringBus;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import com.halyph.rest.ExceptionResource;
import com.halyph.service.UserService;
import com.halyph.service.UserServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import javax.ws.rs.ext.RuntimeDelegate;
import java.util.Arrays;

@Configuration
public class AppConfig {

    @ApplicationPath("/")
    public class JaxRsApiApplication extends Application { }

    @Bean(destroyMethod = "shutdown")
    public SpringBus cxf() {
        return new SpringBus();
    }

    @Bean
    @DependsOn("cxf")
    public Server jaxRsServer(ApplicationContext appContext) {
        JAXRSServerFactoryBean factory = RuntimeDelegate.getInstance().createEndpoint(jaxRsApiApplication(), JAXRSServerFactoryBean.class);
        factory.setServiceBeans(Arrays.<Object>asList(userResource(), exceptionResource()));
        factory.setAddress("/" + factory.getAddress());
        factory.setProvider(jsonProvider());
        return factory.create();
    }

    @Bean
    public JaxRsApiApplication jaxRsApiApplication() {
        return new JaxRsApiApplication();
    }

    @Bean
    public JacksonJsonProvider jsonProvider() {
        return new JacksonJsonProvider();
    }

    @Bean
    public UserService userService() {
        return new UserServiceImpl();
    }

    @Bean
    public UserResource userResource() {
        return new UserResource();
    }

    @Bean
    public ExceptionResource exceptionResource() {
        return new ExceptionResource();
    }
}
Please note, how service and rest beans have been registered, also we added jackson provider which serialize bean in JSON format.

We almost done, now we should verify out work. Run application:
mvn clean tomcat7:run

Test REST API:
curl http://localhost:8080/api/users
curl http://localhost:8080/api/users/1
curl -v http://localhost:8080/api/exception
curl http://localhost:8080/api/users -X POST -H "Content-Type: application/json" -d '{"name":"John"}'
curl http://localhost:8080/api/users

After the last call you should get four users from back-end.
$ curl http://localhost:8080/api/users
[{"id":1,"name":"foo"},{"id":2,"name":"bar"},{"id":3,"name":"baz"}]

$ curl http://localhost:8080/api/users -X POST -H "Content-Type: application/json" -d '{"name":"John"}'

$  curl http://localhost:8080/api/users
[{"id":1,"name":"foo"},{"id":2,"name":"bar"},{"id":3,"name":"baz"},{"id":4,"name":"John"}]

   You can find sources on GitHub.

References
  1. InfoQ: A Comparison of Spring MVC and JAX-RS
  2. REST client, CXF server : JAX-RS or SPRING-MVC ?
  3. REST web services with JAX-RS, Apache CXF and Spring Security
  4. Official Apache CXF Doc: Configuring JAX-RS services in container with Spring configuration file.
  5. Converting Jersey REST Examples to Apache CXF

1 comment:

  1. Just want to say thanks - this helped me finally connect all the dots to go all Java-config for a CXF app (JAX-WS, but pretty much the same setup).

    ReplyDelete