Here's a few facts for the situation that I'm currently facing
I have recently built a RestControllerAdvice
with variousExceptionHandler
as a global exception handler for my Spring RestController.
As I would like to return my customized response json for handling the pre-defined HTTP error as specified in ResponseEntityExceptionHandler
, my RestControllerAdvice
class inherits the ResponseEntityExceptionHandler
and methods like handleHttpRequestMethodNotSupported()
, handleHttpMessageNotReadable()
are overriden.
I have successfully overridden handleHttpMediaTypeNotSupported()
and handleHttpMessageNotReadable()
but when it comes to handleHttpRequestMethodNotSupported()
, I fail to do so.
Here's an excerpt of my code:
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice(annotations=RestController.class)
public class TestRestExceptionHandler extends ResponseEntityExceptionHandler{
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request){
BaseResponseJson response = new BaseResponseJson();
response.setRespCode(BaseResponseJson.JSON_RESP_CODE_ERROR);
response.setRespMsg("Request Method Not Supported");
return handleExceptionInternal(ex, response, headers, status, request);
}
@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request){
BaseResponseJson response = new BaseResponseJson();
response.setRespCode(BaseResponseJson.JSON_RESP_CODE_ERROR);
response.setRespMsg("Message Not Readable");
return handleExceptionInternal(ex, response, headers, status, request);
}
@Override
protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request){
BaseResponseJson response = new BaseResponseJson();
response.setRespCode(BaseResponseJson.JSON_RESP_CODE_ERROR);
response.setRespMsg("Media Type Not Supported");
return handleExceptionInternal(ex, response, headers, status, request);
}
}
The log for handleHttpRequestMethodNotSupported()
is shown as follow:
[2019-06-05T17:49:50.368+0800][XNIO-74 task-7][WARN ][o.s.w.s.m.s.DefaultHandlerExceptionResolver] Resolved exception caused by Handler execution: org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'GET' not supported
The log for handleHttpMessageNotReadable()
is shown as follow:
[2019-06-05T17:50:21.915+0800][XNIO-74 task-8][WARN ][o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver] Resolved exception caused by Handler execution
As you can see, the successful code is handled by ExceptionHandlerExceptionResolver
while the malfunction code is handled by DefaultHandlerExceptionResolver
.
I am wondering what is the underlying reason and I will appreciate if someone can recommend any available solution. Thank you.
From the jackycflau answer, we can summarise as 2 questions.
Q1. Why removing
annotations=RestController.class
will works for HttpRequestMethodNotSupportedExceptionQ2. Why only
HttpRequestMethodNotSupportedException
is not caught?
To answer these 2 questions, we need to take a look to code on how spring handle exceptions. The following source code are based on spring 4.3.5.
During spring DispatcherServlet
processing the request, when error occur, HandlerExceptionResolver
will try to resolve the exception. In the given case, the exception is delegated to ExceptionHandlerExceptionResolver
. The method to determine which method to resolve the exception is (getExceptionHandlerMethod
in ExceptionHandlerExceptionResolver.java
line 417)
/**
* Find an {@code @ExceptionHandler} method for the given exception. The default
* implementation searches methods in the class hierarchy of the controller first
* and if not found, it continues searching for additional {@code @ExceptionHandler}
* methods assuming some {@linkplain ControllerAdvice @ControllerAdvice}
* Spring-managed beans were detected.
* @param handlerMethod the method where the exception was raised (may be {@code null})
* @param exception the raised exception
* @return a method to handle the exception, or {@code null}
*/
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = (handlerMethod != null ? handlerMethod.getBeanType() : null);
if (handlerMethod != null) {
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
if (resolver == null) {
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlerCache.put(handlerType, resolver);
}
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
}
}
for (Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
if (entry.getKey().isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(entry.getKey().resolveBean(), method);
}
}
}
return null;
}
Since we are using @RestControllerAdvice
, we only need to focus in the for loop, which determine which ControllerAdviceBean
to use. We can see that the method isApplicableToBeanType
will determine if the ControllerAdviceBean
is applicable, and the related code are (ControllerAdviceBean.java
line 149)
/**
* Check whether the given bean type should be assisted by this
* {@code @ControllerAdvice} instance.
* @param beanType the type of the bean to check
* @see org.springframework.web.bind.annotation.ControllerAdvice
* @since 4.0
*/
public boolean isApplicableToBeanType(Class<?> beanType) {
if (!hasSelectors()) {
return true;
}
else if (beanType != null) {
for (String basePackage : this.basePackages) {
if (beanType.getName().startsWith(basePackage)) {
return true;
}
}
for (Class<?> clazz : this.assignableTypes) {
if (ClassUtils.isAssignable(clazz, beanType)) {
return true;
}
}
for (Class<? extends Annotation> annotationClass : this.annotations) {
if (AnnotationUtils.findAnnotation(beanType, annotationClass) != null) {
return true;
}
}
}
return false;
}
private boolean hasSelectors() {
return (!this.basePackages.isEmpty() || !this.assignableTypes.isEmpty() || !this.annotations.isEmpty());
}
By reading the code, we can explain what is happening:
When annotations=RestController.class
is removed, hasSelectors
will return false, and hence isApplicableToBeanType
will return true. So HttpRequestMethodNotSupportedException
will be handled by TestRestExceptionHandler
in this case.
For HttpRequestMethodNotSupportedException
, DispatcherSerlvet
can not find controller method to handle request. Hence handlerMethod
passed to getExceptionHandlerMethod
is null
, then beanType
passed to isApplicableToBeanType
is also null and false is returned.
On the other hand, DispatcherSerlvet
can find controller method for HttpMessageNotReadableException
or HttpMediaTypeNotSupportedException
. So the rest controller handler method will be passed to getExceptionHandlerMethod
and isApplicableToBeanType
will return true.