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.
Labels: MVC, REST, Spring