Overriding the handling behavior for standard Spring MVC exceptions
Thanks davidxxx for this answer and examples. They helped me a lot. thank you! I did it this way and it worked for me. Here is an example:
@ControllerAdvice
public class AppExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<?> resourceNotFoundException(final ResourceNotFoundException ex, final WebRequest request) {
final ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(ResourceAlreadyExistsFoundException.class)
public ResponseEntity<?> resourceAlreadyExistsFoundException(final ResourceAlreadyExistsFoundException ex, final WebRequest request) {
final ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(ConstraintViolationException.class)
ResponseEntity<?> onConstraintValidationException(
ConstraintViolationException e) {
List<ErrorDetails> errors = new ArrayList<>();
for (ConstraintViolation violation : e.getConstraintViolations()) {
errors.add(
new ErrorDetails(new Date(), violation.getPropertyPath().toString(), violation.getMessage()));
}
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
ArrayList<ErrorDetails> errors = new ArrayList<>();
for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
errors.add(
new ErrorDetails(new Date(), fieldError.getField(), fieldError.getDefaultMessage()));
}
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> globleExcpetionHandler(final Exception ex, final WebRequest request) {
final ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Spring MVC configures indeed an exception handler for you.
By default, DefaultHandlerExceptionResolver
is used as stated in the class javadoc :
Default implementation of the
HandlerExceptionResolver
interface that resolves standard Spring exceptions and translates them to corresponding HTTP status codes.This exception resolver is enabled by default in the common Spring
org.springframework.web.servlet.DispatcherServlet
.
That is right for MVC controllers.
But for exception handlers for REST controllers (your requirement here), Spring rely on the ResponseEntityExceptionHandler
class.
The first class have methods that return ModelAndView
s while the second class have methods that return ReponseEntity
s.
You can define a custom exception handler by annotating your class with @ControllerAdvice
in both cases (MVC and REST controllers) but since your requirement is for REST controllers, let's focus on that.
Besides annotating a custom exception handler with @ControllerAdvice
, you can also make that to extend a base exception handler class such as ResponseEntityExceptionHandler
to override some behaviors.ResponseEntityExceptionHandler
implementations allows to know all the exceptions actually handled and mapped. Look at the handleException()
method that is the facade method of the ResponseEntityExceptionHandler
class :
/**
* Provides handling for standard Spring MVC exceptions.
* @param ex the target exception
* @param request the current request
*/
@ExceptionHandler({
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
HttpMediaTypeNotAcceptableException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
ServletRequestBindingException.class,
ConversionNotSupportedException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
MethodArgumentNotValidException.class,
MissingServletRequestPartException.class,
BindException.class,
NoHandlerFoundException.class,
AsyncRequestTimeoutException.class
})
@Nullable
public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) {
HttpHeaders headers = new HttpHeaders();
if (ex instanceof HttpRequestMethodNotSupportedException) {
HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED;
return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, headers, status, request);
}
else if (ex instanceof HttpMediaTypeNotSupportedException) {
HttpStatus status = HttpStatus.UNSUPPORTED_MEDIA_TYPE;
return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, headers, status, request);
}
else if (ex instanceof HttpMediaTypeNotAcceptableException) {
HttpStatus status = HttpStatus.NOT_ACCEPTABLE;
return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, headers, status, request);
}
else if (ex instanceof MissingPathVariableException) {
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
return handleMissingPathVariable((MissingPathVariableException) ex, headers, status, request);
}
else if (ex instanceof MissingServletRequestParameterException) {
HttpStatus status = HttpStatus.BAD_REQUEST;
return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, headers, status, request);
}
else if (ex instanceof ServletRequestBindingException) {
HttpStatus status = HttpStatus.BAD_REQUEST;
return handleServletRequestBindingException((ServletRequestBindingException) ex, headers, status, request);
}
else if (ex instanceof ConversionNotSupportedException) {
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
return handleConversionNotSupported((ConversionNotSupportedException) ex, headers, status, request);
}
else if (ex instanceof TypeMismatchException) {
HttpStatus status = HttpStatus.BAD_REQUEST;
return handleTypeMismatch((TypeMismatchException) ex, headers, status, request);
}
else if (ex instanceof HttpMessageNotReadableException) {
HttpStatus status = HttpStatus.BAD_REQUEST;
return handleHttpMessageNotReadable((HttpMessageNotReadableException) ex, headers, status, request);
}
else if (ex instanceof HttpMessageNotWritableException) {
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
return handleHttpMessageNotWritable((HttpMessageNotWritableException) ex, headers, status, request);
}
else if (ex instanceof MethodArgumentNotValidException) {
HttpStatus status = HttpStatus.BAD_REQUEST;
return handleMethodArgumentNotValid((MethodArgumentNotValidException) ex, headers, status, request);
}
else if (ex instanceof MissingServletRequestPartException) {
HttpStatus status = HttpStatus.BAD_REQUEST;
return handleMissingServletRequestPart((MissingServletRequestPartException) ex, headers, status, request);
}
else if (ex instanceof BindException) {
HttpStatus status = HttpStatus.BAD_REQUEST;
return handleBindException((BindException) ex, headers, status, request);
}
else if (ex instanceof NoHandlerFoundException) {
HttpStatus status = HttpStatus.NOT_FOUND;
return handleNoHandlerFoundException((NoHandlerFoundException) ex, headers, status, request);
}
else if (ex instanceof AsyncRequestTimeoutException) {
HttpStatus status = HttpStatus.SERVICE_UNAVAILABLE;
return handleAsyncRequestTimeoutException(
(AsyncRequestTimeoutException) ex, headers, status, request);
}
else {
if (logger.isWarnEnabled()) {
logger.warn("Unknown exception type: " + ex.getClass().getName());
}
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
return handleExceptionInternal(ex, null, headers, status, request);
}
}
So the question is : how to override the exception handler for a specific exception ?
This approach cannot work :
@ExceptionHandler(value = { HttpRequestMethodNotSupportedException.class })
protected ResponseEntity<Object> handleConflict(HttpRequestMethodNotSupportedException ex, WebRequest request) {
...
}
Because inside the exception handler class, Spring doesn't let you to define more than a single time a mapping for a specific Exception
subclass. So adding this mapping in your custom exception handler is not allowed because Spring already defines a mapping for that exception in the ResponseEntityExceptionHandler
class.
Concretely, it will prevent the Spring container from starting successfully.
You should get an exception such as :
Caused by: java.lang.IllegalStateException: Ambiguous @ExceptionHandler method mapped for [class org.springframework.web.HttpRequestMethodNotSupportedException]: {protected org.springframework...
To ease client subclasses to override the actual handling/mapping for a specific exception, Spring implemented the logic of each exception caught and handled by itself in a protected
method of the ResponseEntityExceptionHandler
class.
So in your case (overriding the handler of HttpRequestMethodNotSupportedException
), just override handleHttpRequestMethodNotSupported()
that is what you are looking for :
if (ex instanceof HttpRequestMethodNotSupportedException) {
HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED;
return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, headers, status, request);
}
For example in this way :
@ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status,
WebRequest request) {
// do your processing
...
// go on (or no) executing the logic defined in the base class
return super.handleHttpRequestMethodNotSupported(ex, headers, status, request);
}
}