Tuesday, February 5, 2013

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:

  • @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:

                http://localhost:8080/myApp/spring/warehouses/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());
          }
     }
}


No comments:

Post a Comment