In this post we will
write a CRUD Restful WebService using Spring MVC 4, and write a REST client
with RestTemplate to consume those services. We will also test those services
using external clients.
Short & Quick introduction to REST
REST stands for Representational State Transfer. It’s an
is an architectural style which can be used to design web services, that can be
consumed from a variety of clients. The core idea is that, rather than using
complex mechanisms such as CORBA, RPC or SOAP to connect between machines,
simple HTTP is used to make calls among them.
In Rest based design, resources are being
manipulated using a common set of verbs.
- To
Create a resource : HTTP POST should be used
- To
Retrieve a resource : HTTP GET should be used
- To
Update a resource : HTTP PUT should be used
- To
Delete a resource : HTTP DELETE should be used
That means, you as a REST service developer
or Client, should comply to above criteria, in order to be REST complained.
Often Rest based Web services return JSON or
XML as response, although it is not limited to these types only. Clients can
specify (using HTTP Accept header) the resource type they are
interested in, and server may return the resource , specifying Content-Type of
the resource it is serving.
Rest Based Controller
Following is one possible Rest based
controller, implementing REST API. I said possible, means Other’s may implement
it in another way, still (or even more pure way) conforming to REST style.
This is what our REST API does:
- GET request
to /api/user/ returns a list of users
- GET request
to /api/user/1 returns the user with ID 1
- POST request
to /api/user/ with a user object as JSON creates a new user
- PUT request
to /api/user/3 with a user object as JSON updates the user with ID 3
- DELETE request
to /api/user/4 deletes the user with ID 4
- DELETE request
to /api/user/ deletes all the users
HelloWorldRestController.java
package
com.websystique.springmvc.controller;
import
java.util.List;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.http.HttpHeaders;
import
org.springframework.http.HttpStatus;
import
org.springframework.http.MediaType;
import
org.springframework.http.ResponseEntity;
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.RestController;
import
org.springframework.web.util.UriComponentsBuilder;
import com.websystique.springmvc.model.User;
import
com.websystique.springmvc.service.UserService;
@RestController
public
class HelloWorldRestController {
@Autowired
UserService userService; //Service which will do all data
retrieval/manipulation work
//-------------------Retrieve All
Users--------------------------------------------------------
@RequestMapping(value = "/user/",
method = RequestMethod.GET)
public ResponseEntity
- >
listAllUsers() {
List users =
userService.findAllUsers();
if(users.isEmpty()){
return new
ResponseEntity
- >(HttpStatus.NO_CONTENT);//You many decide
to return HttpStatus.NOT_FOUND
}
return new
ResponseEntity
- >(users, HttpStatus.OK);
}
//-------------------Retrieve Single
User--------------------------------------------------------
@RequestMapping(value =
"/user/{id}", method = RequestMethod.GET, produces =
MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity
getUser(@PathVariable("id") long id) {
System.out.println("Fetching User
with id " + id);
User user = userService.findById(id);
if (user == null) {
System.out.println("User with
id " + id + " not found");
return new
ResponseEntity(HttpStatus.NOT_FOUND);
}
return new
ResponseEntity(user, HttpStatus.OK);
}
//-------------------Create a
User--------------------------------------------------------
@RequestMapping(value = "/user/",
method = RequestMethod.POST)
public ResponseEntity
createUser(@RequestBody User user,
UriComponentsBuilder ucBuilder) {
System.out.println("Creating User
" + user.getName());
if (userService.isUserExist(user)) {
System.out.println("A User
with name " + user.getName() + " already exist");
return new
ResponseEntity(HttpStatus.CONFLICT);
}
userService.saveUser(user);
HttpHeaders headers = new
HttpHeaders();
headers.setLocation(ucBuilder.path("/user/{id}").buildAndExpand(user.getId()).toUri());
return new
ResponseEntity(headers, HttpStatus.CREATED);
}
//------------------- Update a User
--------------------------------------------------------
@RequestMapping(value =
"/user/{id}", method = RequestMethod.PUT)
public ResponseEntity
updateUser(@PathVariable("id") long id, @RequestBody User user) {
System.out.println("Updating User
" + id);
User currentUser =
userService.findById(id);
if (currentUser==null) {
System.out.println("User with
id " + id + " not found");
return new
ResponseEntity(HttpStatus.NOT_FOUND);
}
currentUser.setName(user.getName());
currentUser.setAge(user.getAge());
currentUser.setSalary(user.getSalary());
userService.updateUser(currentUser);
return new
ResponseEntity(currentUser, HttpStatus.OK);
}
//------------------- Delete a User
--------------------------------------------------------
@RequestMapping(value =
"/user/{id}", method = RequestMethod.DELETE)
public ResponseEntity
deleteUser(@PathVariable("id") long id) {
System.out.println("Fetching &
Deleting User with id " + id);
User user = userService.findById(id);
if (user == null) {
System.out.println("Unable to
delete. User with id " + id + " not found");
return new
ResponseEntity(HttpStatus.NOT_FOUND);
}
userService.deleteUserById(id);
return new
ResponseEntity(HttpStatus.NO_CONTENT);
}
//------------------- Delete All Users
--------------------------------------------------------
@RequestMapping(value = "/user/",
method = RequestMethod.DELETE)
public ResponseEntity
deleteAllUsers() {
System.out.println("Deleting All
Users");
userService.deleteAllUsers();
return new
ResponseEntity(HttpStatus.NO_CONTENT);
}
}
Detailed
Explanation:
@RestController :
First of all, we are using Spring 4′s new @RestController annotation. This
annotation eliminates the need of annotating each method with @ResponseBody.
Under the hood, @RestController is itself annotated with @ResponseBody, and can
be considered as combination of @Controller and @ResponseBody.
@RequestBody :
If a method parameter is annotated with @RequestBody, Spring will bind the
incoming HTTP request body(for the URL mentioned in @RequestMapping for that
method) to that parameter. While doing that, Spring will [behind the scenes]
use HTTP
Message converters to convert the HTTP request body into domain object
[deserialize request body to domain object], based on ACCEPT or Content-Type
header present in request.
@ResponseBody :
If a method is annotated with @ResponseBody, Spring will bind the return value
to outgoing HTTP response body. While doing that, Spring will [behind the scenes]
use HTTP
Message converters to convert the return value to HTTP response body
[serialize the object to response body], based on Content-Type present in
request HTTP header. As already mentioned, in Spring 4, you may stop using this
annotation.
ResponseEntity is
a real deal. It represents the entire HTTP response. Good thing about it is
that you can control anything that goes into it. You can specify status code,
headers, and body. It comes with several constructors to carry the information
you want to sent in HTTP Response.
@PathVariable This
annotation indicates that a method parameter should be bound to a URI template
variable [the one in '{}'].
Basically,
@RestController , @RequestBody, ResponseEntity & @PathVariable are all you
need to know to implement a REST API in Spring 4. Additionally, spring provides
several support classes to help you implement something customized.
MediaType
: With
@RequestMapping annotation, you can additionally, specify the MediaType to be
produced or consumed (using produces or consumes attributes)
by that particular controller method, to further narrow down the mapping.
pom.xml:
UserService.java
package
com.websystique.springmvc.service;
import
java.util.List;
import
com.websystique.springmvc.model.User;
public
interface UserService {
User findById(long id);
User findByName(String name);
void saveUser(User user);
void updateUser(User user);
void deleteUserById(long id);
List findAllUsers();
void deleteAllUsers();
public boolean isUserExist(User user);
}
UserServiceImpl.java
package
com.websystique.springmvc.service;
import
java.util.ArrayList;
import
java.util.Iterator;
import
java.util.List;
import
java.util.concurrent.atomic.AtomicLong;
import
org.springframework.stereotype.Service;
import
org.springframework.transaction.annotation.Transactional;
import
com.websystique.springmvc.model.User;
@Service("userService")
@Transactional
public
class UserServiceImpl implements UserService{
private static final AtomicLong counter =
new AtomicLong();
private static List users;
static{
users= populateDummyUsers();
}
public List findAllUsers() {
return users;
}
public User findById(long id) {
for(User user : users){
if(user.getId() == id){
return user;
}
}
return null;
}
public User findByName(String name) {
for(User user : users){
if(user.getName().equalsIgnoreCase(name)){
return user;
}
}
return null;
}
public void saveUser(User user) {
user.setId(counter.incrementAndGet());
users.add(user);
}
public void updateUser(User user) {
int index = users.indexOf(user);
users.set(index, user);
}
public void deleteUserById(long id) {
for (Iterator iterator =
users.iterator(); iterator.hasNext(); ) {
User user = iterator.next();
if (user.getId() == id) {
iterator.remove();
}
}
}
public boolean isUserExist(User user) {
return
findByName(user.getName())!=null;
}
private static List
populateDummyUsers(){
List users = new
ArrayList();
users.add(new
User(counter.incrementAndGet(),"Sam",30, 70000));
users.add(new
User(counter.incrementAndGet(),"Tom",40, 50000));
users.add(new
User(counter.incrementAndGet(),"Jerome",45, 30000));
users.add(new
User(counter.incrementAndGet(),"Silvia",50, 40000));
return users;
}
public void deleteAllUsers() {
users.clear();
}
}
Model Class:User.java
package
com.websystique.springmvc.model;
public
class User {
private long id;
private String name;
private int age;
private double salary;
public User(){
id=0;
}
public User(long id, String name, int age,
double salary){
this.id = id;
this.name = name;
this.age = age;
this.salary = salary;
}
// setters, getters
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (int) (id ^
(id >>> 32));
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (id != other.id)
return false;
return true;
}
@Override
public String toString() {
return "User [id=" + id +
", name=" + name + ", age=" + age
+ ", salary=" +
salary + "]";
}
}
Configuration class: HelloWorldConfiguration.java
package
com.websystique.springmvc.configuration;
import
org.springframework.context.annotation.ComponentScan;
import
org.springframework.context.annotation.Configuration;
import
org.springframework.web.servlet.config.annotation.EnableWebMvc;
@Configuration
@EnableWebMvc
@ComponentScan(basePackages
= "com.websystique.springmvc")
public
class HelloWorldConfiguration {
}
Initialization
Class
package
com.websystique.springmvc.configuration;
import
org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public
class HelloWorldInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class[]
getRootConfigClasses() {
return new Class[] {
HelloWorldConfiguration.class };
}
@Override
protected Class[]
getServletConfigClasses() {
return null;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
Adding
CORS support to your REST API
While
accessing the REST API, you might face issues concerning Same Origin Policy.
Errors
like :
” No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://127.0.0.1:8080′ is therefore not allowed access.” OR
” XMLHttpRequest cannot load http://abc.com/bla. Origin http://localhost:12345 is not allowed by Access-Control-Allow-Origin.” are common in such case.
” No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://127.0.0.1:8080′ is therefore not allowed access.” OR
” XMLHttpRequest cannot load http://abc.com/bla. Origin http://localhost:12345 is not allowed by Access-Control-Allow-Origin.” are common in such case.
Solution
is Cross-Origin
Resource Sharing. Basically, on server side, we can return additional CORS
access control headers with response, which will eventually allow further
inter-domain communication.
With
Spring, we can write a simple filter which adds those CORS specific headers in
each response.
package
com.websystique.springmvc.configuration;
import
java.io.IOException;
import
javax.servlet.Filter;
import
javax.servlet.FilterChain;
import
javax.servlet.FilterConfig;
import
javax.servlet.ServletException;
import
javax.servlet.ServletRequest;
import
javax.servlet.ServletResponse;
import
javax.servlet.http.HttpServletResponse;
public
class CORSFilter implements Filter {
public void doFilter(ServletRequest req,
ServletResponse res, FilterChain chain) throws IOException, ServletException {
System.out.println("Filtering
on...........................................................");
HttpServletResponse response =
(HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin",
"*");
response.setHeader("Access-Control-Allow-Methods",
"POST, GET, PUT, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age",
"3600");
response.setHeader("Access-Control-Allow-Headers",
"x-requested-with");
chain.doFilter(req, res);
}
public void init(FilterConfig filterConfig)
{}
public void destroy() {}
}
Then
we can simply configure it in our Spring configuration like shown below:
package
com.websystique.springmvc.configuration;
import
javax.servlet.Filter;
import
org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public
class HelloWorldInitializer extends AbstractAnnotationConfigDispatcherServletInitializer
{
@Override
protected Class[]
getRootConfigClasses() {
return new Class[] {
HelloWorldConfiguration.class };
}
@Override
protected Class[]
getServletConfigClasses() {
return null;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
@Override
protected Filter[] getServletFilters() {
Filter [] singleton = { new
CORSFilter()};
return singleton;
}
}
That’s
it. With these two additional steps, clients will be able to communicate with
your REST API without worrying about Cross domain issues.
Deploy
and Test this API, let’s dig deeper into how this thing works
At
the at end of day, it’s just a plain controller class, part of a deploy-able
application.[Complete downloadable application code is shown further down in
post which you can deploy straight-away in your container]. I am going to
deploy it, in order to see things live and discuss each operation in detail.
Deployed Application is available at http://localhost:8080/Spring4MVCCRUDRestService.
To
test this API, i will use an external client POSTMAN (An extension from
CHROME). We will write our own client in just few minutes.
JSON Response:
You
might be wondering how the response is sent as JSON string, and the
Content-Type header in response confirms that. Glad you asked. This is due to
the fact that we have included Jackson library in our project.
Since
spring finds this library in class path, it invokes inbuilt MappingJackson2HttpMessageConverter converter
to convert the response (List of objects) into JSON.
No comments:
Post a Comment