Intro

For Spring web applications it is very usual an obvious thing that sometimes we would like to customise error messages that are sent back to the client by providing additional description and/or error code that provides additional information. To achieve this we use @ControllerAdvice annotation to register a bean which catches exception and converts them into structure we want. This is how it can look like:

@Slf4j
@ControllerAdvice
public class CommonErrorHandler {

    @ExceptionHandler({GenericServiceException.class})
    public final ResponseEntity<ErrorDTO> handleGenericServiceException(Exception ex) {
        log.error("ServiceException", ex);
        return new ResponseEntity<>(new ErrorDTO(CommonErrorTypeEnum.DEFAULT_ERROR.getMessage(),
            CommonErrorTypeEnum.DEFAULT_ERROR.getErrorCode()), HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler({
      ConstraintViolationException.class,
      MissingServletRequestParameterException.class,
      MethodArgumentTypeMismatchException.class
      MethodArgumentNotValidException.class
    })
    public final ResponseEntity<ErrorDTO> constraintViolationException(Exception ex) {
        log.error("ConstraintViolationException", ex);
        return new ResponseEntity<>(new ErrorDTO(CommonErrorTypeEnum.BAD_REQUEST.getMessage(),
            CommonErrorTypeEnum.BAD_REQUEST.getErrorCode()), HttpStatus.BAD_REQUEST);
    }

}

This code already serves our needs, but what if (for example) we have multiple modules in our project, every module throws it’s own MethodArgumentNotValidException.class, although in some cases it’s still ok to send back error common to entire application, in other cases we need to provide module specific information regarding: special errorCode or errorMessage.

How to specialise controller advice per controller

One simple way would be to play with order of precedence using @Order annotation:

@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class SpecificErrorHandler {

    @ExceptionHandler({MethodArgumentNotValidException.class})
    public final ResponseEntity<ErrorDTO> handleModuleSpecificError(Exception ex) {
        log.error("Module specific error - MethodArgumentNotValidException", ex);
        return new ResponseEntity<>(new ErrorDTO(SpecificErrorType.SPECIFIC_ERROR.getMessage(),
                SpecificErrorType.SPECIFIC_ERROR.getErrorCode()), HttpStatus.BAD_REQUEST);
    }

This makes our error being handled by SpecificErrorHandler first. This is just a partial solution that works only for specific module and breaks error handling for other modules, because now all MethodArgumentNotValidException errors will be handled by this Advice - definitely not ideal situation.

Thankfully we have another much more suitable way to achieve specialising of advice and still being flexible for other modules - use nice feature from spring @ControllerAdvice annotation. @ControllerAdvice has the following properties:

  • basePackages - allows to define package in which controllers will be advised using string
    @Slf4j
    @ControllerAdvice(basePackages = "org.my.pkg")
    public class SpecificErrorHandler {
    
      @ExceptionHandler({MethodArgumentNotValidException.class})
      public final ResponseEntity<ErrorDTO> handleModuleSpecificError(Exception ex) {
          log.error("Module specific error - MethodArgumentNotValidException", ex);
          return new ResponseEntity<>(new ErrorDTO(SpecificErrorType.SPECIFIC_ERROR.getMessage(),
                  SpecificErrorType.SPECIFIC_ERROR.getErrorCode()), HttpStatus.BAD_REQUEST);
      }
    

    Advice based on basePackages property

  • basePackageClasses - - the same as basePackages, but type safe because uses classes
    @Slf4j
    @ControllerAdvice(basePackageClasses = SpecificController.class)
    public class SpecificErrorHandler {
    
      @ExceptionHandler({MethodArgumentNotValidException.class})
      public final ResponseEntity<ErrorDTO> handleModuleSpecificError(Exception ex) {
          log.error("Module specific error - MethodArgumentNotValidException", ex);
          return new ResponseEntity<>(new ErrorDTO(SpecificErrorType.SPECIFIC_ERROR.getMessage(),
                  SpecificErrorType.SPECIFIC_ERROR.getErrorCode()), HttpStatus.BAD_REQUEST);
      }
    

    Advice based on basePackagesClasses property

  • assignableTypes - also type safe, but instead of advising all controllers in package it will advice only mentioned controllers
    @Slf4j
    @ControllerAdvice(assignableTypes = SpecificController.class)
    public class SpecificErrorHandler {
    
      @ExceptionHandler({MethodArgumentNotValidException.class})
      public final ResponseEntity<ErrorDTO> handleModuleSpecificError(Exception ex) {
          log.error("Module specific error - MethodArgumentNotValidException", ex);
          return new ResponseEntity<>(new ErrorDTO(SpecificErrorType.SPECIFIC_ERROR.getMessage(),
                  SpecificErrorType.SPECIFIC_ERROR.getErrorCode()), HttpStatus.BAD_REQUEST);
      }
    

    Advice based on assignableTypes property

  • annotations - also type safe, but uses annotation to check wich controller to advice
public interface SpecificAdvice extends Annotation {

}

@Slf4j
@ControllerAdvice(annotations = SpecificAdvice.class)
public class SpecificErrorHandler {

    @ExceptionHandler({MethodArgumentNotValidException.class})
    public final ResponseEntity<ErrorDTO> handleModuleSpecificError(Exception ex) {
        log.error("Module specific error - MethodArgumentNotValidException", ex);
        return new ResponseEntity<>(new ErrorDTO(SpecificErrorType.SPECIFIC_ERROR.getMessage(),
                SpecificErrorType.SPECIFIC_ERROR.getErrorCode()), HttpStatus.BAD_REQUEST);
    }

Advice based on annotations property

Conclusion

Spring @ControllerAdvice annotation is very conveniet and easy to use tool that can help you configure different strategies not only per module, but even per specific controllers within the same package, very neat.

Updated: