Spring Boot 之HandlerInterceptor

源代碼可從這里下載。

什么是HandlerInterceptor?

HandlerInterceptor是支持自定義handler執(zhí)行鏈的工作流接口。應(yīng)用程序可以為handler注冊任意數(shù)量的已有或自定義攔截器,以添加常見的預(yù)處理行為,而無需修改每個handler實現(xiàn)。通俗點講就是支持在handler執(zhí)行前后執(zhí)行自定義邏輯。

HandlerInterceptor能干什么?

從HandlerInterceptor工作的機制來分析HandlerInterceptor可以完成哪些工作,又對哪些工作無能為力。

HandlerInterceptor執(zhí)行流程

HandlerInterceptor三個方法:preHandle、postHandle和afterCompletion的執(zhí)行時間點如下:

preHandle:在HandlerMapping確定使用哪個Handler處理請求之后,HandlerAdapter調(diào)用Handler之前,在該階段HandlerInterceptor可以修改Request和Response。

postHandle:在HandlerAdapter調(diào)用Handler之后,DispatcherServlet渲染視圖之前。實際上在調(diào)用postHandler之前HandlerAdapter已經(jīng)完成Response寫并提交,因此postHandler無法修改Response。如果有修改Response的場景,可以使用ResponseBodyAdvice接口。

afterCompletion:請求處理完成之后調(diào)用,適合做一些資源清理工作。

使用HandlerInterceptor的基本原則:與業(yè)務(wù)相關(guān)的細(xì)粒度任務(wù)適合首選HandlerInterceptor。下面是一些可以選擇使用HandlerInterceptor的場景:

  • 鑒權(quán),可以使用HandlerInterceptor但是Filter是更佳的選擇。
  • 審計日志,記錄每一個請求。
  • Token解析或校驗。
  • Handler執(zhí)行時間統(tǒng)計。

自定義HandlerInterceptor

自定義一個攔截器需要實現(xiàn)org.springframework.web.servlet.HandlerInterceptor接口,HandlerInterceptor 接口實現(xiàn)可以有選擇的覆寫下面三個方法:

  • preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    throws Exception
  • postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
    @Nullable ModelAndView modelAndView) throws Exception
  • afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception

說明:上面的三個方法HandlerInterceptor接口已經(jīng)提供默認(rèn)實現(xiàn),如果HandlerInterceptor的實現(xiàn)沒有覆寫方法,則該HandlerInterceptor不會做任何事情。

第一個HandlerInterceptor

下面定義一個僅記錄各方法被調(diào)用時間點的簡單HandlerInterceptor。

HandlerInterceptor:

@Slf4j
public class FirstHandlerInterceptor implements HandlerInterceptor {
    private String name;

    FirstHandlerInterceptor(String name) {
        this.name = name == null ? getClass().getName() : name;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        log.info("enter {} interceptor pre handle method at {}", name, System.currentTimeMillis());
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           @Nullable ModelAndView modelAndView) {
        log.info("enter {} interceptor post handle method at {}", name, System.currentTimeMillis());
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                @Nullable Exception ex) {
        log.info("enter {} interceptor after completion method at {}", name, System.currentTimeMillis());
    }
}

注冊HandlerInterceptor

在Spring Boot中使用WebMvcConfigurer可以非常方便的注冊HandlerInterceptor。下面是一個簡單注冊HandlerInterceptor的樣例代碼:

@EnableWebMvc
@Configuration
public class InterceptorConfigure implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new FirstHandlerInterceptor("FIRST"));
    }
}

其中注解@Configuration@EnableWebMvc 是必須的。

測試HandlerInterceptor

啟動Spring Boot并調(diào)用一個測試接口,測試接口可從這里獲取。

curl -i http://localhost:8080/user/login

應(yīng)用的輸出如下:

enter FIRST interceptor pre handle method at 1583651101064
enter FIRST interceptor post handle method at 1583651101068
enter FIRST interceptor after completion method at 1583651101068

從輸出可以看出三個方法的調(diào)用順序未:preHandle -> postHandle -> afterCompltion

HandlerInterceptor的執(zhí)行順序

和Filter不同,HandlerInterceptor沒有order屬性可以用來標(biāo)記HandlerInterceptor的執(zhí)行順序,HandlerInterceptor的執(zhí)行順序和注冊HandlerInterceptor的順序保持一致。修改上面InterceptorConfigure的代碼以注冊多個HandlerInterceptor:

@EnableWebMvc
@Configuration
public class InterceptorConfigure implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new FirstHandlerInterceptor("FIRST"));
        registry.addInterceptor(new FirstHandlerInterceptor("SECOND"));
    }
}

啟動Spring Boot并調(diào)用一個測試接口,測試接口可從這里獲取。

curl -i http://localhost:8080/user/login

應(yīng)用的輸出如下:

enter FIRST interceptor pre handle method at 1583651310410
enter SECOND interceptor pre handle method at 1583651310410
enter SECOND interceptor post handle method at 1583651310453
enter FIRST interceptor post handle method at 1583651310453
enter SECOND interceptor after completion method at 1583651310453
enter FIRST interceptor after completion method at 1583651310453

HandlerInterceptor的調(diào)用順序未FIRST HandlerInterceptor - > SECOND HandlerInterceptor。多個HandlerInterceptor的執(zhí)行流程示意圖:

多攔截器執(zhí)行流程

異步HandlerInterceptor

AsyncHandlerInterceptor是HandlerInterceptor的子接口,可在異步請求處理后調(diào)用。當(dāng)Handler啟動處理一個異步請求時,DispatcherServlet退出之前不會調(diào)用postHandle和afterCompletion兩個方法(同步請求DispatcherServlet退出前會調(diào)用),因為可能尚未準(zhǔn)備好請求處理的結(jié)果,并且有可能結(jié)果在另外一個線程中產(chǎn)生。在這種場景,DispatcherServlet將改為調(diào)用afterConcurrentHandlingStarted方法,以支持實現(xiàn)在釋放線程之前清理與線程相關(guān)的資源等類似的任務(wù)。

異步處理完成后,請求將分發(fā)到容器以進行進一步處理。在此階段,DispatcherServlet調(diào)用preHandle,postHandle和afterCompletion。攔截器可以通過檢查ServletRequestjavax.servlet.DispatcherType屬性來區(qū)分請求是原始請求還是異步處理之后再次分發(fā)的請求,"REQUEST" 表示原始請求, "ASYNC"表示異步處理之后的請求。

在異步請求超時或網(wǎng)絡(luò)錯誤導(dǎo)致請求完成的場景,HandlerInterceptor的實現(xiàn)需要繼續(xù)完成本來的工作。但是在這種情況下,Servlet容器不會分發(fā)請求,因此也不會調(diào)用postHandle和afterCompletion兩個方法,相反可以使用WebAsyncManager的registerDeferredResultInterceptor和registerCallbackInterceptor方法注冊攔截器以跟蹤異步請求。

同步、異步攔截器樣例

樣例實現(xiàn)下面三個功能:

1、同步攔截器向HttpRequest添加屬性:Service-Transition-Id

2、異步攔截器獲取Service-Transition-Id屬性并打印

3、實現(xiàn)異步Request,并驗證攔截器的調(diào)用屬性。

AsyncHandlerInterceptor

@Slf4j
public class SelfAsyncHandlerInterceptor implements AsyncHandlerInterceptor {
    private String name;

    SelfAsyncHandlerInterceptor(String name) {
        this.name = name;
    }

    @Override
    public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response,
                                               Object handler) {
        log.info("enter {} interceptor", this.name);
        log.info("Service-Transition-Id = {}", request.getAttribute("Service-Transition-Id"));
    }
}

Async Request

@GetMapping("/user/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
    DeferredResult<String> deferredResult = new DeferredResult<String>();
    executor.execute(() -> deferredResult.setResult("Hello Async Request"));
    return deferredResult;
}

測試攔截器:

啟動Spring Boot應(yīng)用,調(diào)用異步接口:

curl -i http://localhost:8080/user/quotes

應(yīng)用輸出:

enter FIRST interceptor pre handle method at 1583658644588
enter async interceptor interceptor pre handle method at 1583658644589
enter quotes
enter async interceptor interceptor
Service-Transition-Id = 2317252d-dade-4ca1-848a-d9653848f8f0
enter FIRST interceptor pre handle method at 1583658644605
enter async interceptor interceptor pre handle method at 1583658644605
enter async interceptor interceptor post handle method at 1583658644621
enter FIRST interceptor post handle method at 1583658644621
enter async interceptor interceptor after completion method at 1583658644622
enter FIRST interceptor after completion method at 1583658644622

從應(yīng)用輸出可以歸納出異步攔截器和同步攔截器一起工作時(同步攔截器先注冊)方法的執(zhí)行順序如下:

同步異步攔截器

參考:

1、Spring API

2、Spring Documentation

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

相關(guān)閱讀更多精彩內(nèi)容

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