Spring ControllerAdvice - 无法覆盖 ResponseEntityExceptionHandler 中的 handleHttpRequestMethodNotSupported()

Spring ControllerAdvice - Fail to override handleHttpRequestMethodNotSupported() in ResponseEntityExceptionHandler

以下是我目前面临的情况的一些事实

  1. 我最近用各种ExceptionHandler构建了一个RestControllerAdvice作为我的Spring RestController的全局异常处理程序。

  2. 因为我想 return 我的自定义响应 json 用于处理 ResponseEntityExceptionHandler 中指定的预定义 HTTP 错误,我的 RestControllerAdvice class 继承了 ResponseEntityExceptionHandler 并且覆盖了 handleHttpRequestMethodNotSupported()handleHttpMessageNotReadable() 等方法。

  3. 我已经成功覆盖了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 的答案

对于 HttpRequestMethodNotSupportedExceptionDispatcherSerlvet 找不到控制器方法来处理请求。因此,传递给 getExceptionHandlerMethodhandlerMethodnull,然后传递给 isApplicableToBeanTypebeanType 也是 null,而 false 是 returned.

另一方面,DispatcherSerlvet可以找到HttpMessageNotReadableExceptionHttpMediaTypeNotSupportedException的控制器方法。因此,其余控制器处理程序方法将传递给 getExceptionHandlerMethod 并且 isApplicableToBeanType 将 return 为真。