Search code examples
springapispring-mvcmicroservicesspring-restcontroller

Spring ControllerAdvice - Fail to override handleHttpRequestMethodNotSupported() in ResponseEntityExceptionHandler


Here's a few facts for the situation that I'm currently facing

  1. I have recently built a RestControllerAdvice with variousExceptionHandler as a global exception handler for my Spring RestController.

  2. 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.

  3. 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.


Solution

  • From the jackycflau answer, we can summarise as 2 questions.

    Q1. Why removing annotations=RestController.class will works for HttpRequestMethodNotSupportedException

    Q2. 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:

    Answer for Q1

    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.

    Answer for Q2

    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.