Spring系列(七) Spring MVC 異常處理

罪惡斯巴克發表於2018-10-05

Servlet傳統異常處理

Servlet規範規定了當web應用發生異常時必須能夠指明, 並確定了該如何處理, 規定了錯誤資訊應該包含的內容和展示頁面的方式.(詳細可以參考servlet規範文件)

處理方式

  • 處理狀態碼<error-code>
  • 處理異常資訊<exception-type>
  • 處理服務地址<location>

Spring MVC 處理方式

所有的請求必然以某種方式轉化為響應.

  • Spring中特定的異常將自動對映為特定的HTTP狀態碼
  • 使用@ResponseStatus註解可以對映某一異常到特定的HTTP狀態碼
  • Controller方法上可以使用@ExceptionHandler註解使其用來處理異常
  • 使用@ControllerAdvice 方式可以統一的方式處理全域性異常

Spring boot 方式

  • 實現ErrorPageRegistrar: 確定是頁面處理的路徑必須固定,優點是比較通用
  • 註冊ErrorPage
  • 實現ErrorPage對應的服務

 原始碼分析

一.介面HandlerExceptionResolver

該介面定義了Spring中該如何處理異常. 它只有一個方法resolveException(), 介面原始碼如下:

 // 由物件實現的介面,這些物件可以解決在處理程式對映或執行期間引發的異常,在典型的情況下是錯誤檢視。在應用程式上下文中,實現器通常被註冊為bean。
 // 錯誤檢視類似於JSP錯誤頁面,但是可以與任何型別的異常一起使用,包括任何已檢查的異常,以及針對特定處理程式的潛在細粒度對映。
public interface HandlerExceptionResolver {
    @Nullable
    ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}

Spring 為該介面提供了若干實現類如下:

HandlerExceptionResolverComposite               委託給其他HandlerExceptionResolver的例項列表
AbstractHandlerExceptionResolver                抽象基類
    AbstractHandlerMethodExceptionResolver      支援HandlerMethod處理器的抽象基類
        ExceptionHandlerExceptionResolver       通過 @ExceptionHandler 註解的方式實現的異常處理
    DefaultHandlerExceptionResolver             預設實現, 處理spring預定義的異常並將其對應到錯誤碼
    ResponseStatusExceptionResolver             通過 @ResponseStatus 註解對映到錯誤碼的異常
    SimpleMappingExceptionResolver              允許將異常類對映到檢視名

二. DefaultHandlerExceptionResolver

這個類是Spring提供的預設實現, 用於將一些常見異常對映到特定的狀態碼. 這些狀態碼定義在介面HttpServletResponse中, 下面是幾個狀態碼的程式碼片段

public interface HttpServletResponse extends ServletResponse {
    ...
    public static final int SC_OK = 200;
    public static final int SC_MOVED_PERMANENTLY = 301;
    public static final int SC_MOVED_TEMPORARILY = 302;
    public static final int SC_FOUND = 302;
    public static final int SC_UNAUTHORIZED = 401;
    public static final int SC_INTERNAL_SERVER_ERROR = 500;
    ...
}

實際上, DefaultHandlerExceptionResolver中並沒有直接實現介面的resolveException方法, 而是實現了抽象類AbstractHandlerExceptionResolverdoResolveException()方法, 後者則在實現了介面的方法中委託給抽象方法doResolveException, 這個方法由子類去實現.

AbstractHandlerExceptionResolverresolveException方法程式碼如下:

@Override
@Nullable
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

    // 判斷是否當前解析器可用於handler
    if (shouldApplyTo(request, handler)) { 
        prepareResponse(ex, response);
        ModelAndView result = doResolveException(request, response, handler, ex);
        if (result != null) {
            // Print warn message when warn logger is not enabled...
            if (logger.isWarnEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
                logger.warn("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result));
            }
            // warnLogger with full stack trace (requires explicit config)
            logException(ex, request);
        }
        return result;
    }
    else {
        return null;
    }
}

接下來我們看DefaultHandlerExceptionResolver實現的doResolveException方法. 程式碼如下;

@Override
    @Nullable
    protected ModelAndView doResolveException(
            HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

        try {
            if (ex instanceof HttpRequestMethodNotSupportedException) {
                return handleHttpRequestMethodNotSupported(
                        (HttpRequestMethodNotSupportedException) ex, request, response, handler);
            }
            else if (ex instanceof HttpMediaTypeNotSupportedException) {
                return handleHttpMediaTypeNotSupported(
                        (HttpMediaTypeNotSupportedException) ex, request, response, handler);
            }
            ....
            else if (ex instanceof NoHandlerFoundException) {
                return handleNoHandlerFoundException(
                        (NoHandlerFoundException) ex, request, response, handler);
            }
            .....
        }
        catch (Exception handlerEx) {
            if (logger.isWarnEnabled()) {
                logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);
            }
        }
        return null;
    }

可以看到程式碼中使用了大量的分支語句, 實際上是將方法傳入的異常型別通過instanceof運算子測試, 通過測試的轉化為特定的異常. 並呼叫處理該異常的特定方法. 我們挑一個比如處理NoHandlerFoundException這個異常類的方法, 這個方法將異常對映為404錯誤.

protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex,
        HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {

    pageNotFoundLogger.warn(ex.getMessage());
    response.sendError(HttpServletResponse.SC_NOT_FOUND); //設定為404錯誤
    return new ModelAndView(); //返回個空檢視
}

上面分析了Spring預設的異常處理實現類DefaultHandlerExceptionResolver.它處理的異常是Spring預定義的幾種常見異常, 它將異常對應到HTTP的狀態碼. 而對於不屬於這些型別的其他異常, 我們可以使用ResponseStatusExceptionResolver來處理, 將其對應到HTTP狀態碼.

三. ResponseStatusExceptionResolver

如何使用?

@GetMapping("/responseStatus")
@ResponseBody
public String responseStatus() throws MyException {
    throw new MyException();
}

@ResponseStatus(code = HttpStatus.BAD_GATEWAY)
public class MyException extends Exception{}

只需要在異常上使用@ResponseStatus註解即可將特定的自定義異常對應到Http的狀態碼.

四. ExceptionHandlerExceptionResolver

使用類似於普通的controller方法, 使用@ExceptionHandler註解的方法將作為處理該註解引數中異常的handler. 比如, 在一個controller中, 我們定義一個處理NPE的異常處理handler方法, 可以用來處理該controller中丟擲的NPE. 程式碼如下:

 @GetMapping("/npe1")
@ResponseBody
public String npe1() throws NullPointerException {
    throw new NullPointerException();
}

@GetMapping("/npe2")
@ResponseBody
public String npe2() throws NullPointerException {
    throw new NullPointerException();
}

@ExceptionHandler(value = {NullPointerException.class})
@ResponseBody
public String npehandler(){
    return "test npe handler";
}

無論是請求/npe1還是請求/npe2, 系統都會丟擲異常, 並交給對應的處理程式npehandler去處理. 使用@ExceptionHandler(value = {NullPointerException.class})註解的方法可以處理本controller範圍內的所有方法排除的npe異常, 如果要將其作為應用中所有controller的異常處理器, 就要將其定義在@ControllerAdvice註解的類中.

@ControllerAdvice
public class ControllerAdvicer {

    @ExceptionHandler(value = {NullPointerException.class})
    @ResponseBody
    public String npehandler(){
        return "test npe handler in advice";
    }
}

要了解其原理, 需要檢視ExceptionHandlerExceptionResolver中的方法doResolveHandlerMethodException

@Override
@Nullable
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
        HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
    
    // 獲取異常對用的處理器, 就是@ExceptionHandler註解的方法包裝, 注意引數handlerMethod, 在方法內部, 它將用來獲取所在Controller的資訊
    ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
    if (exceptionHandlerMethod == null) {
        return null;
    }

    if (this.argumentResolvers != null) {
        exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
    }
    if (this.returnValueHandlers != null) {
        exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
    }

    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    ModelAndViewContainer mavContainer = new ModelAndViewContainer();

    try {
        if (logger.isDebugEnabled()) {
            logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod);
        }
        Throwable cause = exception.getCause();
        // 呼叫異常處理handler的方法.
        if (cause != null) {
            // Expose cause as provided argument as well
            exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
        }
        else {
            // Otherwise, just the given exception as-is
            exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
        }
    }
    catch (Throwable invocationEx) {
        // Any other than the original exception is unintended here,
        // probably an accident (e.g. failed assertion or the like).
        if (invocationEx != exception && logger.isWarnEnabled()) {
            logger.warn("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx);
        }
        // Continue with default processing of the original exception...
        return null;
    }

    if (mavContainer.isRequestHandled()) {
        return new ModelAndView();
    }
    else {
        ModelMap model = mavContainer.getModel();
        HttpStatus status = mavContainer.getStatus();
        ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status);
        mav.setViewName(mavContainer.getViewName());
        if (!mavContainer.isViewReference()) {
            mav.setView((View) mavContainer.getView());
        }
        if (model instanceof RedirectAttributes) {
            Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
            RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
        }
        return mav;
    }
}

可以看到在兩個中文註釋的地方, 其一是方法的開始部分獲取到了異常的handler, 其二是呼叫這個handler的方法. 呼叫方法應該很好理解, 我們接下來檢視方法getExceptionHandlerMethod.

// 找到給定異常對應的@ExceptionHandler註解方法, 預設先在controller類的繼承結構中查詢, 否則繼續在@ControllerAdvice註解的 bean中查詢.
@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
        @Nullable HandlerMethod handlerMethod, Exception exception) {

    Class<?> handlerType = null;

    if (handlerMethod != null) {
        // Local exception handler methods on the controller class itself.
        // To be invoked through the proxy, even in case of an interface-based proxy.
        handlerType = handlerMethod.getBeanType();
        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 advice applicability check below (involving base packages, assignable types
        // and annotation presence), use target class instead of interface-based proxy.
        if (Proxy.isProxyClass(handlerType)) {
            handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
        }
    }

    // 在@ControllerAdvice註解的類中遍歷查詢
    for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
        ControllerAdviceBean advice = entry.getKey();
        if (advice.isApplicableToBeanType(handlerType)) {
            ExceptionHandlerMethodResolver resolver = entry.getValue();
            Method method = resolver.resolveMethod(exception);
            if (method != null) {
                return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
            }
        }
    }

    return null;
}

我們可以看到,它會首先查詢controller中的方法, 如果找不到才去查詢@ControllerAdvice註解的bean. 也就是說controller中的handler的優先順序要高於advice.

上面我們瞭解了幾個Exceptionresolver的使用, 並通過原始碼簡單看了他們各自處理的原理. 但這些Resolver如何載入我們還不知道, 接下來我們重點看下他們是如何載入進去的.

四. ExceptionResolver的載入

在本系列的上一篇Spring系列(六) Spring Web MVC 應用構建分析中, 我們大致提到了DispatcherServlet的啟動呼叫關係如下:

整理下呼叫關係: DispatcherServlet initHandlerMappings <-- initStrategies <-- onRefresh <--
FrameworkServlet initWebApplicationContext <-- initServletBean <--
HttpServletBean init <--
GenericServlet init(ServletConfig config)
最後的GenericServlet是servlet Api的.

正是在initStrategies方法中, DispatcherServlet做了啟動的一系列工作, 除了initHandlerMappings還可以看到一個initHandlerExceptionResolvers的方法, 其原始碼如下:

// 初始化HandlerExceptionResolver, 如果沒有找到任何名稱空間中定義的bean, 預設沒有任何resolver
private void initHandlerExceptionResolvers(ApplicationContext context) {
    this.handlerExceptionResolvers = null;

    if (this.detectAllHandlerExceptionResolvers) {
        // 找到所有ApplicationContext中定義的 HandlerExceptionResolvers 包括在上級上下文中.
        Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
                .beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
            // 保持有序.
            AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
        }
    }
    else {
        try {
            HandlerExceptionResolver her =
                    context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
            this.handlerExceptionResolvers = Collections.singletonList(her);
        }
        catch (NoSuchBeanDefinitionException ex) {
            // Ignore, no HandlerExceptionResolver is fine too.
        }
    }

    // 確保有Resolver, 否則使用預設的
    if (this.handlerExceptionResolvers == null) {
        this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
        if (logger.isTraceEnabled()) {
            logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() +
                    "': using default strategies from DispatcherServlet.properties");
        }
    }
}

好了, 現在我們載入了應用程式中所有定義的Resolver. 當有請求到達時, DispatcherServletdoDispatch方法使用請求特定的handler處理, 當handler發生異常時, 變數dispatchException的值賦值為丟擲的異常, 並委託給方法processDispatchResult

doDispatch的程式碼, 只摘錄出與本議題有關的.

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    ....
    try {
        ModelAndView mv = null;
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    }catch (Exception ex) {
        dispatchException = ex;
    }
    catch (Throwable err) {
        // As of 4.3, we're processing Errors thrown from handler methods as well,
        // making them available for @ExceptionHandler methods and other scenarios.
        dispatchException = new NestedServletException("Handler dispatch failed", err);
    }
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    ....
}

// 處理handler的結果
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
        @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
        @Nullable Exception exception) throws Exception {

    boolean errorView = false;

    // 異常處理
    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException) {
            logger.debug("ModelAndViewDefiningException encountered", exception);
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
        }
        else {
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            mv = processHandlerException(request, response, handler, exception);
            errorView = (mv != null);
        }
    }

    // handler是否返回了view
    if (mv != null && !mv.wasCleared()) {
        render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    }
    else {
        if (logger.isTraceEnabled()) {
            logger.trace("No view rendering, null ModelAndView returned.");
        }
    }

    if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
        // Concurrent handling started during a forward
        return;
    }

    if (mappedHandler != null) {
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}

processDispatchResult方法中可以看到, 如果引數exception不為null, 則會處理異常, 對於ModelAndViewDefiningException型別的異常單獨處理, 對於其他型別的異常, 轉交給processHandlerException方法處理, 這個方法就是異常處理邏輯的核心.

@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
        @Nullable Object handler, Exception ex) throws Exception {

    // Success and error responses may use different content types
    request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

    // 使用註冊的Resolver處理
    ModelAndView exMv = null;
    if (this.handlerExceptionResolvers != null) {
        for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
            exMv = resolver.resolveException(request, response, handler, ex);
            if (exMv != null) {
                break;
            }
        }
    }
    if (exMv != null) {
        if (exMv.isEmpty()) {
            request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
            return null;
        }
        // We might still need view name translation for a plain error model...
        if (!exMv.hasView()) {
            String defaultViewName = getDefaultViewName(request);
            if (defaultViewName != null) {
                exMv.setViewName(defaultViewName);
            }
        }
        if (logger.isTraceEnabled()) {
            logger.trace("Using resolved error view: " + exMv, ex);
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Using resolved error view: " + exMv);
        }
        WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
        return exMv;
    }

    throw ex;
}

從上面程式碼可以看到, this.handlerExceptionResolvers就是在程式啟動時初始化註冊的, spring通過遍歷Resolver列表的方式處理異常, 如果返回結果不為null, 說明處理成功, 就跳出迴圈.

總結

Spring的異常解析器實現全部繼承自介面ResponseStatusExceptionResolver, 上面我們詳細瞭解了該介面在Spring中的幾種實現, 比如處理預定義異常的DefaultHandlerExceptionResolver, 可以對映異常到狀態碼的ResponseStatusExceptionResolver, 還有功能更為強大的ExceptionHandlerExceptionResolver. 同時也簡單瞭解了其使用方式,使用@ExceptionHandler來將方法標記為異常處理器, 結合@ControllerAdvice處理全域性異常.

最後我們探究了異常處理器的載入和處理方式, 我們知道了其通過 DispatcherServlet 的初始化方法initHandlerMappings完成載入器列表的註冊初始化, 並且在具體處理請求的doDispatch中檢測異常, 最終processDispatchResult方法委託給processHandlerException, 該方法迴圈註冊的異常處理器列表完成處理過程.

相關文章