Spring MVC RESTFul Web Service CRUD Example
In this tutorial we show you how to build a RESTFul Web Service using Spring MVC 4. We create a controller that’ll manage CRUD operations like Create, Read, Update and Delete using the correct HTTP request methods POST, GET, PUT and DELETE respectively. This rest service consumes and produces JSON – JavaScript Object Notation. We enable Cross-origin Resource Sharing by implementing a custom filter to set the correct headers. To evaluate the rest service, we are using a chrome extension named postman.
Project Structure
The example in this tutorial has the following project structure.
src |--main | +--java | +--com | +--memorynotfound | +--config | |--ServletInitializer.java | |--WebConfig.java | +--controller | |--UserController.java | +--filter | |--CORSFilter.java | +--model | |--User.java | +--service | |--UserService.java | |--UserServiceImpl.java | +--resources | |--logback.xml | +--webapp | |--WEB-INF pom.xml
Maven Dependencies
We use maven to manage our application dependencies automatically, add the following dependencies to your project’s pom.xml file. We are returning JSON, so make sure the com.fasterxml.jackson.core:jackson-databind resides on your class-path. This library will automatically serialize and deserialize your Java Objects to and from JSON.
<?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.memorynotfound.spring.mvc.rest</groupId> <artifactId>crud</artifactId> <version>1.0.0-SNAPSHOT</version> <name>SPRING-MVC - ${project.artifactId}</name> <url>https://memorynotfound.com</url> <packaging>war</packaging> <properties> <spring.version>4.2.6.RELEASE</spring.version> <jackson.version>2.7.4</jackson.version> <logback.version>1.1.7</logback.version> </properties> <dependencies> <!-- spring libraries --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <!-- Needed for JSON View --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <!-- logging --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <!-- servlet api --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.2</version> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> <plugin> <artifactId>maven-war-plugin</artifactId> <version>2.6</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> </build> </project>
We configure the application using Java configuration. This means that we completely remove the web.xml servlet descriptor from the project. We can use the maven-war-plugin to tell the compiler not to fail on a missing web.xml file, by setting the failOnMissingWebXml to false.
Rest Controller
There are multiple ways you can implement a Rest Web Service using Spring MVC 4. In my opinion, the following is one of the more verbose and thus easier to understand ways. By verbose, I mean that you control the ResponseEntity directly in the code, this lets you add headers and HTTP Status codes directly to the response. Let us start by implementing our Rest API. This controller exposes the following Rest APIs:
- HTTP GET request to /users returns a list of users
- HTTP GET request to /users/1 returns the user with id 1
- HTTP POST request to /users with a user object in JSON creates a new user
- HTTP PUT request to /users/1 with a user object in JSON updates the user with id 1
- HTTP DELETE request to /users/1 deletes the user with id 1
package com.memorynotfound.controller; import com.memorynotfound.model.User; import com.memorynotfound.service.UserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.util.UriComponentsBuilder; import java.util.List; @RestController @RequestMapping("/users") public class UserController { private final Logger LOG = LoggerFactory.getLogger(UserController.class); @Autowired private UserService userService; // =========================================== Get All Users ========================================== @RequestMapping(method = RequestMethod.GET) public ResponseEntity<List<User>> getAll() { LOG.info("getting all users"); List<User> users = userService.getAll(); if (users == null || users.isEmpty()){ LOG.info("no users found"); return new ResponseEntity<List<User>>(HttpStatus.NO_CONTENT); } return new ResponseEntity<List<User>>(users, HttpStatus.OK); } // =========================================== Get User By ID ========================================= @RequestMapping(value = "{id}", method = RequestMethod.GET) public ResponseEntity<User> get(@PathVariable("id") int id){ LOG.info("getting user with id: {}", id); User user = userService.findById(id); if (user == null){ LOG.info("user with id {} not found", id); return new ResponseEntity<User>(HttpStatus.NOT_FOUND); } return new ResponseEntity<User>(user, HttpStatus.OK); } // =========================================== Create New User ======================================== @RequestMapping(method = RequestMethod.POST) public ResponseEntity<Void> create(@RequestBody User user, UriComponentsBuilder ucBuilder){ LOG.info("creating new user: {}", user); if (userService.exists(user)){ LOG.info("a user with name " + user.getUsername() + " already exists"); return new ResponseEntity<Void>(HttpStatus.CONFLICT); } userService.create(user); HttpHeaders headers = new HttpHeaders(); headers.setLocation(ucBuilder.path("/user/{id}").buildAndExpand(user.getId()).toUri()); return new ResponseEntity<Void>(headers, HttpStatus.CREATED); } // =========================================== Update Existing User =================================== @RequestMapping(value = "{id}", method = RequestMethod.PUT) public ResponseEntity<User> update(@PathVariable int id, @RequestBody User user){ LOG.info("updating user: {}", user); User currentUser = userService.findById(id); if (currentUser == null){ LOG.info("User with id {} not found", id); return new ResponseEntity<User>(HttpStatus.NOT_FOUND); } currentUser.setId(user.getId()); currentUser.setUsername(user.getUsername()); userService.update(user); return new ResponseEntity<User>(currentUser, HttpStatus.OK); } // =========================================== Delete User ============================================ @RequestMapping(value = "{id}", method = RequestMethod.DELETE) public ResponseEntity<Void> delete(@PathVariable("id") int id){ LOG.info("deleting user with id: {}", id); User user = userService.findById(id); if (user == null){ LOG.info("Unable to delete. User with id {} not found", id); return new ResponseEntity<Void>(HttpStatus.NOT_FOUND); } userService.delete(id); return new ResponseEntity<Void>(HttpStatus.OK); } }
- @RestController was introduced in Spring MVC 4 and is a convenience annotation that itself is annotated with @Controller and @ResponseBody annotation. This eliminates the need of annotating your controller methods with @ResponseBody individually.
- @RequestMapping annotation is used to map URLs such as /users onto an entire class or a particular handler method. A class-level annotation like /users maps a specific request path onto a controller, with additional method-level annotations further narrowing the mapping for a specific HTTP request method like “GET”, “PUT”, “POST” or “DELETE” etc.
- RequestMethod is an enumeration of possible HTTP request methods like GET, PUT, POST, DELETE, etc.
- @PathVariable annotation indicates that a method parameter should be bound to a URI template variable. To process the @PathVariable annotation, Spring MVC needs to find the matching URI template variable by name. You can specify it in the annotation or, if the URI template variable name matches the method argument name, you can omit that detail.
- @RequestBody annotation indicates a method parameter should be bound to the body of the HTTP web request. Behind the scene, a HttpMessageConverter is responsible for converting from the HTTP request message to an object and converting from an object to the HTTP response body.
- @ResponseBody annotation indicates a method return value should be bound to the HTTP response body. By annotating your class with @RestController, you no longer need to add this annotation to every method individually.
- ResponseEntity extends from HttpEntity which allows you to add a HttpStatus code directly to the response. The ResponseEntity represents the entire HTTP response. You can add header information, status codes and add content to the body.
- HttpHeaders represents HTTP request and response headers. This class has some convenience methods for setting popular header types like Content-Type, Access-Control-Allow-Headers, etc.
CorsFilter
A common problem while developing single page applications or applications that use AJAX is Cross Origin Resource Sharing or CORS. For security reasons, most browsers block requests originating from another domain outside the domain from which the request/resource originated.
Problem: When accessing a REST API, you might receive one of the following errors:
XMLHttpRequest cannot load http://example.com/login. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access.
XMLHttpRequest cannot load http://example.com. Origin http://localhost:8080 is not allowed by Access-Control-Allow-Origin.
Solution: Cross-Origin Resource Sharing. We can add HTTP response headers to control inter-domain communication. Using Spring MVC 4 the easiest way is to add a filter. Using the following headers, you can granularly control CORS:
- Access-Control-Allow-Origin (required) indicates which origin site(s) is/are allowed. This header can only contain a single domain, or it can contain a ‘*’ to allow requests from any origin. If – for security reasons – you need to support multiple domains, you can inspect from where the request is coming and allow the domain based on a condition.
- Access-Control-Allow-Methods (required) – Comma-delimited list of the supported HTTP methods.
- Access-Control-Allow-Header (required if the request has an Access-Control-RequestHeaders header) – Comma-delimited list of the supported request headers.
- Access-Control-Allow-Credentials (optional) – By default, cookies are not included in CORS requests. You can include cookies by setting this header to true.
- Access-Control-Max-Age (optional) – The value of this header allows the response to be cached for a specified number of seconds.
package com.memorynotfound.filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class CORSFilter extends OncePerRequestFilter { private final Logger LOG = LoggerFactory.getLogger(CORSFilter.class); @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException { LOG.info("Adding CORS Headers ........................"); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE"); res.setHeader("Access-Control-Allow-Headers", "*"); res.setHeader("Access-Control-Max-Age", "3600"); chain.doFilter(req, res); } }
Servlet Descriptor Initializer
Registers and configures the DispatcherServlet with the location of your Spring configuration file. The CORSFilter is also registered.
package com.memorynotfound.config; import com.memorynotfound.filter.CORSFilter; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; import javax.servlet.Filter; public class ServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getServletConfigClasses() { return new Class[] { WebConfig.class }; } @Override protected String[] getServletMappings() { return new String[] { "/" }; } @Override protected Class<?>[] getRootConfigClasses() { return null; } @Override protected Filter[] getServletFilters() { return new Filter[]{ new CORSFilter()}; } }
Spring MVC Configuration
Next, we need to configure Spring MVC 4. We used Java Configuration. We’ll briefly explain the different configurations.
- @EnableWebMvc imports the Spring MVC configuration from WebMvcConfigurationSupport if it is used in conjunction with @Configuration.
- @ComponentScan configures component scanning directives for use with @Configuration. You need to register which packages it can scan to find your Spring components.
package com.memorynotfound.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @EnableWebMvc @Configuration @ComponentScan("com.memorynotfound") public class WebConfig extends WebMvcConfigurerAdapter { }
The model
package com.memorynotfound.model; import java.io.Serializable; public class User implements Serializable { private int id; private String username; public User() { } public User(int id, String username) { this.id = id; this.username = username; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; if (id != user.id) return false; if (username != null ? !username.equals(user.username) : user.username != null) return false; return true; } @Override public int hashCode() { int result = id; result = 31 * result + (username != null ? username.hashCode() : 0); return result; } @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + '}'; } }
Create the User Service
package com.memorynotfound.service; import com.memorynotfound.model.User; import java.util.List; public interface UserService { List<User> getAll(); User findById(int id); User findByName(String name); void create(User user); void update(User user); void delete(int id); boolean exists(User user); }
package com.memorynotfound.service; import com.memorynotfound.model.User; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @Service public class UserServiceImpl implements UserService { private static final AtomicInteger counter = new AtomicInteger(); static List<User> users = new ArrayList<User>( Arrays.asList( new User(counter.incrementAndGet(), "Daenerys Targaryen"), new User(counter.incrementAndGet(), "John Snow"), new User(counter.incrementAndGet(), "Arya Stark"), new User(counter.incrementAndGet(), "Cersei Baratheon"))); @Override public List<User> getAll() { return users; } @Override public User findById(int id) { for (User user : users){ if (user.getId() == id){ return user; } } return null; } @Override public User findByName(String name) { for (User user : users){ if (user.getUsername().equals(name)){ return user; } } return null; } @Override public void create(User user) { user.setId(counter.incrementAndGet()); users.add(user); } @Override public void update(User user) { int index = users.indexOf(user); users.set(index, user); } @Override public void delete(int id) { User user = findById(id); users.remove(user); } @Override public boolean exists(User user) { return findByName(user.getUsername()) != null; } }
Deploy and Test Spring MVC RESTFul API using Postman
Finally, we created a working Rest API. Now it’s time to test if everything is working properly. We start by deploying the service in a servlet container. We used tomcat, which is running on port 8081 and deployed the Rest API on the spring-mvc-rest context path. You can download this example below if you want to test the example straight away.
1. Getting all the users
HTTP GET http://localhost:8081/spring-mvc-rest/users
As you saw in the UserServiceImpl we mocked a couple of users to bypass the (DAO) database-layer. As you can see, the service returns a list of users in JSON format.
2. Get a single user by ID
HTTP GET http://localhost:8081/spring-mvc-rest/users/1
3. Create a new User
HTTP POST http://localhost:8081/spring-mvc-rest/users
When sending a request body, we need to provide The Content-Type header to instruct the server which format to expect. We are sending JSON format so we need to add a Content-Type header with the value application/json.
When we retrieve all the users again, we can see that a new user has been added.
4. Updating an existing User
HTTP PUT http://localhost:8081/spring-mvc-rest/users/
Remember, every time we send a request with a request body we need to set the Content-Type header. Note: take a look at the response headers. You can see that our CORSFilter added the appropriate headers to the response. This is important when you want to access resources from a different domain.
5. Deleting a User
HTTP DELETE http://localhost:8081/spring-mvc-rest/users/
Download
From:一号门
COMMENTS