Migrating Spring MVC RESTful web services to Spring 4
mongoDB, Spring, Spring MVC ·Spring 4 brings several improvements for MVC applications. In this post I will focus on restful web services and try these improvements by performing a migration from Spring MVC 3.2 to Spring 4.0. We will take a project implemented with Spring 3.2 and perform the steps to upgrade it to Spring 4.0. The following points sum up the content of this post:
- Migration from Spring 3.2 to Spring 4.0
- Changes in @ResponseBody and inclusion of @RestController
- Synchronous and Asynchronous calls
The source code of the following projects can be found at github:
1 The Spring 3.2 RESTful sample
The starting project is implemented with Spring 3.2 (pom.xml) . It consists in a Spring MVC application that access a database to retrieve data about TV series. Let’s have a look at its REST API to see it clearer:
Spring configuration
root-context.xml
<import resource="db-context.xml"/> <!-- Detects annotations like @Component, @Service, @Controller, @Repository, @Configuration --> <context:component-scan base-package="xpadro.spring.web.controller,xpadro.spring.web.service"/> <!-- Detects MVC annotations like @RequestMapping --> <mvc:annotation-driven/>
db-context.xml
<!-- Registers a mongo instance --> <bean id="mongo" class="org.springframework.data.mongodb.core.MongoFactoryBean"> <property name="host" value="localhost" /> </bean> <bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate"> <constructor-arg name="mongo" ref="mongo" /> <constructor-arg name="databaseName" value="rest-db" /> </bean>
Service implementation
This class is responsible of retrieving the data from a mongoDB database:
@Service public class SeriesServiceImpl implements SeriesService { @Autowired private MongoOperations mongoOps; @Override public Series[] getAllSeries() { List<Series> seriesList = mongoOps.findAll(Series.class); return seriesList.toArray(new Series[0]); } @Override public Series getSeries(long id) { return mongoOps.findById(id, Series.class); } @Override public void insertSeries(Series series) { mongoOps.insert(series); } @Override public void deleteSeries(long id) { Query query = new Query(); Criteria criteria = new Criteria("_id").is(id); query.addCriteria(criteria); mongoOps.remove(query, Series.class); } }
Controller implementation
This controller will handle requests and interact with the service in order to retrieve series data:
@Controller @RequestMapping(value="/series") public class SeriesController { private SeriesService seriesService; @Autowired public SeriesController(SeriesService seriesService) { this.seriesService = seriesService; } @RequestMapping(method=RequestMethod.GET) @ResponseBody public Series[] getAllSeries() { return seriesService.getAllSeries(); } @RequestMapping(value="/{seriesId}", method=RequestMethod.GET) public ResponseEntity<Series> getSeries(@PathVariable("seriesId") long id) { Series series = seriesService.getSeries(id); if (series == null) { return new ResponseEntity<Series>(HttpStatus.NOT_FOUND); } return new ResponseEntity<Series>(series, HttpStatus.OK); } @RequestMapping(method=RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) public void insertSeries(@RequestBody Series series, HttpServletRequest request, HttpServletResponse response) { seriesService.insertSeries(series); response.setHeader("Location", request.getRequestURL().append("/").append(series.getId()).toString()); } @RequestMapping(value="/{seriesId}", method=RequestMethod.DELETE) @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteSeries(@PathVariable("seriesId") long id) { seriesService.deleteSeries(id); } }
Integration testing
These integration tests will test our controller within a mock Spring MVC environment. In this way, we will be able to also test the mappings of our handler methods. For this purpose, the MockMvc class becomes very useful. If you want to learn how to write tests of Spring MVC controllers I highly recommend the Spring MVC Test Tutorial series by Petri Kainulainen.
@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(locations={ "classpath:xpadro/spring/web/test/configuration/test-root-context.xml", "classpath:xpadro/spring/web/configuration/app-context.xml"}) public class SeriesIntegrationTest { private static final String BASE_URI = "/series"; private MockMvc mockMvc; @Autowired private WebApplicationContext webApplicationContext; @Autowired private SeriesService seriesService; @Before public void setUp() { reset(seriesService); mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); when(seriesService.getAllSeries()).thenReturn(new Series[]{ new Series(1, "The walking dead", "USA", "Thriller"), new Series(2, "Homeland", "USA", "Drama")}); when(seriesService.getSeries(1L)).thenReturn(new Series(1, "Fringe", "USA", "Thriller")); } @Test public void getAllSeries() throws Exception { mockMvc.perform(get(BASE_URI) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().contentType("application/json;charset=UTF-8")) .andExpect(jsonPath("$", hasSize(2))) .andExpect(jsonPath("$[0].id", is(1))) .andExpect(jsonPath("$[0].name", is("The walking dead"))) .andExpect(jsonPath("$[0].country", is("USA"))) .andExpect(jsonPath("$[0].genre", is("Thriller"))) .andExpect(jsonPath("$[1].id", is(2))) .andExpect(jsonPath("$[1].name", is("Homeland"))) .andExpect(jsonPath("$[1].country", is("USA"))) .andExpect(jsonPath("$[1].genre", is("Drama"))); verify(seriesService, times(1)).getAllSeries(); verifyZeroInteractions(seriesService); } @Test public void getJsonSeries() throws Exception { mockMvc.perform(get(BASE_URI + "/{seriesId}", 1L) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().contentType("application/json;charset=UTF-8")) .andExpect(jsonPath("$.id", is(1))) .andExpect(jsonPath("$.name", is("Fringe"))) .andExpect(jsonPath("$.country", is("USA"))) .andExpect(jsonPath("$.genre", is("Thriller"))); verify(seriesService, times(1)).getSeries(1L); verifyZeroInteractions(seriesService); } @Test public void getXmlSeries() throws Exception { mockMvc.perform(get(BASE_URI + "/{seriesId}", 1L) .accept(MediaType.APPLICATION_XML)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_XML)) .andExpect(xpath("/series/id").string("1")) .andExpect(xpath("/series/name").string("Fringe")) .andExpect(xpath("/series/country").string("USA")) .andExpect(xpath("/series/genre").string("Thriller")); verify(seriesService, times(1)).getSeries(1L); verifyZeroInteractions(seriesService); } }
I’m showing some of the tests implemented. Check SeriesIntegrationTesting for complete implementation.
Functional testing
The application contains some functional testing by using the RestTemplate class. You need the webapp deployed in order to test this.
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations={ "classpath:xpadro/spring/web/configuration/root-context.xml", "classpath:xpadro/spring/web/configuration/app-context.xml"}) public class SeriesFunctionalTesting { private static final String BASE_URI = "http://localhost:8080/spring-rest-api-v32/spring/series"; private RestTemplate restTemplate = new RestTemplate(); @Autowired private MongoOperations mongoOps; @Before public void setup() { List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>(); converters.add(new StringHttpMessageConverter()); converters.add(new Jaxb2RootElementHttpMessageConverter()); converters.add(new MappingJacksonHttpMessageConverter()); restTemplate.setMessageConverters(converters); initializeDatabase(); } private void initializeDatabase() { try { mongoOps.dropCollection("series"); mongoOps.insert(new Series(1, "The walking dead", "USA", "Thriller")); mongoOps.insert(new Series(2, "Homeland", "USA", "Drama")); } catch (DataAccessResourceFailureException e) { fail("MongoDB instance is not running"); } } @Test public void getAllSeries() { Series[] series = restTemplate.getForObject(BASE_URI, Series[].class); assertNotNull(series); assertEquals(2, series.length); assertEquals(1L, series[0].getId()); assertEquals("The walking dead", series[0].getName()); assertEquals("USA", series[0].getCountry()); assertEquals("Thriller", series[0].getGenre()); assertEquals(2L, series[1].getId()); assertEquals("Homeland", series[1].getName()); assertEquals("USA", series[1].getCountry()); assertEquals("Drama", series[1].getGenre()); } @Test public void getJsonSeries() { List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>(); converters.add(new MappingJacksonHttpMessageConverter()); restTemplate.setMessageConverters(converters); String uri = BASE_URI + "/{seriesId}"; ResponseEntity<Series> seriesEntity = restTemplate.getForEntity(uri, Series.class, 1l); assertNotNull(seriesEntity.getBody()); assertEquals(1l, seriesEntity.getBody().getId()); assertEquals("The walking dead", seriesEntity.getBody().getName()); assertEquals("USA", seriesEntity.getBody().getCountry()); assertEquals("Thriller", seriesEntity.getBody().getGenre()); assertEquals(MediaType.parseMediaType("application/json;charset=UTF-8"), seriesEntity.getHeaders().getContentType()); } @Test public void getXmlSeries() { String uri = BASE_URI + "/{seriesId}"; ResponseEntity<Series> seriesEntity = restTemplate.getForEntity(uri, Series.class, 1L); assertNotNull(seriesEntity.getBody()); assertEquals(1l, seriesEntity.getBody().getId()); assertEquals("The walking dead", seriesEntity.getBody().getName()); assertEquals("USA", seriesEntity.getBody().getCountry()); assertEquals("Thriller", seriesEntity.getBody().getGenre()); assertEquals(MediaType.APPLICATION_XML, seriesEntity.getHeaders().getContentType()); } }
That’s all, the web application is tested and running. Now is time to migrate to Spring 4.
2 Migrating to Spring 4
Check this page to read information about migrating from earlier versions of the Spring framework
2.1 Changing maven dependencies
This section explains which dependencies should be modified. You can take a look at the complete pom.xml here.
The first step is to change Spring dependencies version from 3.2.3.RELEASE to 4.0.0.RELEASE:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.0.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.0.0.RELEASE</version> </dependency>
The next step is to update to Servlet 3.0 specification. This step is important since some of the Spring features are based on Servlet 3.0 and won’t be available. In fact, trying to execute SeriesIntegrationTesting will result in a ClassNotFoundException due to this reason, which is also explained this Spring JIRA ticket.
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency>
2.2 Updating of Spring namespace
Don’t forget to change the namespace of your spring configuration files:
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
Review the information page linked in section 2 since there are some changes regarding mvc namespace.
2.3 Deprecation of jackson libraries
If you check SeriesFunctionalTesting (setup method) again you will notice that the Jackson converter is now deprecated. If you try to run the test it will throw a NoSuchMethodError due to method change in Jackson libraries:
java.lang.NoSuchMethodError: org.codehaus.jackson.map.ObjectMapper.getTypeFactory()Lorg/codehaus/jackson/map/type/TypeFactory
In Spring 4, support to Jackson 1.x has been deprecated in favor of Jackson v2. Let’s change the old dependency:
<dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-mapper-asl</artifactId> <version>1.4.2</version> </dependency>
For these:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.3.0</version> </dependency>
Finally, if you are explicitly registering message converters you will need to change the deprecated class for the new version:
//converters.add(new MappingJacksonHttpMessageConverter()); converters.add(new MappingJackson2HttpMessageConverter());
2.4 Migration complete
The migration is done. Now you can run the application and execute its tests. The next section will review some of the improvements I mentioned at the beginning of this post.
3 Spring 4 Web improvements
3.1 @ResponseBody and @RestController
If your REST API serves content in JSON or XML format, some of the API methods (annotated with @RequestMapping) will have its return type annotated with @ResponseBody. With this annotation present, the return type will be included into the response body. In Spring 4 we can simplify this in two ways:
Annotate the controller with @ResponseBody
This annotation can now be added on type level. In this way, the annotation is inherited and we are not forced to put this annotation in every method.
@Controller @ResponseBody public class SeriesController {
Annotate the controller with @RestController
@RestController public class SeriesController {
This annotation simplifies the controller even more. If we check this annotation we will see that it is itself annotated with @Controller and @ResponseBody:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Controller @ResponseBody public @interface RestController {
Including this annotation won’t affect methods annotated with @ResponseEntity. The handler adapter looks up into a list of return value handlers in order to resolve who is capable of handling the response. The handler responsible of handling the ResponseEntity return type is asked before the ResponseBody type, so it will be used if ResponseEntity annotation is present at the method.
3.2 Asynchronous calls
Using the utility class RestTemplate for calling a RESTful service will block the thread until it receives a response. Spring 4 includes AsyncRestTemplate in order to execute asynchronous calls. Now you can make the call, continue doing other calculations and retrieve the response later.
@Test public void getAllSeriesAsync() throws InterruptedException, ExecutionException { logger.info("Calling async /series"); Future<ResponseEntity<Series[]>> futureEntity = asyncRestTemplate.getForEntity(BASE_URI, Series[].class); logger.info("Doing other async stuff..."); logger.info("Blocking to receive response..."); ResponseEntity<Series[]> entity = futureEntity.get(); logger.info("Response received"); Series[] series = entity.getBody(); assertNotNull(series); assertEquals(2, series.length); assertEquals(1L, series[0].getId()); assertEquals("The walking dead", series[0].getName()); assertEquals("USA", series[0].getCountry()); assertEquals("Thriller", series[0].getGenre()); assertEquals(2L, series[1].getId()); assertEquals("Homeland", series[1].getName()); assertEquals("USA", series[1].getCountry()); assertEquals("Drama", series[1].getGenre()); }
Asynchronous calls with callback
Although the previous example makes an asynchronous call, the thread will block if we try to retrieve the response with futureEntity.get() if the response hasn’t already been sent.
AsyncRestTemplate returns ListenableFuture, which extends Future and allows us to register a callback. The following example makes an asynchronous call and keeps going with its own tasks. When the service returns a response, it will be handled by the callback:
@Test public void getAllSeriesAsyncCallable() throws InterruptedException, ExecutionException { logger.info("Calling async callable /series"); ListenableFuture<ResponseEntity<Series[]>> futureEntity = asyncRestTemplate.getForEntity(BASE_URI, Series[].class); futureEntity.addCallback(new ListenableFutureCallback<ResponseEntity<Series[]>>() { @Override public void onSuccess(ResponseEntity<Series[]> entity) { logger.info("Response received (async callable)"); Series[] series = entity.getBody(); validateList(series); } @Override public void onFailure(Throwable t) { fail(); } }); logger.info("Doing other async callable stuff ..."); Thread.sleep(6000); //waits for the service to send the response }
4 Conclusion
We took a Spring 3.2.x web application and migrated it to the new release of Spring 4.0.0. We also reviewed some of the improvements that can be applied to a Spring 4 web application.
I’m publishing my new posts on Google plus and Twitter. Follow me if you want to be updated with new content.