'Spring Boot: RestControllerAdvice: handle wrapped exceptions
Let's say we have @RestControllerAdvice-annotated class like this:
@RestControllerAdvice
public class RestResponseExceptionHandler {
@ExceptionHandler(MyBusinessException .class)
public ResponseEntity<ErrorResponse> handleMyBusinessException (MyBusinessException ex) {
return createResponseEntity(ex, ex.getErrorCode());
}
@ExceptionHandler({IllegalArgumentException.class, ValidationException.class, DataIntegrityViolationException.class})
public ResponseEntity<ErrorResponse> handleInvalidPropertyException(RuntimeException ex) {
return createResponseEntity(ex, ErrorCode.DATA_INVALID);
}
[...]
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException ex) {
return createResponseEntity(ex, ErrorCode.UNKNOWN);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception ex) {
return createResponseEntity(ex, ErrorCode.UNKNOWN);
}
@ExceptionHandler(WrapperException .class)
public ResponseEntity<ErrorResponse> handleWrapperException (WrapperException ex) {
Exception exception = ex.getWrappedException();
// re-dispatch exception here
}
}
For a known WrapperException, is it possible to somehow re-dispatch the wrapped exception?
I tried several things, e.g. rethrowing the wrapped excption or explicitly call a custom method of our ErrorController and re-throw the exception there, but so far no luck.
Solution 1:[1]
Why would you want to re-throw which may create unnecessary branching. You can do a conditional check and call the appropriate exception handler method as below.
@ExceptionHandler(WrapperException .class)
public ResponseEntity<ErrorResponse> handleWrapperException (WrapperException ex) {
Exception exception = ex.getWrappedException();
if (exception instanceof MyBusinessException) {
return handleMyBusinessException((MyBusinessException) exception);
}
return //Default
}
Solution 2:[2]
I think i figured the spring-ish way to do it. You can autowire in your RestResponseExceptionHandler bean of this type - HandlerExceptionResolver. Spring autoconfigures few of those for you, i managed to make it work with this one - handlerExceptionResolver. Something like this:
@RestControllerAdvice
public class ErrorHandler {
private static final String BEAN1 = "handlerExceptionResolver";
private static final String BEAN2 = "customResolver";
private final HandlerExceptionResolver resolver;
@Autowired
public ErrorHandler(@Qualifier(BEAN2) HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
@ExceptionHandler(WrapperException.class)
public void exception(WrapperException exception, HttpServletRequest request, HttpServletResponse response) {
Exception wrappedException = exception.getWrappedException();
//this will dispatch the handling to the handler for the wrapped exception
this.resolver.resolveException(request, response, null, wrappedException);
}
@ExceptionHandler(IndexOutOfBoundsException.class)
public ResponseEntity<Object> indexOutOfBoundsException() {
return ResponseEntity.of(Optional.of("IndexOutOfBoundsException"));
}
@ExceptionHandler(NullPointerException.class)
public ResponseEntity<Object> runtimeException() {
return ResponseEntity.of(Optional.of("NullPointerException"));
}
@ExceptionHandler(IOException.class)
public ResponseEntity<Object> ioException() {
return ResponseEntity.of(Optional.of("IOException"));
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Object> illegalArgumentException() {
return ResponseEntity.of(Optional.of("IllegalArgumentException"));
}
}
EDIT: Changed to autowire custom defined bean, the name of the spring autoconfigured bean is still there.
You will have to use the request and response in the wrapped exception handler, otherwise won't be able to handle it. When you call resolveException() with the wrapped one, it will reroute it to the correct handler for the wrapped exception. I did some testing using a controller to throw exceptions like this one, and everything was resolved correctly.
@Controller
public class ExcController {
@GetMapping("/thr")
public String throwExc() throws Exception {
throw new Exception(new NullPointerException());
}
@GetMapping("/thr2")
public String throwExc2() throws Exception {
throw new IOException();
}
}
EDIT: Found another implementation of HandlerExceptionResolver, which works - ExceptionHandlerExceptionResolver. To avoid circular dependencies of already existing beans, declare custom resolver to handle WrapperExceptions:
@Configuration
public class ExceptionResolverConfig {
@Bean
public HandlerExceptionResolver customResolver() {
return new ExceptionHandlerExceptionResolver();
}
}
Then autowire this bean in ErrorHandler. ExceptionHandlerExceptionResolver worked for me without any additional configuration.
Solution 3:[3]
As of Spring 5.3, @ExceptionHandler already looks into the cause exceptions when trying to find a match. So if you are on a recent Spring version, you can just remove your @ExceptionHandler(WrapperException.class) method, and it should just work as you expect.
The decision whether Spring matches the top-level (root) exception or the wrapped exception is a bit involved, and is fully explained in the manual: https://docs.spring.io/spring-framework/docs/5.3.15/reference/html/web.html#mvc-ann-exceptionhandler
The simplest way to get the right order working is to write the most unspecific exception handlers (i.e. for Exception and RuntimeException and similar) in their own @ControllerAdvice class and define a low precedence for this bean, e.g. with @Order(Ordered.LOWEST_PRECEDENCE). The handlers for the specific wrapped exceptions should then be defined in one or multiple beans of higher precedence.
Since this seems to have quite a high regression potential (e.g. by refactoring and shuffling things around), it is important to have some kind of test or tests for the correct precedence of exception handling.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | shazin |
| Solution 2 | |
| Solution 3 |
