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:
- 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.
- 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> - Write
Userclass 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(seeJaxb2RootElementHttpMessageConverter). 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 theUser.javalisting: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(); } } - In order to represent a list of users we will create a new class
UserList.javapackage 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; } } - 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.Mapfor 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 interfaceUserService.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 implementationUserServiceImpl.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(); } } - 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 seeUserController.javasource 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(); } } - 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> - Configure web application. Our
web.xmlconfiguration is also minimal. You can see that we only registered the Spring MVCDispatcherServletand added theLog4jConfigListenerfor 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> - Build the project. From command line, run
mvn clean installcommand from the directory wherepom.xmlis located. Build must be successful. - Deploy the resulted web application. Run
mvn tomcat:runfrom command line from the same location. - 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, runmvn testcommand.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()); } } - Testing our RESTful services using web browser:

Retrieve user with id = 12 
Retrieve user list - 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.javaandUserList.java)
HttpMessageConverter). - Add the following dependency to
- 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:
- A Comparison of Spring MVC and JAX-RS (from InfoQ).
- Using Spring 3.0 MVC for RESTful web services (describes some existing issues along with solutions).
- Spring 3 Type Conversion and Validation (from SpringSource TeamBlog)
- rest-based services with spring
- Configure Maven to generate classes from XML Schema using JAXB
0 comments:
Post a Comment