Pages

14 September, 2011

CRUD RESTful services using Spring Framework

In this post I will show how to build CRUD (Create, Read, Update, Delete) REST-ful (Representational State Transfer) services using Spring Framework. Spring Framework version used in this example is 3.0.6.RELEASE. Apache Tomcat version used for testing the web services is 6.0.29 (default Tomcat version used by Tomcat Maven Plugin of Apache Maven 2.2.1). Project sources are available for download. So let’s start, step by step:

  1. First we have to define the resource on which the CRUD operation will be performed and how this resource will be represented. In our example we will use a resource that represents a User. The resource will be represented using XML (Extensible Markup Language). At the end of the post I will show how you can switch the resource representation to JSON (JavaScript Object Notation) or any other supported form.
  2. Create a Maven project. We will use Maven for project management and build. Maven POM is shown below:
    <?xml version="1.0"?>
    <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
        xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <modelVersion>4.0.0</modelVersion>
        <groupId>org.zmeu</groupId>
        <artifactId>zmeu-blog-spring-rest</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <packaging>war</packaging>
        <name>ZMEU Blog Spring REST</name>
        <build>
            <pluginManagement>
                <plugins>
                    <plugin>
                        <artifactId>maven-compiler-plugin</artifactId>
                        <configuration>
                            <source>1.6</source>
                            <target>1.6</target>
                        </configuration>
                    </plugin>
                </plugins>
            </pluginManagement>
        </build>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-web</artifactId>
                <version>3.0.6.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-webmvc</artifactId>
                <version>3.0.6.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.0</version>
            </dependency>
            <!-- Logging dependencies -->
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.6.1</version>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-log4j12</artifactId>
                <version>1.6.1</version>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>1.2.16</version>
                <scope>runtime</scope>
            </dependency>
            <!-- Testing dependencies -->
            <dependency>
                <groupId>org.testng</groupId>
                <artifactId>testng</artifactId>
                <version>6.2</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
    </project>
    
  3. Write User class that will represent our resource. Due to fact that we have decided to represent our resource using XML, we will annotate our class using JAXB (Java Architecture for XML Binding) annotations. When JAXB is used, Spring requires java classes to be annotated with @XmlRootElement (see Jaxb2RootElementHttpMessageConverter). If you are using Java 6 then you don’t have to do anything special because Java 6 is coming already with JAXB2, otherwise you will have to include dependencies to JAXB into your project. You can see below the User.java listing:
    package org.zmeu.blog.spring.rest.domain;
    
    import java.util.Date;
    
    import javax.xml.bind.annotation.XmlAccessType;
    import javax.xml.bind.annotation.XmlAccessorType;
    import javax.xml.bind.annotation.XmlRootElement;
    import javax.xml.bind.annotation.XmlType;
    
    import org.apache.commons.lang3.builder.EqualsBuilder;
    import org.apache.commons.lang3.builder.HashCodeBuilder;
    import org.apache.commons.lang3.builder.ToStringBuilder;
    
    @XmlAccessorType(XmlAccessType.PROPERTY)
    @XmlType(name = "userType")
    @XmlRootElement(name = "user")
    public class User {
        private long id;
        private String name;
        private Date registrationDate;
    
        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 Date getRegistrationDate() {
            return registrationDate;
        }
    
        public void setRegistrationDate(Date registrationDate) {
            this.registrationDate = registrationDate;
        }
    
        @Override
        public String toString() {
            ToStringBuilder builder = new ToStringBuilder(this);
            builder.append("id", getId());
            builder.append("name", getName());
            builder.append("registrationDate", getRegistrationDate());
            return builder.toString();
        }
    
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (!(obj instanceof User)) {
                return false;
            }
            User other = (User) obj;
            EqualsBuilder equalsBuilder = new EqualsBuilder();
            equalsBuilder.append(getId(), other.getId());
            return equalsBuilder.isEquals();
        }
        
        @Override
        public int hashCode() {
            HashCodeBuilder hashCodeBuilder = new HashCodeBuilder();
            hashCodeBuilder.append(getId());
            return hashCodeBuilder.toHashCode();
        }
    }
    
  4. In order to represent a list of users we will create a new class UserList.java
    package org.zmeu.blog.spring.rest.domain;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import javax.xml.bind.annotation.XmlAccessType;
    import javax.xml.bind.annotation.XmlAccessorType;
    import javax.xml.bind.annotation.XmlElement;
    import javax.xml.bind.annotation.XmlRootElement;
    import javax.xml.bind.annotation.XmlType;
    
    @XmlAccessorType(XmlAccessType.PROPERTY)
    @XmlType(name = "userListType")
    @XmlRootElement(name = "userList")
    public class UserList {
        private List<User> users = new ArrayList<User>();
    
        public UserList() {}
    
        public UserList(List<User> users) {
            this.users = users;
        }
    
        @XmlElement(name = "user")
        public List<User> getUsers() {
            return users;
        }
    
        public void setUsers(List<User> users) {
            this.users = users;
        }
    }
    
  5. Write a Spring service that will provide CRUD operations for our resource. In our case this is a simple service which uses a java.util.Map for storage. Normally this should be a service that performs some validations and delegates the persistence activities to a DAO (Data Access Object). First define the interface UserService.java:
    package org.zmeu.blog.spring.rest.service;
    
    import java.util.Collection;
    
    import org.zmeu.blog.spring.rest.domain.User;
    
    public interface UserService {
        User create(User user);
    
        User read(long userId);
    
        User update(User user);
    
        User delete(long userId);
    
        Collection<User> list();
    }
    
    And after the implementation UserServiceImpl.java:
    package org.zmeu.blog.spring.rest.service;
    
    import java.util.Collection;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ConcurrentMap;
    import java.util.concurrent.atomic.AtomicLong;
    
    import org.apache.commons.lang3.Validate;
    import org.springframework.stereotype.Service;
    import org.zmeu.blog.spring.rest.domain.User;
    
    @Service
    public class UserServiceImpl implements UserService {
        private final AtomicLong USER_ID_SEQ = new AtomicLong();
        private final ConcurrentMap<Long, User> usersMap = new ConcurrentHashMap<Long, User>();
        
        @Override
        public User create(User user) {
            user.setId(USER_ID_SEQ.incrementAndGet());
            usersMap.put(user.getId(), user);
            return user;
        }
    
        @Override
        public User read(long userId) {
            return usersMap.get(userId);
        }
    
        @Override
        public User update(User user) {
            User updatedUser = usersMap.replace(user.getId(), user);
            Validate.isTrue(updatedUser != null, "Unable to find user with id: " + user.getId());
            return updatedUser;
        }
    
        @Override
        public User delete(long userId) {
            User removedUser = usersMap.remove(userId);
            Validate.isTrue(removedUser != null, "Unable to find user with id: " + userId);
            return removedUser;
        }
    
        @Override
        public Collection<User> list() {
            return usersMap.values();
        }
    }
    
  6. Write a Spring MVC controller that will do the actual work required by a RESTful service. Our controller maps to the following locations:
    URL HTTP Method Description
    http://host:port/servletContextPath/users GET Retrieves the user list.
    http://host:port/servletContextPath/users POST Creates a new user.
    http://host:port/servletContextPath/users/12 GET Retrieves user with id = 12.
    http://host:port/servletContextPath/users/12 PUT Updates user with id = 12.
    http://host:port/servletContextPath/users/12 DELETE Deletes user with id = 12.

    Below you can see UserController.java source code:
    package org.zmeu.blog.spring.rest.controller;
    
    import java.util.ArrayList;
    
    import org.apache.commons.lang3.Validate;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.ResponseStatus;
    import org.zmeu.blog.spring.rest.domain.User;
    import org.zmeu.blog.spring.rest.domain.UserList;
    import org.zmeu.blog.spring.rest.service.UserService;
    
    @Controller
    @RequestMapping(value = "/users")
    public class UserController {
        private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);
    
        @Autowired
        private UserService userService;
    
        @RequestMapping(method = RequestMethod.POST)
        @ResponseBody
        public User create(@RequestBody User user) {
            LOGGER.info("Creating new user {}", user);
            return userService.create(user);
        }
    
        @RequestMapping(value = "/{userId}", method = RequestMethod.GET)
        @ResponseBody
        public User read(@PathVariable(value = "userId") long userId) {
            LOGGER.info("Reading user with id {}", userId);
            User user = userService.read(userId);
            Validate.isTrue(user != null, "Unable to find user with id: " + userId);
            return user;
        }
    
        @RequestMapping(value = "/{userId}", method = RequestMethod.PUT)
        @ResponseStatus(value = HttpStatus.NO_CONTENT)
        public void update(@PathVariable(value = "userId") long userId, @RequestBody User user) {
            LOGGER.info("Updating user with id {} with {}", userId, user);
            Validate.isTrue(userId == user.getId(), "userId doesn't match URL userId: " + user.getId());
            userService.update(user);
        }
    
        @RequestMapping(value = "/{userId}", method = RequestMethod.DELETE)
        @ResponseStatus(value = HttpStatus.NO_CONTENT)
        public void delete(@PathVariable(value = "userId") long userId) {
            LOGGER.info("Deleting user with id {}", userId);
            userService.delete(userId);
        }
    
        @RequestMapping(method = RequestMethod.GET)
        @ResponseBody
        public UserList list() {
            LOGGER.info("Listing users");
            return new UserList(new ArrayList<User>(userService.list()));
        }
    
        @ExceptionHandler(IllegalArgumentException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ResponseBody
        public String handleClientErrors(Exception ex) {
            LOGGER.error(ex.getMessage(), ex);
            return ex.getMessage();
        }
    
        @ExceptionHandler(Exception.class)
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        @ResponseBody
        public String handleServerErrors(Exception ex) {
            LOGGER.error(ex.getMessage(), ex);
            return ex.getMessage();
        }
    }
    
  7. Configure Spring. As you already observed, we used annotations all over, so Spring configuration is minimal (applicationContext.xml).
    <?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:context="http://www.springframework.org/schema/context" 
            xmlns:mvc="http://www.springframework.org/schema/mvc"
            xsi:schemaLocation="http://www.springframework.org/schema/beans 
                                http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                                http://www.springframework.org/schema/context 
                                http://www.springframework.org/schema/context/spring-context-3.0.xsd
                                http://www.springframework.org/schema/mvc 
                                http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
    
        <context:component-scan base-package="org.zmeu.blog.spring.rest" />
    
        <mvc:annotation-driven />
    
    </beans>
    
  8. Configure web application. Our web.xml configuration is also minimal. You can see that we only registered the Spring MVC DispatcherServlet and added the Log4jConfigListener for logging.
    <?xml version="1.0" encoding="UTF-8"?>
    <web-app version="2.5" 
            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">
    
        <context-param>
            <param-name>log4jConfigLocation</param-name>
            <param-value>classpath:log4j.properties</param-value>
        </context-param>
        <listener>
            <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
        </listener>
    
        <servlet>
            <servlet-name>DispatcherServlet</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>classpath:applicationContext.xml</param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>DispatcherServlet</servlet-name>
            <url-pattern>/</url-pattern>
        </servlet-mapping>
    
    </web-app>
    
  9. Build the project. From command line, run mvn clean install command from the directory where pom.xml is located. Build must be successful.
  10. Deploy the resulted web application. Run mvn tomcat:run from command line from the same location.
  11. Test and consume RESTful services. Below you can see the class that is testing our RESTful services. I used TestNG testing framework (JUnit can be used as well). Tests that are shown below can be used as an example of how to consume RESTful services. All tests are disabled by default @Test(enabled = false). You have to enable them. This is done in order to skip tests execution at the build time. Tests should be run only after deploying the web application. In order to execute the tests, run mvn test command.
    package org.zmeu.blog.spring.rest.controller;
    
    import static org.testng.Assert.assertEquals;
    import static org.testng.Assert.assertTrue;
    import static org.testng.Assert.fail;
    
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.web.client.HttpClientErrorException;
    import org.springframework.web.client.RestTemplate;
    import org.testng.annotations.BeforeClass;
    import org.testng.annotations.Test;
    import org.zmeu.blog.spring.rest.domain.User;
    import org.zmeu.blog.spring.rest.domain.UserList;
    
    @Test(enabled = false)
    public class UserControllerTest {
        private static final String REST_SERVICE_URL = "http://localhost:8080/zmeu-blog-spring-rest/users";
        private RestTemplate restTemplate;
    
        @BeforeClass
        protected void beforeClass() {
            restTemplate = new RestTemplate();
        }
    
        public void create() {
            createAndAssertUser();
        }
    
        public void read() {
            User createdUser = createAndAssertUser();
            User user = restTemplate.getForObject(REST_SERVICE_URL + "/{userId}", User.class, createdUser.getId());
            assertUser(user, createdUser);
        }
    
        public void update() {
            User user = createAndAssertUser();
            user.setName("Updated user name");
            restTemplate.put(REST_SERVICE_URL + "/{userId}", user, user.getId());
            User updatedUser = restTemplate.getForObject(REST_SERVICE_URL + "/{userId}", User.class, user.getId());
            assertUser(updatedUser, user);
        }
    
        public void updateIncorrectUrl() {
            User user = createAndAssertUser();
            user.setName("Updated user name");
            try {
                restTemplate.put(REST_SERVICE_URL + "/{userId}", user, user.getId() + 1);
                fail("Expecting HttpClientErrorException: 400 Bad Request");
            } catch (HttpClientErrorException e) {
                assertEquals(e.getStatusCode(), HttpStatus.BAD_REQUEST);
            }
        }
    
        public void delete() {
            User createdUser = createAndAssertUser();
            restTemplate.delete(REST_SERVICE_URL + "/{userId}", createdUser.getId());
            try {
                restTemplate.getForObject(REST_SERVICE_URL + "/{userId}", User.class, createdUser.getId());
                fail("Expecting HttpClientErrorException: 400 Bad Request");
            } catch (HttpClientErrorException e) {
                assertEquals(e.getStatusCode(), HttpStatus.BAD_REQUEST);
            }
        }
    
        public void list() {
            UserList initialUsers = restTemplate.getForObject(REST_SERVICE_URL, UserList.class);
            User createdUser = createAndAssertUser();
            UserList users = restTemplate.getForObject(REST_SERVICE_URL, UserList.class);
            List<User> createdUsers = new ArrayList<User>(users.getUsers());
            createdUsers.removeAll(initialUsers.getUsers());
            assertEquals(createdUsers.size(), 1);
            assertUser(createdUsers.get(0), createdUser);
        }
    
        private User createAndAssertUser() {
            User user = new User();
            user.setId(0);
            user.setName("User name");
            user.setRegistrationDate(new Date());
            return createAndAssertUser(user);
        }
    
        private User createAndAssertUser(User user) {
            User createdUser = restTemplate.postForObject(REST_SERVICE_URL, user, User.class);
            assertUserNoId(createdUser, user);
            return createdUser;
        }
    
        private void assertUserNoId(User actual, User expected) {
            assertTrue(actual.getId() > 0);
            assertEquals(actual.getName(), expected.getName());
            assertEquals(actual.getRegistrationDate(), expected.getRegistrationDate());
        }
    
        private void assertUser(User actual, User expected) {
            assertTrue(actual.getId() > 0);
            assertEquals(actual.getName(), expected.getName());
            assertEquals(actual.getRegistrationDate(), expected.getRegistrationDate());
        }
    }
    
  12. Testing our RESTful services using web browser:
    RESTful service. Retrieve user by id.
    Retrieve user with id = 12
    RESTful service. Retrieve user list.
    Retrieve user list
  13. Changing the representation of your resources from XML to JSON is very easy. No changes are required to be done in controllers or client side. You have to perform the following actions only:
    • Add the following dependency to pom.xml:
      <dependency>
          <groupId>org.codehaus.jackson</groupId>
          <artifactId>jackson-mapper-asl</artifactId>
          <version>1.8.5</version>
          <scope>runtime</scope>
      </dependency>
      
    • Remove all JAXB annotations from data-type classes (User.java and UserList.java)
    Taking an arbitrary Java object returned from a controller handler method and converting it into a client-pleasing representation is a job for one of Spring’s HTTP message converters. Spring comes with a variety of message converters: XML, JSON, Atom, RSS, binary, String, etc (see all classes implementing HttpMessageConverter).
  14. You are done!

IMPORTANT: Although Spring 3 supports validation of arguments and return types for Spring MVC controllers using JSR-303 (Bean validation), it doesn’t work in case of RESTful controllers (see also bugs SPR-6709 and SPR-6928).

Also you can have a look at the following related articles:

0 comments:

Post a Comment