Spring統一異常處理器無法處理非法請求異常

1.問題描述

Forwarding to error page from request [/aaa/bbb/ccc/bookhistory/] due to exception [賬號未登錄] com.xxx.web.base.exception.BusinessException: 賬號未登錄
at com.xxx.web.core.interceptor.LoginInterceptor.preHandle(LoginInterceptor.java:20)
at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:136)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:986)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:622)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:291)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.logging.log4j.web.Log4jServletFilter.doFilter(Log4jServletFilter.java:71)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at com.sankuai.oceanus.http.filter.InfFilter.doFilter(InfFilter.java:94)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at com.dianping.merchant.common.filter.ShopAccountBizContextFilter.doFilter(ShopAccountBizContextFilter.java:98)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at com.dianping.cat.servlet.CatFilter.logTransaction(CatFilter.java:302)
at com.dianping.cat.servlet.CatFilter.doFilter(CatFilter.java:86)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:109)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:130)
at org.springframework.boot.web.servlet.support.ErrorPageFilter.access$000(ErrorPageFilter.java:66)
at org.springframework.boot.web.servlet.support.ErrorPageFilter$1.doFilterInternal(ErrorPageFilter.java:105)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:123)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:521)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1096)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:674)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1500)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1456)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)

對登錄校驗,做了一個攔截器,統一校驗,對於未登錄用戶拋出自定義異常,然後使用Spring的統一異常處理統一處理,然而還是報錯了。

2.具體問題

正常來說,哪怕用戶未登錄,也不是拋出異常,而是友好提示,如:項目中攔截器拋出的未登錄異常,正常來說是會被程序中的異常統一處理器,統一處理:

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(value = BusinessException.class)
    @ResponseBody
    public RespNVO<String> businessExceptionHandle(HttpServletRequest request,BusinessException e){
        return RespNVO.error(e.getCode(),e.getMessage());
    }
    //...略過部分代碼
}

爲什麼這個異常未被統一處理器處理掉?原因在於請求路徑有問題:這個請求是非法請求URL,例如“/aaa/bbb/ccc/bookhistory/”,項目中並沒有對應的請求處理方法,我們來看Spring對於這種請求拋出的異常是如何處理的:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;

        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

        try {
            ModelAndView mv = null;
            Exception dispatchException = null;

            try {
                processedRequest = checkMultipart(request);
                multipartRequestParsed = (processedRequest != request);

                // 獲取當前請求路徑的處理handler:mappedHandler,包括具體處理該請求的handler以及對該請求其作用的相關攔截器
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }

                // 獲取具體處理請求的適配器
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

                // Process last-modified header, if supported by the handler.
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (logger.isDebugEnabled()) {
                        logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                    }
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }
                //處理攔截器前置請求,本次在這裏會拋出異常,由下方代碼catch住
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // 處理請求
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                applyDefaultViewName(processedRequest, mv);
        //攔截器後置處理
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
            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
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        catch (Exception ex) {
            triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
        }
        catch (Throwable err) {
            triggerAfterCompletion(processedRequest, response, mappedHandler,
                    new NestedServletException("Handler processing failed", err));
        }
        finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                // Instead of postHandle and afterCompletion
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            }
            else {
                // Clean up any resources used by a multipart request.
                if (multipartRequestParsed) {
                    cleanupMultipart(processedRequest);
                }
            }
        }
    }

1.如果處理請求中未拋出異常,則走的是非法請求路徑的處理方法:

非法請求路徑的時候,mappedHandler的值:

   HandlerExecutionChain with handler [ResourceHttpRequestHandler [locations=[class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/], ServletContext resource [/]], resolvers=[org.springframework.web.servlet.resource.PathResourceResolver@433ab5fd]]] and 3 interceptors

其中ResourceHttpRequestHandler是Spring對http請求靜態資源的一種描述(六個資源路徑:META-INF/resources/、resources/、static/、public/、/,以及默認的[]),由PathResourceResolver負責尋找對應路徑資源。

如果資源未找到,則跳到springboot默認錯誤頁面(如果未配置對應的錯誤跳轉頁面的話):404:Whitelabel Error Page(具體見ErrorMvcAutoConfiguration)

對於正常請求,返回的mappedHandler的具體值如:

   HandlerExecutionChain with handler [com.dianping.joy.common.resource.vo.RespNVO<com.dianping.joy.category.merchant.web.biz.fitness.vo.CoachListVO> com.dianping.joy.category.merchant.web.biz.fitness.controller.CoachReadController.fetchCoachList(java.lang.Integer,java.lang.Integer)] and 4 interceptors

其handler類型爲HandlerMethod,其值包含相應的處理方法相關信息。

   handler的類型對之後的異常處理流程是有影響的,詳見後續分析。

2.請求中拋出異常:看代碼知道,有異常的情況,異常會先被暫存在dispatchException,並傳到processDispatchResult方法中進行處理:

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);
            }
        }

        // Did the handler return a view to render?
        if (mv != null && !mv.wasCleared()) {
            render(mv, request, response);
            if (errorView) {
                WebUtils.clearErrorRequestAttributes(request);
            }
        }
        else {
            if (logger.isDebugEnabled()) {
                logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
                        "': assuming HandlerAdapter completed request handling");
            }
        }

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

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

processDispatchResult邏輯:有異常,進行異常處理邏輯:ModelAndViewDefiningException與其他異常處理。這裏此次請求即會進入processHandlerException方法內,進行異常處理流程:

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

        // Check registered HandlerExceptionResolvers...
        ModelAndView exMv = null;
        if (this.handlerExceptionResolvers != null) {
            for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
                exMv = handlerExceptionResolver.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.isDebugEnabled()) {
                logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex);
            }
            WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
            return exMv;
        }

        throw ex;
    }

具體異常處理,由handlerExceptionResolvers(List),即一系列的異常處理器處理,內置的有:DefaultErrorAttributes(緩存錯誤信息,供後面錯誤視圖使用)以及HandlerExceptionResolverComposite(其內同樣包含一個List,是一種有序的異常處理器)。

對於HandlerExceptionResolverComposite,其內置的異常處理器有如下三個:

第一個,ExceptionHandlerExceptionResolver,即上面提到的統一處理器入口。

第二個,ResponseStatusExceptionResolver,需要配合ResponseStatus使用,故本次情況可以忽略。

第三個,DefaultHandlerExceptionResolver,默認的異常處理器,是Spring提供的對默認一些異常的處理方法,比如常見的TypeMismatchException、MethodArgumentNotValidException、BindException等處理。

不管是第一個還是第三個異常處理器,其具體處理流程由AbstractHandlerExceptionResolver(是二者的抽象實現父類)實現:

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

        if (shouldApplyTo(request, handler)) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Resolving exception from handler [" + handler + "]: " + ex);
            }
            prepareResponse(ex, response);
            ModelAndView result = doResolveException(request, response, handler, ex);
            if (result != null) {
                logException(ex, request);
            }
            return result;
        }
        else {
            return null;
        }
    }

這裏的重點是shouldApplyTo以及對應的doResolveException(具體實現由實現類實現)方法,shouldApplyTo這個方法是對以上內置的三個異常處理器是否可以處理本次請求異常的判定,具體判定,內置實現有兩種:AbstractHandlerExceptionResolver以及其實現抽象類AbstractHandlerMethodExceptionResolver,其中上面說到的ExceptionHandlerExceptionResolver的直接抽象類爲AbstractHandlerMethodExceptionResolver,而第三個DefaultHandlerExceptionResolver直接抽象類是AbstractHandlerExceptionResolver(具體類圖見最後附錄):

//AbstractHandlerExceptionResolver
protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
    //請求處理的handler是否爲空
        if (handler != null) {
      //是否有手動設置的對應處異常理器
            if (this.mappedHandlers != null && this.mappedHandlers.contains(handler)) {
                return true;
            }
      //是否有手動異常處理類
            if (this.mappedHandlerClasses != null) {
                for (Class<?> handlerClass : this.mappedHandlerClasses) {
                    if (handlerClass.isInstance(handler)) {
                        return true;
                    }
                }
            }
        }
        // 默認二者均爲空,即所有handler均可進入異常處理
        return (this.mappedHandlers == null && this.mappedHandlerClasses == null);
    }
//AbstractHandlerMethodExceptionResolver
protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
        if (handler == null) {
      //爲空,走默認判定流程
            return super.shouldApplyTo(request, null);
        }
        else if (handler instanceof HandlerMethod) {
      //如果是HandlerMethod類型
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            handler = handlerMethod.getBean();
            return super.shouldApplyTo(request, handler);
        }
        else {
            return false;
        }
    }

AbstractHandlerExceptionResolver的shouldApplyTo方法,默認是所有handler發生異常時均可進入異常處理流程的。

對於AbstractHandlerMethodExceptionResolver來說,若handler類型是HandlerMethod,會將請求處理類的bean當做handler放入異常處理判定中。

對於本次請求,handler類型爲ResourceHttpRequestHandler,第一個內置異常處理器ExceptionHandlerExceptionResolver,shouldApplyTo返回false(所以,非法請求的異常是不會被統一異常處理器處理的),第三個返回true,即進入默認的異常處理器,而默認處理器中並沒有處理本次自定義異常類型(BusinessException),故最終流程會走到processHandlerException方法的最後:重新拋出該異常。異常被再次拋出後將無後續異常處理方法,故前端收到的是後端拋出的異常代碼。

3.結論

   對於非法請求,自定義的統一異常處理器無法處理其內發生的異常。

附錄:

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章