關(guān)于SpringMVC攔截器執(zhí)行兩遍的原因分析以及如何解決

最近在項(xiàng)目中碰到了如題的問(wèn)題,在spring-boot項(xiàng)目中,同一次http請(qǐng)求,HandlerInterceptor攔截器執(zhí)行了兩次,與此同時(shí)這個(gè)問(wèn)題還有個(gè)特點(diǎn),它并沒(méi)有干擾具體的業(yè)務(wù)功能,就是controller正常返回,沒(méi)有任何錯(cuò)誤。

什么情況下HandlerInterceptor會(huì)執(zhí)行兩遍?

并不是所有的controller都是這樣的,經(jīng)過(guò)測(cè)試目前發(fā)現(xiàn)有以下兩種的controller會(huì)出現(xiàn)這樣的情況(前提是你沒(méi)有重寫過(guò)它的RequestMappingHandlerAdapterViewNameMethodReturnValueHandler)。

@Controller
public class GreetingController {

    /**
     * 第一種情況:方法返回值類型為 void 類型,并且不存在RequestMapping對(duì)應(yīng)的視圖
     *
     * @param name
     * @throws IOException
     */
    @RequestMapping("/greet1")
    public void greet1(String name) throws IOException {
        System.out.println("name = " + name);
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        HttpServletResponse response = ((ServletRequestAttributes) requestAttributes).getResponse();
        PrintWriter writer = response.getWriter();
        writer.write(name);
        writer.flush();
        writer.close();
    }


    /**
     * 第二種情況:方法返回值類型為 String 類型,并且不存在返回內(nèi)容對(duì)應(yīng)的視圖
     *
     * @param name
     * @return
     */
    @RequestMapping("/greet2")
    public String greet2(String name) {
        System.out.println("name = " + name);
        return name;
    }

}

對(duì)于以上兩種情況,你所有的HandlerInterceptor都至少會(huì)執(zhí)行兩遍,甚至三遍。這里先給出怎么解決這個(gè)問(wèn)題的方案,后面再分析問(wèn)題原因。

如何解決HandlerInterceptor攔截器執(zhí)行多次問(wèn)題?

重寫RequestMappingHandlerAdapterViewNameMethodReturnValueHandler這兩個(gè)類,并將其注入容器中

public class CustomizedHandlerAdapter extends RequestMappingHandlerAdapter {

    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        setReturnValueHandlers(getReturnValueHandlers().stream().filter(
                h -> h.getClass() != ViewNameMethodReturnValueHandler.class
        ).collect(Collectors.toList()));
    }
}

public class HandlerVoidMethod extends ViewNameMethodReturnValueHandler {

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        /*
         * 這里只處理了void返回值的方法,對(duì)于String返回值的方法則沒(méi)有處理,原因是系統(tǒng)中可能還會(huì)用到springmvc的視圖功能(例如jsp)
         * 如果說(shuō)是前后分離的項(xiàng)目,springmvc層只提供純接口的話,那么可以將下面代碼全部刪除,
         * 只寫上一行  mavContainer.setRequestHandled(true);  即可
         */
        if (void.class == returnType.getParameterType()) {
            mavContainer.setRequestHandled(true);//這行代碼是重點(diǎn),它的作用是告訴其他組件本次請(qǐng)求已經(jīng)被程序內(nèi)部處理完畢,可以直接放行了
        } else {
            super.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
        }
    }
}

@Configuration
public class WebConfig {

    @Bean
    public HandlerVoidMethod handlerVoidMethod() {
        return new HandlerVoidMethod();
    }

    @Bean
    public CustomizedHandlerAdapter handlerAdapter(HandlerVoidMethod handlerVoidMethod) {
        CustomizedHandlerAdapter chl = new CustomizedHandlerAdapter();
        chl.setCustomReturnValueHandlers(Arrays.asList(handlerVoidMethod));
        return chl;
    }
}

通過(guò)以上代碼,則能解決mvc攔截器執(zhí)行多次的問(wèn)題。

springmvc攔截器為什么會(huì)執(zhí)行多次?

簡(jiǎn)單來(lái)講就是controller中的void方法會(huì)導(dǎo)致springmvc使用你的請(qǐng)求url作為視圖名稱,然后它在渲染視圖之前會(huì)檢查你的視圖名稱,發(fā)現(xiàn)這視圖會(huì)導(dǎo)致循環(huán)請(qǐng)求,就拋出一個(gè)ServletException,tomcat截取到這個(gè)異常后就轉(zhuǎn)發(fā)到/error頁(yè)面,就在這個(gè)轉(zhuǎn)發(fā)的過(guò)程中導(dǎo)致了springmvc重新開(kāi)始DispatcherServlet的整個(gè)流程,所以攔截器自然就執(zhí)行了多次。

HandlerInterceptor相關(guān)知識(shí)

對(duì)于攔截器HandlerInterceptor的機(jī)制需要有個(gè)大致的了解,這里簡(jiǎn)單講下springmvc中的攔截器是怎么執(zhí)行的,我們都知道springmvc中處理請(qǐng)求都是從DispatcherServletdoDispatch方法開(kāi)始的,

// DispatcherServlet類 doDispatch方法
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    ......省略部分前面的代碼,直接從 1035行這里開(kāi)始

    // HandlerExecutionChain mappedHandler = getHandler(processedRequest); 在1016行有設(shè)置mappedHandler的值,HandlerExecutionChain是一個(gè)攔截器調(diào)用鏈,它是鏈?zhǔn)綀?zhí)行的,這里是鏈?zhǔn)綀?zhí)行所有的攔截器里面的 preHandle 方法
    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
        return;
    }

    // 真正代用我們自己寫的業(yè)務(wù)代碼的入口
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

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

    applyDefaultViewName(processedRequest, mv);
    // 這里是鏈?zhǔn)綀?zhí)行所有的攔截器里面的 postHandle 方法
    mappedHandler.applyPostHandle(processedRequest, response, mv);

    ......省略后面的代碼
}

多個(gè)攔截器它的執(zhí)行順序是和棧的入棧出棧順序有點(diǎn)類似,我們把preHandle方法比作入棧,postHandle方法比作出棧,所以就是preHandle先執(zhí)行的postHandle反而后執(zhí)行。

例如我們有三個(gè)攔截器,分別為 A,B,C。它們的執(zhí)行順序如下圖:

image-20200607182601897.png

攔截器執(zhí)行的相關(guān)源碼:

    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HandlerInterceptor[] interceptors = getInterceptors();//獲取所有的攔截器
        if (!ObjectUtils.isEmpty(interceptors)) {
            for (int i = 0; i < interceptors.length; i++) {// 這里是從第一個(gè)開(kāi)始
                HandlerInterceptor interceptor = interceptors[i];
                if (!interceptor.preHandle(request, response, this.handler)) {
                    triggerAfterCompletion(request, response, null);
                    return false;
                }
                this.interceptorIndex = i;
            }
        }
        return true;
    }

    void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
        HandlerInterceptor[] interceptors = getInterceptors();//獲取所有的攔截器
        if (!ObjectUtils.isEmpty(interceptors)) {
            for (int i = interceptors.length - 1; i >= 0; i--) {// 這里是從最后一個(gè)開(kāi)始
                HandlerInterceptor interceptor = interceptors[i];
                interceptor.postHandle(request, response, this.handler, mv);
            }
        }
    }

上面簡(jiǎn)要的講了下攔截器從哪里開(kāi)始以及它的執(zhí)行順序相關(guān)的東西,經(jīng)過(guò)了攔截器的前置攔截之后,springmvc通過(guò)反射執(zhí)行了我們的具體業(yè)務(wù)方法,那在執(zhí)行具體的業(yè)務(wù)方法時(shí)有兩個(gè)很重要的問(wèn)題,一是如何處理我們業(yè)務(wù)方法的參數(shù)(千奇百怪的);二是如何處理我們業(yè)務(wù)方法的返回值(也是多種多樣的)。在springmvc中通過(guò)HandlerMethodArgumentResolver來(lái)處理方法參數(shù),通過(guò)HandlerMethodReturnValueHandler來(lái)處理方法返回值。在本文中,方法的入?yún)⑴c本文所討論的問(wèn)題關(guān)系不大,因此這里就不展開(kāi)敘述HandlerMethodArgumentResolver相關(guān)的東西了。重點(diǎn)說(shuō)下與問(wèn)題相關(guān)的HandlerMethodReturnValueHandler類。

HandlerMethodReturnValueHandler

對(duì)于controller方法的返回值的處理,springmvc框架中內(nèi)置了20多種默認(rèn)的返回值處理器,在RequestMappingHandlerAdapter#getDefaultReturnValueHandlers方法中可以看到它設(shè)置的一些默認(rèn)的HandlerMethodReturnValueHandler

    private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
        List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>();

        // Single-purpose return value types
        // 返回值類型是ModelAndView或其子類
        handlers.add(new ModelAndViewMethodReturnValueHandler());
        // 返回值類型是Model或其子類
        handlers.add(new ModelMethodProcessor());
        // 返回值類型是View或其子類
        handlers.add(new ViewMethodReturnValueHandler());

        // ResponseBody注解
        handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(), this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager));
        handlers.add(new StreamingResponseBodyReturnValueHandler());

        // 用來(lái)處理返回值類型是HttpEntity的方法
        handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice));
        handlers.add(new HttpHeadersReturnValueHandler());
        handlers.add(new CallableMethodReturnValueHandler());
        handlers.add(new DeferredResultMethodReturnValueHandler());
        handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));

        // Annotation-based return value types
        // 返回值有@ModelAttribute注解
        handlers.add(new ModelAttributeMethodProcessor(false));
        handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice));

        // Multi-purpose return value types
        // 返回值是void或String, 將返回的字符串作為view視圖的名字
        handlers.add(new ViewNameMethodReturnValueHandler());
        // 返回值類型是Map
        handlers.add(new MapMethodProcessor());

        // Custom return value types,自定義返回值處理
        if (getCustomReturnValueHandlers() != null) {
            handlers.addAll(getCustomReturnValueHandlers());
        }

        // Catch-all
        if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) {
            handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
        }
        else {
            handlers.add(new ModelAttributeMethodProcessor(true));
        }
        return handlers;
    }

在本文的問(wèn)題中,controller的返回值為void和String兩種都有,剛好對(duì)應(yīng)ViewNameMethodReturnValueHandler這個(gè)處理器

public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValueHandler {

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        Class<?> paramType = returnType.getParameterType();
        // 返回值類型匹配,void和String
        return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
    }

    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
            ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        // 返回值處理這里,只處理了String類型的返回值,將返回值的結(jié)果作為視圖名稱設(shè)置到ModelAndViewContainer對(duì)象中
        // 對(duì)于void類型的返回值,這里并沒(méi)有處理,
        if (returnValue instanceof CharSequence) {
            String viewName = returnValue.toString();
            mavContainer.setViewName(viewName);
            if (isRedirectViewName(viewName)) {
                mavContainer.setRedirectModelScenario(true);
            }
        }
        else if (returnValue != null) {
            // should not happen
            throw new UnsupportedOperationException("Unexpected return type: " +
                    returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
        }
    }
}

這里有個(gè)小細(xì)節(jié),handleReturnValue方法中returnValue參數(shù)前面是加了一個(gè)@Nullable注解的,意味這這個(gè)參數(shù)的值可能是null,當(dāng)你的controller為void方法時(shí),returnValue就會(huì)為null,那handleReturnValue這個(gè)方法就不會(huì)對(duì)mavContainer這個(gè)對(duì)象做任何處理。

ModelAndView對(duì)象的流轉(zhuǎn)過(guò)程

mavContainer這個(gè)參數(shù)是從RequestMappingHandlerAdapter中的invokeHandlerMethod方法中創(chuàng)建并一路傳進(jìn)來(lái)的,整個(gè)調(diào)用鏈如下圖:

image-20200607193526861.png

最終在DispatcherServletdoDispatch方法中得到上圖中最后返回的ModelAndView對(duì)象

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
                ......省略部分前面的代碼
                // Actually invoke the handler.
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

                ......省略部代碼
                    
                // 直接看這里,設(shè)置默認(rèn)視圖
                applyDefaultViewName(processedRequest, mv);
    
                ......省略部分前面的代碼
}

private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
    // 這里判斷是否設(shè)置了視圖,通過(guò)前面的分析,我們可以知道,由于我們的controller是void類型的,所以是沒(méi)有設(shè)置視圖的
    if (mv != null && !mv.hasView()) {
        // 獲取默認(rèn)視圖名稱
        String defaultViewName = getDefaultViewName(request);
        if (defaultViewName != null) {
            // 設(shè)置默認(rèn)視圖名稱
            mv.setViewName(defaultViewName);
        }
    }
}

獲取默認(rèn)視圖名稱的方法:getDefaultViewName,由于這個(gè)方法里面調(diào)用棧比較深,這里直接給出它調(diào)用的最里面的那個(gè)方法:

org.springframework.web.util.UrlPathHelper#getPathWithinApplication

    public String getPathWithinApplication(HttpServletRequest request) {
        String contextPath = getContextPath(request);
        String requestUri = getRequestUri(request);
        String path = getRemainingPath(requestUri, contextPath, true);
        if (path != null) {
            // Normal case: URI contains context path.
            return (StringUtils.hasText(path) ? path : "/");
        }
        else {
            return requestUri;
        }
    }

最后這個(gè)方法其實(shí)就是獲取了controller的requestMapping,然后返回出去。再結(jié)合前面的getDefaultViewName方法可知,這個(gè)默認(rèn)視圖名稱就是requestMapping的值。在渲染視圖之前springmvc還做了個(gè)判斷,就是看你的視圖名稱是不是本次請(qǐng)的uri中的一部分或者和uri一樣,如果是的話,就會(huì)拋出一個(gè)異常,說(shuō)你是一個(gè)循環(huán)視圖路徑

image.png

tomcat對(duì)錯(cuò)誤頁(yè)面的處理

在tomcat的StandardHostValve類中,它獲取到了上面springmvc拋出的ServletException異常,它寫了個(gè)很明了的注釋,尋找一個(gè)應(yīng)用級(jí)別的錯(cuò)誤頁(yè)面,如果存在的話則渲染它(就是重定向到錯(cuò)誤頁(yè)面)

image.png

通過(guò)debug直到執(zhí)行到下面的代碼,通過(guò)下圖中第二行后面的debug信息也可以看到錯(cuò)誤頁(yè)面的路徑為/error

image.png

通過(guò)RequestDispatcher這個(gè)類名能猜到應(yīng)該是請(qǐng)求分發(fā)器,看看它是如何創(chuàng)建RequestDispatcher對(duì)象的

    public RequestDispatcher getRequestDispatcher(final String path) {
        ......省略部代碼
      
        try {     
            ......省略部代碼

            // Construct a RequestDispatcher to process this request
            // 創(chuàng)建一個(gè)新的請(qǐng)求調(diào)度器來(lái)處理該請(qǐng)求
            return new ApplicationDispatcher(wrapper, uri, wrapperPath, pathInfo,
                    queryString, mapping, null);
        } finally {          
            mappingData.recycle();
        }
    }
image.png

這里可以很明顯的看到開(kāi)始了請(qǐng)求轉(zhuǎn)發(fā),這對(duì)于springmvc來(lái)講就已經(jīng)開(kāi)始了一個(gè)新的請(qǐng)求,它會(huì)再次進(jìn)入到DispatcherServletdoDispatch方法中,整個(gè)springmvc的流程會(huì)再重新走一遍,所以攔截器自然也會(huì)再執(zhí)行一次,只不過(guò)這次在攔截器中看到的url已經(jīng)變成/error了,而不是之前的requestMapping里面的值。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容