Some time ago, I wrote about how the error response of a Spring based REST API can be enhanced, in order to provide the clients with a better understanding of why a request error occurred. In the first post I explained how a server-side exception triggered by an incoming request can be translated into a generic ErrorMessage class, which in turn was serialized to the response body. In the second part, the solution was generalized, to prevent the @ExceptionHandler methods from being duplicated to all controllers. However, the release of Spring 3.2 last December provides a new feature which greatly simplifies the general solution.

@ControllerAdvice

The @ControllerAdvice is a new TYPE annotation that was added as part of the release. A class annotated with it will act as a global helper class for all controllers. In other words, any local, controller specific @ExceptionHandler method that is moved from the @Controller class to a class annotated with @ControllerAdvice, will be applicable for the entire application. Consequently, all the boiler plate code that was created in the generic solution in my last post can be removed. The revisited solution can be as simple as:

@ControllerAdvice
class GlobalControllerExceptionHandler {
    
    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    ErrorMessage handleException(SomeException ex) {
        ErrorMessage errorMessage = createErrorMessage(ex);
        return errorMessage;
    }

    @ExceptionHandler
    @ResponseStatus(HttpStatus.GONE)
    @ResponseBody
    ErrorMessage handleException(OtherException ex) {
        ErrorMessage errorMessage = createErrorMessage(ex);
        return errorMessage;
    }   
}

The previous posts contained error handling examples of some Spring MVC exceptions, namely the MethodArgumentNotValidException, the HttpMediaTypeNotSupportedException and the HttpMessageNotReadableException. A corresponding Spring 3.2 based implementation can be found in the GlobalControllerExceptionHandler, or continue reading for yet another implementation.

ResponseEntityExceptionHandler

The above example works well, but it can be hard to identify the Spring MVC specific exceptions to implement a common error response handling strategy for them. One way of overcoming this problem is to extend the ResponseEntityExceptionHandler class, which was also added to the Spring 3.2 release. Similarly to the DefaultHandlerExceptionResolver, it provides methods for handling the exceptions, but it allows the developer to specify ResponseEntitys as return values (as opposed to the ModelAndViews that are returned by the methods in the DefaultHandlerExceptionResolver). The implementation is still straight forward, create a class, annotate it with the @ControllerAdvice, extend the ResponseEntityExceptionHandler class and override the methods with the exception types that you are interested in:

@ControllerAdvice
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
        List<ObjectError> globalErrors = ex.getBindingResult().getGlobalErrors();
        List<String> errors = new ArrayList<>(fieldErrors.size() + globalErrors.size());
        String error;
        for (FieldError fieldError : fieldErrors) {
            error = fieldError.getField() + ", " + fieldError.getDefaultMessage();
            errors.add(error);
        }
        for (ObjectError objectError : globalErrors) {
            error = objectError.getObjectName() + ", " + objectError.getDefaultMessage();
            errors.add(error);
        }
        ErrorMessage errorMessage = new ErrorMessage(errors);
        return new ResponseEntity(errorMessage, headers, status);
    }

    @Override
    protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        String unsupported = "Unsupported content type: " + ex.getContentType();
        String supported = "Supported content types: " + MediaType.toString(ex.getSupportedMediaTypes());
        ErrorMessage errorMessage = new ErrorMessage(unsupported, supported);
        return new ResponseEntity(errorMessage, headers, status);
    }

    @Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        Throwable mostSpecificCause = ex.getMostSpecificCause();
        ErrorMessage errorMessage;
        if (mostSpecificCause != null) {
            String exceptionName = mostSpecificCause.getClass().getName();
            String message = mostSpecificCause.getMessage();
            errorMessage = new ErrorMessage(exceptionName, message);
        } else {
            errorMessage = new ErrorMessage(ex.getMessage());
        }
        return new ResponseEntity(errorMessage, headers, status);
    }
}

Comments

  • The @ControllerAdvice annotation is an @Component. As such, an annotated class will be registered as a Spring bean if the package in which it is located in is subject to component scanning.
  • The @ControllerAdvice also supports methods annotated with @InitBinder and @ModelAttribute.

References

Updated: