Centralize validation and exception handling with @ControllerAdvice

The ControllerAdvice annotation introduced by Spring 3.2 allows us to handle several functionalities in a way that can be shared by all controllers (through its handler methods, annotated with @RequestMapping). This annotation is mainly used to define the following methods:

Source code can be found at github.

1 Adding validation and exception handling

The following is a description of  the controller’s handler methods before implementing the @ControllerAdvice.
Add person controller:
@RequestMapping(value="/persons", method=RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
public void addPerson(@Valid @RequestBody Person person, HttpServletRequest request, HttpServletResponse response) {
    personRepository.addPerson(person);
    logger.info("Person added: "+person.getId());
    response.setHeader("Location", request.getRequestURL().append("/").append(person.getId()).toString());
}

@InitBinder
public voidinitBinder(WebDataBinder binder) {
    binder.setValidator(new PersonValidator());
}

@ExceptionHandler({MethodArgumentNotValidException.class})
publicResponseEntity<String> handleValidationException(MethodArgumentNotValidException pe) {
    return new ResponseEntity<String>(pe.getMessage(), HttpStatus.BAD_REQUEST);
}

 

Besides the handler method, this controller has the following methods:
Get person controller:
@RequestMapping(value="/persons/{personId}", method=RequestMethod.GET)
public @ResponseBody Person getPerson(@PathVariable("personId") long id) {
    return personRepository.getPerson(id);
}

@ExceptionHandler({PersonNotFoundException.class})
publicResponseEntity<String> handlePersonNotFound(PersonNotFoundException pe) {
    return newResponseEntity<String>(pe.getMessage(), HttpStatus.NOT_FOUND);
}

 

This controller adds an exception handler for handling when a request asks to retrieve a person that does not exist.

Update person controller:
@RequestMapping(value="/persons", method=RequestMethod.PUT)
@ResponseStatus(HttpStatus.CREATED)
public void updatePerson(@Valid @RequestBody Person person, HttpServletRequest request, HttpServletResponse response) {
    personRepository.updatePerson(person);
    logger.info("Person updated: "+person.getId());
    response.setHeader("Location", request.getRequestURL().append("/").append(person.getId()).toString());
}

@InitBinder
public void initBinder(WebDataBinder binder) {
    binder.setValidator(new PersonValidator());
}

@ExceptionHandler({PersonNotFoundException.class})
public ResponseEntity<String> handlePersonNotFound(PersonNotFoundException pe) {
    return newResponseEntity<String>(pe.getMessage(), HttpStatus.NOT_FOUND);
}

@ExceptionHandler({Exception.class})
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException pe) {
    return newResponseEntity<String>(pe.getMessage(), HttpStatus.BAD_REQUEST);
}

 

We are repeating code, since @ExceptionHandler is not global.

2 Centralizing code

ControllerAdvice annotation is itself annotated with @Component. Hence, the class that we are implementing will be autodetected through classpath scanning.

@ControllerAdvice
public classCentralControllerHandler {
@InitBinder
public void initBinder(WebDataBinder binder) {
    binder.setValidator(new PersonValidator());
}

@ExceptionHandler({PersonNotFoundException.class})
public ResponseEntity<String> handlePersonNotFound(PersonNotFoundException pe) {
    return newResponseEntity<String>(pe.getMessage(), HttpStatus.NOT_FOUND);
}

@ExceptionHandler({MethodArgumentNotValidException.class})
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException pe) {
    return newResponseEntity<String>(pe.getMessage(), HttpStatus.BAD_REQUEST);
}
}

Finally, we can delete these methods from the controllers, taking rid of code duplication, since this class will handle exception handling and validation for all handler methods annotated with @RequestMapping.

 

3 Testing

The methods described below, test the retrieval of persons:

@Test
public void getExistingPerson() {
    String uri = "http://localhost:8081/rest-controlleradvice/spring/persons/{personId}";
    Person person = restTemplate.getForObject(uri, Person.class, 1l);
    assertNotNull(person);
    assertEquals("Xavi", person.getName());
}

@Test
public void getNonExistingPerson() {
    String uri = "http://localhost:8081/rest-controlleradvice/spring/persons/{personId}";
    try {
        restTemplate.getForObject(uri, Person.class, 5l);
        throw new AssertionError("Should have returned an 404 error code");
    } catch(HttpClientErrorException e) {
        assertEquals(HttpStatus.NOT_FOUND, e.getStatusCode());
    }
}

 

The rest of tests can be found with the source code linked above.