Tuesday, March 5, 2013

Centralize validation and exception handling with @ControllerAdvice



1 Introduction

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:
  • @ExceptionHandler: Handles exceptions thrown by handler methods.
  • @InitBinder: Initializes the WebDataBinder, which will be used to populate objects passed as arguments to the handler methods. Usually, it is used to register property editors or validators.
  • @ModelAttribute: Binds a parameter or return value to an attribute, which will then be exposed to a web view.

Source code can be found at github.


2 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 void initBinder(WebDataBinder binder) {
binder.setValidator(new PersonValidator());
}
      
@ExceptionHandler({MethodArgumentNotValidException.class})
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException pe) {
return new ResponseEntity<String>(pe.getMessage(), HttpStatus.BAD_REQUEST);
}


Besides the handler method, this controller has the following methods:
  • initBinder: Registers a validator to prevent that a person with invalid data is introduced. To make the validator validate the person object passed as a parameter, it is necessary to add the @Valid annotation to the argument. Spring 3 fully supports JSR-303 bean validation API, but it does not implement it. The reference implementation which is used in this example is Hibernate Validator 4.x.
  • handleValidationException: Handles the MethodArgumentNotValidException that can be thrown by the handler method. This exception is thrown by Spring MVC when an argument annotated with @Valid, fails its validation.


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})
public ResponseEntity<String> handlePersonNotFound(PersonNotFoundException pe) {
return new ResponseEntity<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 new ResponseEntity<String>(pe.getMessage(), HttpStatus.NOT_FOUND);
}
      
@ExceptionHandler({Exception.class})
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException pe) {
return new ResponseEntity<String>(pe.getMessage(), HttpStatus.BAD_REQUEST);
}
 
We are repeating code, since @ExceptionHandler is not global.


3 Centralizing code

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

@ControllerAdvice
public class CentralControllerHandler {
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.setValidator(new PersonValidator());
}
      
@ExceptionHandler({PersonNotFoundException.class})
public ResponseEntity<String> handlePersonNotFound(PersonNotFoundException pe) {
return new ResponseEntity<String>(pe.getMessage(), HttpStatus.NOT_FOUND);
}
      
@ExceptionHandler({MethodArgumentNotValidException.class})
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException pe) {
return new ResponseEntity<String>(pe.getMessage(), HttpStatus.BAD_REQUEST);
}
}

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

 

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

13 comments:

  1. When I put the @ExceptionHandler in the controller class, it works fine but when I am removing it and putting in the class annotated with @ControllerAdvice..it does not reach in this exception handler method. Do I have to do anything specific to load Controller Advice class other than component scan config which I already have.

    ReplyDelete
    Replies
    1. make sure you have in your dispath-servlet.

      Delete
  2. You don't need to do anything else. Are you sure that the package of the @ControllerAdvice annotated class is located within the component scan path? Also check if the exception captured by the exception handler is the same as the one being thrown by the @RequestMapping method.

    ReplyDelete
  3. Xavier, can you post the XML configuration that shows the component-scan configuration ?

    ReplyDelete
  4. Hi,

    You can check the source code at the link provided in the first section, but here's a direct link to the file you asked:

    https://github.com/xpadro/spring-rest/blob/master/rest-controlleradvice/src/main/resources/xpadro/spring/rest/configuration/root-context.xml


    Regards

    ReplyDelete
  5. Prou interessant seria fer ús de validation groups, validació de camps més complexos (amb propis validadors) juntament amb anotacions de validació :)

    ReplyDelete
  6. Estic d'acord amb tu, potser en un altre post ;)

    Gràcies pel comentari!

    ReplyDelete
  7. What is the purpose/responsibility of PersonValidator here... i couldn't find that class also....

    ReplyDelete
    Replies
    1. Hi Manish,

      The purpose of PersonValidator is to prevent an invalid person to be introduced. Check initBinder description in section 2 of this post. The class is not shown here because the validation implementation is not the central point of this post. However, if you want to check it, you can take a look at the source code provided at the beginning. Here's a direct link to the requested validator class:
      https://github.com/xpadro/spring-rest/tree/master/rest-controlleradvice/src/main/java/xpadro/spring/rest/validator

      Delete
  8. Nice and clean example. It seems with SpringMVC there are many different ways to achieve the same result. What is your opinion on returning a ResponseEntity from your getPerson method instead of @ResponseBody Person ?

    ReplyDelete
    Replies
    1. Hi Andre,

      Thanks for your feedback, I appreciate it. About your question, I'm returning a @ResponseBody because I just want access to the response body. If you see the test example at section 4, returning @ResponseBody plus using message converters allows us to receive a Person instance directly in our test class.

      I would use @ResponseEntity if I wanted more control and needed access to response headers in addition to the body. Check the following example:

      ResponseEntity entity = template.getForEntity("http://example.com", String.class);
      String body = entity.getBody();
      MediaType contentType = entity.getHeaders().getContentType();
      HttpStatus statusCode = entity.getStatusCode();

      The above example is taken from http://docs.spring.io/spring/docs/3.0.x/api/org/springframework/http/ResponseEntity.html

      Delete
  9. Lovely. Ive been wondering how to setup all my property editors in a single place. Your combination of @ControllerAdvice and @InitBinder did the trick

    ReplyDelete
    Replies
    1. Great Ashario, it's nice to know it helped you.

      Delete