Spring ControllerAdvice - 无法覆盖 ResponseEntityExceptionHandler 中的 handleHttpRequestMethodNotSupported()
Spring ControllerAdvice - Fail to override handleHttpRequestMethodNotSupported() in ResponseEntityExceptionHandler
以下是我目前面临的情况的一些事实
我最近用各种ExceptionHandler
构建了一个RestControllerAdvice
作为我的Spring RestController的全局异常处理程序。
因为我想 return 我的自定义响应 json 用于处理 ResponseEntityExceptionHandler
中指定的预定义 HTTP 错误,我的 RestControllerAdvice
class 继承了 ResponseEntityExceptionHandler
并且覆盖了 handleHttpRequestMethodNotSupported()
、handleHttpMessageNotReadable()
等方法。
我已经成功覆盖了handleHttpMediaTypeNotSupported()
和handleHttpMessageNotReadable()
,但是当涉及到handleHttpRequestMethodNotSupported()
时,我没有这样做。
这是我的代码的摘录:
@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);
}
}
handleHttpRequestMethodNotSupported()
的日志如下:
[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
handleHttpMessageNotReadable()
的日志如下:
[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
可以看到,成功代码由ExceptionHandlerExceptionResolver
处理,故障代码由DefaultHandlerExceptionResolver
处理。
我想知道根本原因是什么,如果有人可以推荐任何可用的解决方案,我将不胜感激。谢谢。
我找到了问题的罪魁祸首,它与 @RestControllerAdvice
注释有关。
原来我把class注释成@RestControllerAdvice(annotations=RestController.class)
。
在我删除 annotations
key-value 对后(即只需将 class 注释为 @RestControllerAdvice
),HttpRequestMethodNotSupportedException
现在已成功捕获。
这是我只能分享的解决方案。我不明白根本原因,这种行为对我来说似乎很奇怪......可能是因为 HttpRequestMethodNotSupportedException
不受 @RestController
的控制??? (只是一个疯狂的猜测)。如果有人能对这种行为给出完整的解释,我会很高兴。
根据 jackycflau 的回答,我们可以总结为 2 个问题。
Q1. Why removing annotations=RestController.class
will works for HttpRequestMethodNotSupportedException
Q2. Why only HttpRequestMethodNotSupportedException
is not caught?
要回答这 2 个问题,我们需要查看有关 spring 如何处理异常的代码。以下源码均基于spring 4.3.5.
在springDispatcherServlet
处理请求的过程中,当出现错误时,HandlerExceptionResolver
会尝试解决异常。在给定的情况下,异常被委托给 ExceptionHandlerExceptionResolver
。判断用什么方法解决异常的方法是(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;
}
因为我们使用的是@RestControllerAdvice
,我们只需要关注for循环,它决定使用哪个ControllerAdviceBean
。我们可以看到方法isApplicableToBeanType
会判断ControllerAdviceBean
是否适用,相关代码为(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());
}
通过阅读代码,我们可以解释发生了什么:
Q1 的答案
当删除 annotations=RestController.class
时,hasSelectors
将 return 为假,因此 isApplicableToBeanType
将 return 为真。所以在这种情况下 HttpRequestMethodNotSupportedException
将由 TestRestExceptionHandler
处理。
Q2 的答案
对于 HttpRequestMethodNotSupportedException
,DispatcherSerlvet
找不到控制器方法来处理请求。因此,传递给 getExceptionHandlerMethod
的 handlerMethod
是 null
,然后传递给 isApplicableToBeanType
的 beanType
也是 null,而 false 是 returned.
另一方面,DispatcherSerlvet
可以找到HttpMessageNotReadableException
或HttpMediaTypeNotSupportedException
的控制器方法。因此,其余控制器处理程序方法将传递给 getExceptionHandlerMethod
并且 isApplicableToBeanType
将 return 为真。
以下是我目前面临的情况的一些事实
我最近用各种
ExceptionHandler
构建了一个RestControllerAdvice
作为我的Spring RestController的全局异常处理程序。因为我想 return 我的自定义响应 json 用于处理
ResponseEntityExceptionHandler
中指定的预定义 HTTP 错误,我的RestControllerAdvice
class 继承了ResponseEntityExceptionHandler
并且覆盖了handleHttpRequestMethodNotSupported()
、handleHttpMessageNotReadable()
等方法。我已经成功覆盖了
handleHttpMediaTypeNotSupported()
和handleHttpMessageNotReadable()
,但是当涉及到handleHttpRequestMethodNotSupported()
时,我没有这样做。
这是我的代码的摘录:
@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);
}
}
handleHttpRequestMethodNotSupported()
的日志如下:
[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
handleHttpMessageNotReadable()
的日志如下:
[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
可以看到,成功代码由ExceptionHandlerExceptionResolver
处理,故障代码由DefaultHandlerExceptionResolver
处理。
我想知道根本原因是什么,如果有人可以推荐任何可用的解决方案,我将不胜感激。谢谢。
我找到了问题的罪魁祸首,它与 @RestControllerAdvice
注释有关。
原来我把class注释成@RestControllerAdvice(annotations=RestController.class)
。
在我删除 annotations
key-value 对后(即只需将 class 注释为 @RestControllerAdvice
),HttpRequestMethodNotSupportedException
现在已成功捕获。
这是我只能分享的解决方案。我不明白根本原因,这种行为对我来说似乎很奇怪......可能是因为 HttpRequestMethodNotSupportedException
不受 @RestController
的控制??? (只是一个疯狂的猜测)。如果有人能对这种行为给出完整的解释,我会很高兴。
根据 jackycflau 的回答,我们可以总结为 2 个问题。
Q1. Why removing
annotations=RestController.class
will works for HttpRequestMethodNotSupportedExceptionQ2. Why only
HttpRequestMethodNotSupportedException
is not caught?
要回答这 2 个问题,我们需要查看有关 spring 如何处理异常的代码。以下源码均基于spring 4.3.5.
在springDispatcherServlet
处理请求的过程中,当出现错误时,HandlerExceptionResolver
会尝试解决异常。在给定的情况下,异常被委托给 ExceptionHandlerExceptionResolver
。判断用什么方法解决异常的方法是(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;
}
因为我们使用的是@RestControllerAdvice
,我们只需要关注for循环,它决定使用哪个ControllerAdviceBean
。我们可以看到方法isApplicableToBeanType
会判断ControllerAdviceBean
是否适用,相关代码为(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());
}
通过阅读代码,我们可以解释发生了什么:
Q1 的答案
当删除 annotations=RestController.class
时,hasSelectors
将 return 为假,因此 isApplicableToBeanType
将 return 为真。所以在这种情况下 HttpRequestMethodNotSupportedException
将由 TestRestExceptionHandler
处理。
Q2 的答案
对于 HttpRequestMethodNotSupportedException
,DispatcherSerlvet
找不到控制器方法来处理请求。因此,传递给 getExceptionHandlerMethod
的 handlerMethod
是 null
,然后传递给 isApplicableToBeanType
的 beanType
也是 null,而 false 是 returned.
另一方面,DispatcherSerlvet
可以找到HttpMessageNotReadableException
或HttpMediaTypeNotSupportedException
的控制器方法。因此,其余控制器处理程序方法将传递给 getExceptionHandlerMethod
并且 isApplicableToBeanType
将 return 为真。