Enterprise Java

Migrating Spring MVC RESTful web services to Spring 4

1   Introduction

Spring 4 brings several improvements for MVC applications. In this post I will focus on restful web services and try these improvements by taking a project implemented with Spring 3.2 and upgrading it to Spring 4. The following points sum up the content of this post:

The source code of the following projects can be found at github:

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

blog-rest-api

Spring configuration

<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.

3   Migrating to Spring 4

Check this page to read information about migrating from earlier versions of the Spring framework

3.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 here.

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
</dependency>

3.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.

3.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());

3.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.

4 Spring 4 Web improvements

4.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.

4.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
}

5 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.
 

Xavier Padro

Xavier is a software developer working in a consulting firm based in Barcelona. He is specialized in web application development with experience in both frontend and backend. He is interested in everything related to Java and the Spring framework.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Dhiren Hamal
Dhiren Hamal
8 years ago

Thank you very much. previously I had used Jacson with spring 3 however that works fine but when I tried with spring 4 the same version of jacson I got the “class ‘org.springframework.http.converter.json.mappingjackson2httpmessageconverter’ not found” error on my context.xml file. Now your blog really hepls me to resolve the problem.

Back to top button