Create and test REST services with Spring MVC



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:


                http://localhost:8080/myApp/spring/warehouses/1



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());
}



 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: , , ,