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
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
(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.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(); } }
- 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; } }
- 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 interfaceUserService.java
:
And after the implementationpackage 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(); }
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(); } }
- 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.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(); } }
- 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.xml
configuration is also minimal. You can see that we only registered the Spring MVCDispatcherServlet
and added theLog4jConfigListener
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>
- Build the project. From command line, run
mvn clean install
command from the directory wherepom.xml
is located. Build must be successful. - Deploy the resulted web application. Run
mvn tomcat:run
from 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 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()); } }
- 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.java
andUserList.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
Hi would you be able to provide an example how to use RestTemplate to POST an ArrayList , in this case how do you POST your UserList object using RestTemplate POST method?
ReplyDeleteIn order to be able to POST an UserList you have to first add support for it in UserController. It should look something like this:
Delete@RequestMapping(value = "/bulk", method = RequestMethod.POST)
@ResponseBody
public UserList bulkCreate(@RequestBody UserList users) {
LOGGER.info("Creating new users {}", users);
// logic goes here, return the created users instead of input
return users;
}
And then you will be able to create bulk users as follows:
UserList createdUsers = restTemplate.postForObject(REST_SERVICE_URL + "/bulk", userList, UserList.class);
Below is a response to Uma Ravi question: "Both Get and Put have same urls. How does the request get differentiated and mapped. Please explain on this."
ReplyDeleteHTTP defines methods (sometimes referred to as verbs) to indicate the desired action to be performed on the identified resource.
In our case the resource (User with id=12), which is identified by a URL (http://host:port/servletContextPath/users/12) supports 3 types of actions identified by corresponding HTTP methods (GET, PUT, DELETE). Although URL is the same, the actions are different.
It works same way as HttpServlet. You map the servlet to a URL path inside web.xml and then implement the actions which you want to support by using doGet(), doPost(), doPut(), doDelete(), etc. methods.
Hi,
ReplyDeleteThanks for this tutorial, but I did not see where data is coming from, for example you said in your browser tests :
Retrieve user with id = 12
Then
Retrieve user list
There is no database or hardcoded data in your code.
thanks
I made the screenshot after running UserControllerTest few times. UserServiceImpl uses a simple map for persistence, so all the created users stays there, in memory.
DeleteThanks Andrei .............. Your explanation is so neat and clean .......... A beginner can understand very easily ........... Hats off to you ... Ur explanation made me to write some comments on you........... Thanks again... Keep up the good work ........... :)
ReplyDeleteHi Andrei,
ReplyDeleteWhen I am running the Unit Test , It's showing the following error .
Note : I have changed the servletContextPath as I tried with the existing one showing the same error .
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running TestSuite
2014-02-08 19:44:00,715 WARN [main] org.springframework.web.client.RestTemplate - POST request for "http://localhost:8080/tapanesh/users" resulted in 404 (Not found); invoking error handler
2014-02-08 19:44:00,740 WARN [main] org.springframework.web.client.RestTemplate - POST request for "http://localhost:8080/tapanesh/users" resulted in 404 (Not found); invoking error handler
2014-02-08 19:44:00,798 WARN [main] org.springframework.web.client.RestTemplate - GET request for "http://localhost:8080/tapanesh/users" resulted in 404 (Not found); invoking error handler
2014-02-08 19:44:00,812 WARN [main] org.springframework.web.client.RestTemplate - POST request for "http://localhost:8080/tapanesh/users" resulted in 404 (Not found); invoking error handler
2014-02-08 19:44:00,825 WARN [main] org.springframework.web.client.RestTemplate - POST request for "http://localhost:8080/tapanesh/users" resulted in 404 (Not found); invoking error handler
2014-02-08 19:44:00,836 WARN [main] org.springframework.web.client.RestTemplate - POST request for "http://localhost:8080/tapanesh/users" resulted in 404 (Not found); invoking error handler
Tests run: 6, Failures: 6, Errors: 0, Skipped: 0, Time elapsed: 1.623 sec <<< FAILURE!
Could u plz tell me why this 404 exception happening ... ?
I will appreciate your time .
Thanks in Advance .
Hi Tapanesh,
DeleteCheck your console after running mvn tomcat:run for the following message:
[INFO] Running war on http://localhost:8080/zmeu-blog-spring-rest
It will show you what is the actual servletContextPath. By default the maven tomcat plugin uses the artifactId for your pom.xml.
Hey, Andrei
ReplyDeleteThank's for your tutorial. But there is a little problem, when I try to deploy .war file on Tomcat I receive the following error: SEVERE: Error listenerStart, and when i go to Logs in Tomcat there is the following error:
Feb 17, 2014 5:08:51 PM org.apache.catalina.core.ApplicationContext log
INFO: Initializing log4j from [classpath:log4j.properties]
Feb 17, 2014 5:08:51 PM org.apache.catalina.core.StandardContext listenerStart
SEVERE: Exception sending context initialized event to listener instance of class org.springframework.web.util.Log4jConfigListener
java.lang.IllegalArgumentException: Invalid 'log4jConfigLocation' parameter: class path resource [log4j.properties] cannot be resolved to URL because it does not exist
at org.springframework.web.util.Log4jWebConfigurer.initLogging(Log4jWebConfigurer.java:155)
at org.springframework.web.util.Log4jConfigListener.contextInitialized(Log4jConfigListener.java:45)
at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4939)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5434)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:901)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:877)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:633)
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:976)
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1653)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
at java.util.concurrent.FutureTask.run(FutureTask.java:262)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
I have placed my applicationContext.xml in WEB-INF, web.xml in WEB-INF and log4j.properties in resources.
Could you help me, please?
Thank's!
Hi Bogdan,
DeleteYou can download the project sources from http://azagorneanu.googlecode.com/svn/trunk/blog/zmeu-blog-spring-rest.zip
There you will see exactly where all the files should be placed.
Hey, could you tell me how can i run the tests, or how to populate the map with users, 'cause for now i've tried all the possible paths and none of them create any new users.
ReplyDeleteHi,
DeleteRun steps 9, 10 and 11. In step 11 run the UserControllerTest.read() test (you will have to enable it first, check the enabled attribute of @Test). The test can be run from your IDE, just make sure you have the TestNG plugin installed. You can use JUnit instead of TestNG, just replace the TestNG annotations with the JUnit one.
The read() test will create and then read an user. After you will be able to read it through browser. Check step 12 and screenshots.