Reputation: 86747
I want to let HandlerExceptionResolver resolve any Exceptions that I don't explicit catch via @ExceptionHandler
annotation.
Anyways, I want to apply specific logic on those exceptions. Eg send a mail notification or log additionally. I can achieve this by adding a @ExceptionHandler(Exception.class)
catch as follows:
@RestControllerAdvice
public MyExceptionHandler {
@ExceptionHandler(IOException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Object io(HttpServletRequest req, Exception e) {
return ...
}
@ExceptionHandler(Exception.class)
public Object exception(HttpServletRequest req, Exception e) {
MailService.send();
Logger.logInSpecificWay();
//TODO how to continue in the "normal" spring way with HandlerExceptionResolver?
}
}
Problem: if I add @ExceptionHandler(Exception.class)
like that, I can catch those unhandled exceptions.
BUT I cannot let spring
continue the normal workflow with HandlerExceptionResolver
to create the response ModelAndView
and set a HTTP STATUS code automatically.
Eg if someone tries a POST
on a GET
method, spring by default would return a 405 Method not allowed
. But with an @ExceptionHandler(Exception.class)
I would swallow this standard handling of spring...
So how can I keep the default HandlerExceptionResolver
, but still apply my custom logic?
Upvotes: 29
Views: 28313
Reputation: 36686
I had the same issue and solved it creating a implementation of the interface HandlerExceptionResolver
and removing the generic @ExceptionHandler(Exception.class)
from the generic handler method.
.
It works this way:
Spring will try to handle the exception calling MyExceptionHandler
first, but it will fail to find a handler because the annotation was removed from the generic handler. Next it will try other implementations of the interface HandlerExceptionResolver
. It will enter this generic implementation that just delegates to the original generic error handler.
After that, I need to convert the ResponseEntity
response to ModelAndView
using MappingJackson2JsonView
because this interface expects a ModelAndView
as return type.
@Component
class GenericErrorHandler(
private val errorHandler: MyExceptionHandler,
private val objectMapper: ObjectMapper
) : HandlerExceptionResolver {
override fun resolveException(request: HttpServletRequest, response: HttpServletResponse, handler: Any, ex: Exception): ModelAndView? {
// handle exception
val responseEntity = errorHandler.handleUnexpectedException(ex)
// prepare JSON view
val jsonView = MappingJackson2JsonView(objectMapper)
jsonView.setExtractValueFromSingleKeyModel(true) // prevents creating the body key in the response json
// prepare ModelAndView
val mv = ModelAndView(jsonView, mapOf("body" to responseEntity.body))
mv.status = responseEntity.statusCode
mv.view = jsonView
return mv
}
}
Upvotes: 1
Reputation: 86747
To provide a complete solution: it works just by extending ResponseEntityExceptionHandler
, as that handles all the spring-mvc
errors.
And the ones not handled can then be caught using @ExceptionHandler(Exception.class)
.
@RestControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> exception(Exception ex) {
MailService.send();
Logger.logInSpecificWay();
return ... custom exception
}
}
Upvotes: 17
Reputation: 131
Create classes for exception handling in this way
@RestControllerAdvice
public class MyExceptionHandler extends BaseExceptionHandler {
}
public class BaseExceptionHandler extends ResponseEntityExceptionHandler {
}
Here ResponseEntityExceptionHandler is provided by spring and override the several exception handler methods provided by it related to the requestMethodNotSupported,missingPathVariable,noHandlerFound,typeMismatch,asyncRequestTimeouts ....... with your own exception messages or error response objects and status codes
and have a method with @ExceptionHandler(Exception.class)
in MyExceptionHandler where the thrown exception comes finally if it doesn't have a matching handler.
Upvotes: 0
Reputation: 4547
Well, I was facing the same problem some time back and have tried several ways like extending ResponseEntityExceptionHandler
but all them were solving some problems but creating other ones.
Then I have decided to go with a custom solution which was also allowing me to send additional information and I have written below code
@RestControllerAdvice
public class MyExceptionHandler {
private final Logger log = LoggerFactory.getLogger(this.getClass());
@ExceptionHandler(NumberFormatException.class)
public ResponseEntity<Object> handleNumberFormatException(NumberFormatException ex) {
return new ResponseEntity<>(getBody(BAD_REQUEST, ex, "Please enter a valid value"), new HttpHeaders(), BAD_REQUEST);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Object> handleIllegalArgumentException(IllegalArgumentException ex) {
return new ResponseEntity<>(getBody(BAD_REQUEST, ex, ex.getMessage()), new HttpHeaders(), BAD_REQUEST);
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<Object> handleAccessDeniedException(AccessDeniedException ex) {
return new ResponseEntity<>(getBody(FORBIDDEN, ex, ex.getMessage()), new HttpHeaders(), FORBIDDEN);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> exception(Exception ex) {
return new ResponseEntity<>(getBody(INTERNAL_SERVER_ERROR, ex, "Something Went Wrong"), new HttpHeaders(), INTERNAL_SERVER_ERROR);
}
public Map<String, Object> getBody(HttpStatus status, Exception ex, String message) {
log.error(message, ex);
Map<String, Object> body = new LinkedHashMap<>();
body.put("message", message);
body.put("timestamp", new Date());
body.put("status", status.value());
body.put("error", status.getReasonPhrase());
body.put("exception", ex.toString());
Throwable cause = ex.getCause();
if (cause != null) {
body.put("exceptionCause", ex.getCause().toString());
}
return body;
}
}
Upvotes: 5