Introduction
The first part of this example shows how to create
RESTful web services using Spring MVC. The controller contains CRUD operations
on warehouses and its products. For this example, the repository is a stub that
simulates access to the database.
The second part will access these services using
the RestTemplate class and test them.
Source code available at github.
Configuration
The context configuration is quite simple. It is
split in two xml files. The parent context:
<?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:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
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">
<!-- Detects
annotations like @Component, @Service, @Controller...
-->
<context:component-scan base-package="xpadro.tutorial.rest"/>
<!-- Detects MVC
annotations like @RequestMapping -->
<mvc:annotation-driven/>
</beans>
And the servlet context, which contains the stub
repository:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<!-- The warehouse
repository. Simulates the retrieval of data from the database -->
<bean id="warehouseRepository" class="xpadro.tutorial.rest.repository.WarehouseRepositoryImpl"/>
</beans>
The web.xml file just contains basic Spring
configuration:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>SpringRestTest</display-name>
<!-- Root context
configuration -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:xpadro/tutorial/rest/configuration/root-context.xml</param-value>
</context-param>
<!-- Loads Spring
root context, which will be the parent context -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Spring servlet
-->
<servlet>
<servlet-name>springServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:xpadro/tutorial/rest/configuration/app-context.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springServlet</servlet-name>
<url-pattern>/spring/*</url-pattern>
</servlet-mapping>
</web-app>
And finally the pom.xml with all the dependencies,
which can be found here.
Creating the RESTful services
The controller has the following methods:
getWarehouse: Returns an existing warehouse.
@RequestMapping(value="/warehouses/{warehouseId}", method=RequestMethod.GET)
public @ResponseBody Warehouse getWarehouse(@PathVariable("warehouseId") int id) {
return warehouseRepository.getWarehouse(id);
}
This method uses several MVC annotations,
explained below:
- @RequestMapping: This annotation maps requests based
on method onto specific handlers, in this case, the getWarehouse method, but
only if the HTTP request method is GET. Specifying the method, you can have
multiple methods mapped to the same uri. For example, the following request
will be handled by this method and return the warehouse identified by 1:
- @PathVariable: Extract values from request URL. In
the method above, it extracts the warehouseId value from the request URL and
maps it to the id parameter.
- @ResponseBody: Bounds the return value of the
method to the response body. For this task it uses HTTP message converters. The
function of these converters is to convert between HTTP request/response and
object.
addProduct: Adds a new product to an existing
warehouse.
@RequestMapping(value="/warehouses/{warehouseId}/products", method=RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
public void addProduct(@PathVariable("warehouseId") int warehouseId, @RequestBody Product product, HttpServletRequest request,
HttpServletResponse response) {
warehouseRepository.addProduct(warehouseId, product);
response.setHeader("Location", request.getRequestURL().append("/")
.append(product.getId()).toString());
}
- With the @ResponseStatus annotation, we are
defining that there won’t be a view returned. Instead, we will return a
response with an empty body.
- Like @ResponseBody annotation, the @RequestBody
annotation uses converters to transform request data into the object passed as
a parameter.
Other methods are defined in this controller but
won’t put them all here. You can look up the source code linked above.
Setting the exception handler
You can have multiple exception handlers, each one
mapped to one or more exception types. Using the @ExceptionHandler annotation
allows you to handle exceptions raised by methods annotated with
@RequestMapping. Instead of forwarding to a view, it allows you to set a
response status code. For example:
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler({ProductNotFoundException.class})
public void
handleProductNotFound(ProductNotFoundException pe) {
logger.warn("Product not found. Code: "+pe.getMessage());
}
Testing the services
The test class is as follows:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={
"classpath:xpadro/tutorial/rest/configuration/root-context.xml",
"classpath:xpadro/tutorial/rest/configuration/app-context.xml"})
public class WarehouseTesting {
private static final int WAREHOUSE_ID = 1;
private static final int PRODUCT_ID = 4;
private RestTemplate restTemplate = new RestTemplate();
/**
*
Tests accessing to an existing warehouse
*/
@Test
public void getWarehouse() {
String uri = "http://localhost:8081/rest_test/spring/warehouses/{warehouseId}";
Warehouse warehouse = restTemplate.getForObject(uri, Warehouse.class, WAREHOUSE_ID);
assertNotNull(warehouse);
assertEquals("WAR_BCN_004", warehouse.getName());
}
/**
*
Tests the addition of a new product to an existing warehouse.
*/
@Test
public void addProduct() {
//Adds the new product
String uri = "http://localhost:8081/rest_test/spring/warehouses/{warehouseId}/products";
Product product = new Product(PRODUCT_ID, "PROD_999");
URI newProductLocation = restTemplate.postForLocation(uri, product, WAREHOUSE_ID);
//Checks we can access
to the created product
Product createdProduct = restTemplate.getForObject(newProductLocation, Product.class);
assertEquals(product,
createdProduct);
assertNotNull(createdProduct.getId());
}
/**
*
Tests the removal of an existing product
*/
@Test
public void removeProduct() {
String uri = "http://localhost:8081/rest_test/spring/warehouses/{warehouseId}/products/{productId}";
restTemplate.delete(uri, WAREHOUSE_ID, PRODUCT_ID);
try {
restTemplate.getForObject(uri, Product.class, WAREHOUSE_ID, PRODUCT_ID);
throw new AssertionError("Should have returned an 404 error
code");
} catch (HttpClientErrorException e) {
assertEquals(HttpStatus.NOT_FOUND, e.getStatusCode());
}
}
}
Labels: MVC, REST, Spring, Test