Centralize validation and exception handling with @ControllerAdvice
Spring, Spring MVC ·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.
1 Adding validation and exception handling
@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); }
- 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.
@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.
@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()); } }