Kim Rudolph

Spring MVC REST Example

The goal of this tutorial is to build a simple REST style web service. It is based on the previous hello world example. It should listen and respond to requests using JSON or XML as data formats.

Evolving from “Hello world!”, there are kittens involved in this tutorial, representing the resources.

Adding New Dependencies

To teach Spring MVC the formats JSON and XML, the mappers for those have to be added as dependencies to the pom.xml.

<!-- JSON Mapper -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.2.3</version>
</dependency>

<!-- XML Mapper -->
<dependency>
  <groupId>javax.xml.bind</groupId>
  <artifactId>jaxb-api</artifactId>
  <version>2.2.11</version>
</dependency>

Cats in a Basket

The service needs some beans to interact on. There are one or many Cats sitting in a Basket. Both classes go in the de.kimrudolph.tutorials.beans package.

The @XmlRootElement annotation describes how the xml mapper should name the corresponding entity. The basket has the function to wrap a simple list of cats. @XmlElement defines the mapping of the list entry descriptor, which the XML mapper can not resolve on its own. The result will be something like <cats><cat></cat></cats>.

package de.kimrudolph.tutorials.beans;

import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlRootElement;

import org.codehaus.jackson.annotate.JsonProperty;

/**
 * Container holding {@link Cat}s.
 */
@XmlRootElement(name = "basket")
public class Basket {

    private List<Cat> cats = new ArrayList<Cat>();

    @XmlElement(name = "cat", type = Cat.class)
    public List<Cat> getCats() {
        return cats;
    }

    public void add(Cat cat) {
        this.cats.add(cat);
    }

    public void remove(String name) {
        this.cats.remove(get(name));
    }

    public Cat get(String name) {
        for (Cat cat : cats) {
            if (cat.getName().equals(name)) {
                return cat;
            }
        }
        return null;
    }

    public void setCats(List<Cat> cats) {
        this.cats = cats;
    }
}
package de.kimrudolph.tutorials.beans;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "cat")
public class Cat {

    private String name;
    private Integer cuteness;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getCuteness() {
        return cuteness;
    }

    public void setCuteness(Integer cuteness) {
        this.cuteness = cuteness;
    }

}

CRUD Controller

Create, read, update and delete are the methods to interact with resources, in this case: cats.

It is a little harsh to CRUD a cat, instead think of

Using the REST style, the CRUD actions are represented in the following grid:

HTTP Verb Routing Action
GET /cats Return all cats
GET /cats/kitty Return the cat named kitty
POST /cats Create a cat
PUT /cats/kitty Update the cat named kitty
DELETE /cats/kitty Delete the cat named kitty

The initial basket is prefilled with the two cats Kitty and Mr. Cuddles. No persistence provider and no validations are used in this example.

package de.kimrudolph.tutorials.controllers;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import de.kimrudolph.tutorials.beans.Basket;
import de.kimrudolph.tutorials.beans.Cat;

@Controller
@RequestMapping(
    value = "/cats",
    headers = { "Accept=application/xml,application/json" })
public class CatsController {

    private final Basket basket;

    /**
     * Prefill the {@link Basket} with 2 cute {@link Cat}s.
     */
    public CatsController() {

        basket = new Basket();

        final Cat kitty = new Cat();
        kitty.setName("kitty");
        kitty.setCuteness(11);
        basket.add(kitty);

        final Cat cuddles = new Cat();
        cuddles.setName("cuddles");
        cuddles.setCuteness(5);
        basket.add(cuddles);
    }

    @RequestMapping(method = RequestMethod.GET)
    @ResponseBody
    public Basket getAll() {

        return basket;
    }

    @RequestMapping(value = "{name}", method = RequestMethod.GET)
    @ResponseBody
    public Cat get(@PathVariable final String name) {

        return basket.get(name);
    }

    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    @ResponseBody
    public Cat create(@RequestBody final Cat cat) {

        basket.add(cat);
        return cat;
    }

    @RequestMapping(method = RequestMethod.PUT)
    @ResponseStatus(HttpStatus.OK)
    public void update(@RequestBody final Cat cat) {

        basket.get(cat.getName()).setCuteness(cat.getCuteness());
    }

    @RequestMapping(value = "/{name}", method = RequestMethod.DELETE)
    @ResponseStatus(HttpStatus.OK)
    public void delete(@PathVariable final String name) {

        basket.remove(name);
    }
}

Testing the Controller

The spring-test project provides an easy to use interface, including

package de.kimrudolph.tutorials.controllers;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Before;
import org.junit.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

public class CatsControllerTest {

    private MockMvc service;

    @Before
    public void setup() {

        service = MockMvcBuilders.standaloneSetup(new CatsController())
            .build();
    }

    @Test
    public void testGetCatJsonRequest() throws Exception {

        service
            .perform(get("/cats/kitty.json"))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(content().string("{\"name\":\"kitty\",\"cuteness\":11}"));
    }

    @Test
    public void testGetAllCatJsonRequest() throws Exception {

        service
            .perform(get("/cats.json"))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(
                content().string(
                    "{\"cats\":[{\"name\":\"kitty\",\"cuteness\":11},"
                        + "{\"name\":\"cuddles\",\"cuteness\":5}]}"));
    }

    @Test
    public void testCreateCatJsonRequest() throws Exception {

        service
            .perform(
                post("/cats").contentType(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON)
                    .content("{\"name\":\"grumpy\",\"cuteness\":11}"))
            .andExpect(status().isCreated())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(
                content().string("{\"name\":\"grumpy\",\"cuteness\":11}"));
    }

    @Test
    public void testUpdateCatJsonRequest() throws Exception {

        service.perform(
            put("/cats").contentType(MediaType.APPLICATION_JSON).content(
                "{\"name\":\"kitty\",\"cuteness\":10}")).andExpect(
            status().isOk());
    }

    @Test
    public void testDeleteCatJsonRequest() throws Exception {

        service.perform(
            delete("/cats/kitty").contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk());
    }

    @Test
    public void testGetCatXmlRequest() throws Exception {

        service
            .perform(get("/cats/kitty.xml"))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_XML))
            .andExpect(
                content().string(
                    "<?xml version=\"1.0\" encoding=\"UTF-8\" "
                        + "standalone=\"yes\"?><cat><cuteness>11"
                        + "</cuteness><name>kitty</name></cat>"));
    }

    @Test
    public void testGetAllCatXmlRequest() throws Exception {

        service
            .perform(get("/cats.xml"))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_XML))
            .andExpect(
                content()
                    .string(
                        "<?xml version=\"1.0\" encoding=\"UTF-8\" "
                            + "standalone=\"yes\"?><basket>"
                            + "<cat><cuteness>11</cuteness><name>kitty</name></cat>"
                            + "<cat><cuteness>5</cuteness><name>cuddles</name></cat>"
                            + "</basket>"));
    }

    @Test
    public void testCreateCatXmlRequest() throws Exception {

        service
            .perform(
                post("/cats")
                    .contentType(MediaType.APPLICATION_XML)
                    .accept(MediaType.APPLICATION_XML)
                    .content(
                        "<cat><cuteness>10</cuteness><name>grumpy</name></cat>"))
            .andExpect(status().isCreated())
            .andExpect(content().contentType(MediaType.APPLICATION_XML))
            .andExpect(
                content()
                    .string(
                        "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
                            + "<cat><cuteness>10</cuteness><name>grumpy</name></cat>"));
    }

    @Test
    public void testUpdateCatXmlRequest() throws Exception {

        service.perform(
            put("/cats").contentType(MediaType.APPLICATION_XML).content(
                "<cat><cuteness>10</cuteness><name>kitty</name></cat>"))
            .andExpect(status().isOk());
    }

    @Test
    public void testDeleteCatXmlRequest() throws Exception {

        service.perform(
            delete("/cats/kitty").contentType(MediaType.APPLICATION_XML))
            .andExpect(status().isOk());
    }
}

Testing via Command-line

First, for consistency reasons, change the application path from hello-world to basket in the pom.xml.

<!-- Tomcat 7 Plugin for development environment -->
<plugin>
  <groupId>com.googlecode.t7mp</groupId>
  <artifactId>maven-t7-plugin</artifactId>
  <version>0.9.13</version>
  <configuration>
    <!-- configuration of the path on 
    localhost http://localhost:8080/<path> -->
    <contextPath>basket</contextPath>
  </configuration>
</plugin>

Start the server so that the service will respond to requests pointing to http://localhost:8080/basket.

$ mvn package t7:run

There is a command-line tool cUrl which suits the need for easy testing. No need for setting up a test client.

There are two ways to tell the server the prefered or expected content type. First possibility is the type suffix, eg. .json or .xml. Another option is to specify in the request header the way how to expect answers (Accept) or which format the request has (Content-type).

Get all cats as JSON

$ curl --header "Accept: application/json" localhost:8080/basket/cats
$ curl localhost:8080/basket/cats.json
$ {"cats":[{"name":"kitty","cuteness":11},{"name":"cuddles","cuteness":5}]}

Get all cats as XML

$ curl --header "Accept: application/xml" localhost:8080/basket/cats
$ curl localhost:8080/basket/cats.xml
$ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><basket><cat><cuteness>11</cuteness><name>kitty</name></cat><cat><cuteness>5</cuteness><name>cuddles</name></cat></basket>

Get a single cat as JSON

$ curl -1 localhost:8080/basket/cats/kitty.json
$ curl -i --header "Accept: application/json" localhost:8080/basket/cats/kitty
$ HTTP/1.1 200 OK
$ {"name":"kitty","cuteness":11}

Get a single cat as XML

$ curl -i localhost:8080/basket/cats/kitty.xml
$ curl -i --header "Accept: application/xml" localhost:8080/basket/cats/kitty
$ HTTP/1.1 200 OK
$ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><cat><cuteness>11</cuteness><name>kitty</name></cat>

Create a cat via JSON. There needs to be an Accept header to tell the service, how the response body should be formatted.

$ curl -i \
  --header "Content-type: application/json" \
  --header "Accept: application/json" \
  -X POST localhost:8080/basket/cats \
  --data '{"name":"grumpy","cuteness":10}'
$ HTTP/1.1 201 Created
$ {"name":"grumpy","cuteness":10}

Create a cat via XML

$ curl -i \
  --header "Content-type: application/xml" \
  --header "Accept: application/xml" \
  -X POST localhost:8080/basket/cats \
  --data  '<cat><cuteness>10</cuteness><name>grumpy</name></cat>'
$ HTTP/1.1 201 Created
$ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><cat><cuteness>10</cuteness><name>grumpy</name></cat>

Update a cat via JSON

$ curl -i \
  --header "Content-type: application/json" \
  -X PUT localhost:8080/basket/cats \
  --data '{"name":"grumpy","cuteness":12}'
$ HTTP/1.1 200 OK

Update a cat via XML

$ curl -i \
  --header "Content-type: application/xml" \
  -X PUT localhost:8080/basket/cats \
  --data '<cat><name>grumpy</name><cuteness>12</cuteness></cat>'
$ HTTP/1.1 200 OK

Delete a cat via JSON or XML

$ curl -i -X DELETE localhost:8080/basket/cats/grumpy
$ HTTP/1.1 200 OK

Exception Handling

But there is no check if there actually is a cat on the GET, UPDATE and DELETE actions. There should be a HTTP/1.1 404 Not Found response to tell the user that there was an error finding the cat. To accomplish that, there has to be a name check and an exception to be thrown if the requested cat is not in the basket.

The following exception class will be thrown in that case. It goes in the de.kimrudolph.tutorials.exceptions package.

package de.kimrudolph.tutorials.exceptions;

public class CatNotFoundException extends RuntimeException {

    private static final long serialVersionUID = 5968241099616074887L;

    public CatNotFoundException(final String name) {

        super(name);
    }
}

Every get() method call in the Basket throws an CatNotFoundException if there is no cat found with the given name.

...
public Cat get(final String name) {

    for (final Cat cat : cats) {
        if (cat.getName().equals(name)) {
            return cat;
        }
    }
    throw new CatNotFoundException(name);
}
...

That exception can be thrown anywhere in a request execution. It should be catched and translated at a central location. The @ControllerAdvice annotation is picked up like a @Controller annotation and is therefore known to the context via the already defined @ComponentScan. In this case, only the specific CatNotFoundException should be catched and the response body modified.

package de.kimrudolph.tutorials.exceptions;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value = { CatNotFoundException.class })
    protected ResponseEntity<Object> handleMissingCat(
        final RuntimeException exception, final WebRequest request) {

        final String bodyOfResponse = "Cat '" + exception.getMessage()
            + "' has not been found. Maybe you misspelled the name?";

        final HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.TEXT_PLAIN);

        return handleExceptionInternal(exception, bodyOfResponse, headers,
            HttpStatus.NOT_FOUND, request);
    }

}

As the ResponseEntityExceptionHandler runs in the application context, an integration test is needed. To create a context for that, a simple @Configuration driven context has to be created in the de.kimrudolph.tutorials.configuration package.

package de.kimrudolph.tutorials.configuration;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = { "de.kimrudolph.tutorials" })
public class TestApplicationContext {

}

The actual test is located in the de.kimrudolph.tutorials.exceptions package and tests the full stack for exception handling.

package de.kimrudolph.tutorials.exceptions;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import de.kimrudolph.tutorials.configuration.TestApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = { TestApplicationContext.class })
public class ExceptionIntegrationTest {

    private MockMvc service;

    @Autowired
    private WebApplicationContext context;

    @Before
    public void setup() {

        service = MockMvcBuilders.webAppContextSetup(context).build();
    }

    @Test
    public void testGetCatNotFoundRequest() throws Exception {

        service
            .perform(get("/cats/wuff").contentType(MediaType.APPLICATION_XML))
            .andExpect(status().isNotFound())
            .andExpect(content().contentType(MediaType.TEXT_PLAIN))
            .andExpect(
                content().string(
                    "Cat 'wuff' has not been found. "
                        + "Maybe you misspelled the name?"));

        service
            .perform(get("/cats/wuff").contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isNotFound())
            .andExpect(content().contentType(MediaType.TEXT_PLAIN))
            .andExpect(
                content().string(
                    "Cat 'wuff' has not been found. "
                        + "Maybe you misspelled the name?"));
    }
}

Now any request gets a respond with the correct return code if the cat is not found.

$ curl -i -X DELETE localhost:8080/basket/cats/wuff
$ HTTP/1.1 404 Not Found
$ Cat 'wuff' has not been found. Maybe you misspelled the name?

The Result

A working example of a REST service based on Spring MVC and speaking JSON and XML.

The full application can be found at the spring-mvc-rest repository.