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); }
basePackageClasses
- - the same asbasePackages
, 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); }
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); }
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);
}
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.