深入解析:Spring中Filter與Interceptor的區別及正確用法
自從我們開始使用 Spring,我們經常聽到過濾器(Filter)和攔截器(Interceptor)。然而,當真正需要使用它們時,可能會對它們的區別和相似點感到困惑。產生這種困惑的主要原因是它們的用途相似(例如,授權檢查、日志處理、數據壓縮/解壓等)。
使用過濾器可以實現的場景同樣可以用攔截器實現,因此它們的邊界變得模糊不清。為了解釋它們的差異和相似之處,我們將深入探討兩者的起源和設計理念。
本文基于 SpringBoot 2.7.5 版本進行講解。
過濾器:外來引入的概念
基本概念
仔細研究源代碼,我們會發現過濾器的概念實際上是從 Servlet 引入的外來概念,它遵循 Servlet 規范??梢钥匆幌?Filter 類的全限定名稱:
javax.servlet.Filter
可以看出,Filter 用于Tomcat 等 Web 容器中的 Servlet 相關處理,而并非 Spring 原生的工具。這一發現有助于我們理解為什么 Spring 中的過濾器和攔截器具有相似的功能。
由于它們分別由不同的作者為各自的系統創建,因此出現了類似的思想和實現方法也是可以理解的。畢竟,英雄所見略同。
隨后,Spring 引入并兼容了 Tomcat 容器的處理邏輯,使得兩個相似的概念可以存在于同一應用上下文中(注意,Spring 并沒有將它們合并,而只是使其兼容),這也導致開發人員容易產生困惑。
為了更好地理解 Filter 的作用,讓我們引入官方的注釋進行說明:
過濾器是一個對象,它可以對對資源的請求(如 servlet 或靜態內容)或資源的響應或兩者執行過濾任務。
從這個定義中,我們可以提取兩條有用的信息:
- 執行時機:Filter 的執行時機有兩個,在請求處理前和在響應返回前。
- 執行內容:過濾器本質上執行的是過濾任務,而過濾條件基于對資源的請求或對資源的響應。
除了上述信息外,結合 Tomcat 中 Servlet 容器的結構設計,我們可以推導出 Filter 的執行流程圖:
圖片
在實際開發場景中,資源請求的預處理或資源響應的后處理可能并不限于單一類型的過濾任務。
因此,Tomcat 設計中使用了責任鏈模式來處理需要多種不同類型過濾器處理請求或響應的場景。
這一概念也體現在前面提到的流程圖中。需要注意的是,由于采用了線性數據結構(鏈結構),在實際的過濾器操作過程中存在固有的執行順序。這意味著在實現自定義過濾器時,必須確保過濾器之間不存在依賴反轉。
當然,如果過濾器之間沒有依賴關系,那么執行順序就不是問題。Tomcat 使用 org.apache.catalina.core.ApplicationFilterChain 來實現上述的責任鏈模式??梢酝ㄟ^以下代碼更好地理解這一概念:
publicfinalclassApplicationFilterChainimplementsFilterChain{
publicvoiddoFilter(ServletRequest request,ServletResponse response)
throwsIOException,ServletException{
if(Globals.IS_SECURITY_ENABLED){
finalServletRequest req = request;
finalServletResponse res = response;
try{
java.security.AccessController.doPrivileged(
(java.security.PrivilegedExceptionAction<Void>)()->{
// 實際執行過濾操作
internalDoFilter(req,res);
returnnull;
}
);
}catch(PrivilegedActionException pe){
...
}
}else{
// 實際執行過濾操作
internalDoFilter(request,response);
}
}
privatevoidinternalDoFilter(ServletRequest request,
ServletResponse response)
throwsIOException,ServletException{
// 如果存在下一個過濾器,則調用它
if(pos < n){
ApplicationFilterConfig filterConfig = filters[pos++];
try{
Filter filter = filterConfig.getFilter();
...
if(Globals.IS_SECURITY_ENABLED){
...
}else{
// 結合 Filter 類進行分析,實際上是執行回調函數,
// 該方法的第三個參數傳遞了當前的 applicationFilterChain 對象,結合上面的 pos 指針確定過濾鏈是否已完全執行
filter.doFilter(request, response,this);
}
}catch(IOException|ServletException|RuntimeException e){
throw e;
}catch(Throwable e){
...
}
return;
}
// 執行到鏈的末端——調用 servlet 實例
try{
...
// 實際執行 servlet 服務,注意這僅是進入 servlet 實例,而未真正進入具體處理器
servlet.service(request, response);
...
}catch(IOException|ServletException|RuntimeException e){
throw e;
}catch(Throwable e){
...
}finally{
...
}
}
}
從上述代碼可以看出,Tomcat 使用 pos 指針來記錄過濾器鏈中過濾器的執行位置。只有在鏈中的所有過濾器都執行完畢并通過后,request 和 response 對象才會提交給 servlet 實例進行相應的服務處理。
需要注意的是,此時尚未涉及具體的 handler,意味著過濾器的處理無法細化到具體處理器類的請求/響應,而只能較為模糊地處理整個 servlet 實例級別的請求/響應。
當然,從上述代碼中還可以看出一個問題,即似乎僅對資源請求進行過濾處理,而沒有對資源響應進行過濾處理。
實際上,資源響應的過濾處理隱藏在每個過濾器的 doFilter 方法中。當實現自定義過濾器時,需要遵循以下邏輯來處理資源響應:
@Override
publicvoiddoFilter(ServletRequest request,ServletResponse response,FilterChain chain)throwsIOException,ServletException{
// TODO 前置處理
// 調用 applicationFilterChain 對象的 doFilter 方法(這實際上是回調邏輯)。必須包含這一步,否則鏈式結構會在此處中斷。
chain.doFilter(request, response);
// TODO 后置處理
}
結合 ApplicationFilterChain 中的 internalDoFilter 方法,可以發現隱含的入棧和出棧邏輯(本質上是方法堆棧)。資源請求的前置處理實際上是一個入棧過程,當所有前置處理過濾器入棧完畢后,servlet.service(request, response) 開始執行。
在 servlet 服務處理完成后,出棧過程開始,逐個按順序執行后置處理邏輯,直至方法結束退出。
必須指出,這種邏輯對初學者來說不太友好。由于 Filter 只是一個接口,無法像抽象類那樣提供模板方法,初學者在沒有參考示例的情況下可能很難使用,若只是查看源碼可能會有類似疑問。
還要提醒大家,實現自定義過濾器時必須遵循上述模板,否則可能會導致鏈式流程被破壞或后置邏輯無法實現。
在 Spring 中的使用
雖然提到了 Spring,但這里實際討論的是 Spring Boot 中的使用方法。要在 Spring Boot 中實現自定義過濾器,只需添加注入邏輯將其放入 Spring 容器。Spring Boot 提供了兩種方式來完成此操作:
- 在自定義過濾器上使用 @Component 注解;
- 在自定義過濾器上使用 @WebFilter 注解,并在啟動類上使用 @ServletComponentScan 注解;
推薦使用第二種方法注入過濾器,因為 Spring 提供了 Tomcat 原生處理不具備的額外功能,即 URL 匹配功能。
結合 @WebFilter 注解中的 urlPattern 字段,Spring 能進一步細化過濾器處理的粒度,使開發者更靈活。此外,可通過 Spring 提供的 @Order 注解來自定義過濾器的注入順序。
攔截器:Spring 原生功能
基本概念
探討完過濾器后,我們將目光轉向攔截器。此時發現,攔截器的概念源自 Spring,對應的接口類為 HandlerInterceptor(還有一個異步攔截器接口類,此處不展開,有興趣的同學可自行閱讀源碼)。
查看相應源碼后發現,HandlerInterceptor 提供了三個與執行時機相關的方法,而不同于 Filter 僅提供一個簡單的 doFilter 方法:
- preHandle:在執行相應處理程序之前執行,進行前置處理;
- postHandle:在請求處理完成后但在渲染 ModelAndView 對象之前執行,進行與 ModelAndView 對象相關的后置處理;
- afterCompletion:在渲染 ModelAndView 對象后且在返回響應前執行,對結果進行后置處理。
與 Filter 類僅提供的 doFilter 方法相比,HandlerInterceptor 的方法定義更為精準和易用。無需閱讀源碼或參考示例,便可大致猜測如何實現自定義攔截器。
結合 org.springframework.web.servlet.DispatcherServlet#doDispatch 的源碼,可以繪制出以下流程圖(此處不貼出具體代碼,有興趣的同學可自行查看):
圖片
可以看到,攔截器的執行邏輯全部包含在 servlet 實例中。結合前述過濾器的執行流程說明,不難發現過濾器就像夾心餅干的兩片餅干,將 servlet 和攔截器包在中間,攔截器的執行時機在過濾器前置處理之后、后置處理之前。
此外,通過閱讀源碼還可發現,Spring 在使用攔截器時同樣使用了責任鏈模式。在不同任務和邏輯需順序執行的場景中,這種模式十分有用。
需要注意的是,由于 Spring 在設計攔截器時已明確定義了不同階段的方法,因此攔截器的實際執行過程并未采用與過濾器相同的推棧和彈棧方式。
在 Spring 中的使用
要在 Spring Boot 中使用攔截器,除了實現 HandlerInterceptor 接口外,還需要顯式地在 Spring 的 Web 配置中進行注冊,如下所示:
@Configuration
publicclassWebConfigimplementsWebMvcConfigurer{
@Override
publicvoidaddInterceptors(InterceptorRegistry registry){
registry.addInterceptor(newDemoInterceptor()).addPathPatterns("/api/*").excludePathPatterns("/api/ok");
}
}
從上述代碼可以看到,Spring 也為自定義攔截器提供了與過濾器相同的路徑匹配功能。借助該功能,自定義攔截器可以更細致地處理請求和響應。這一點再次重疊了過濾器的功能,但這當然是 Spring 內部提供的功能。
常見使用場景
確實,在文章開頭我們已介紹了一些兩者的功能。這里再簡單總結一下。
從以上分析可以看出,過濾器和攔截器的設計初衷是將請求的前置處理和響應的后置處理從業務代碼中分離出來,作為通用處理邏輯供開發者擴展實現。這一設計思想類似于 AOP。
在實際開發中,自定義過濾器或攔截器常用于實現以下操作:
- 用戶登錄驗證;
- 權限檢查;
- 日志攔截;
- 數據壓縮/解壓;
- 加解密處理;
- …
這里不展示各場景的編碼實現,有興趣的同學可以自行搜索學習。
一點建議:雖然上述場景看似繁多,但其實本質都是在處理請求參數或響應結果。理解這一點后,設計和實現這些場景就會相對容易。
總結
通過以上分析可見,過濾器和攔截器在Spring Boot中的核心區別在于執行時機、應用場景及使用便捷性。過濾器圍繞請求的全流程運行,適合系統級通用邏輯處理(如數據壓縮、編碼設置),而攔截器位于控制器層面,更適合業務邏輯擴展(如權限校驗、日志記錄)。
設計上,過濾器通過“推入-彈出”機制延續過濾鏈,邏輯較為復雜;而攔截器提供明確的接口方法,執行流程更為直觀。此外,二者均采用職責鏈模式,體現AOP思想,幫助實現請求的分層處理。
因此,開發中根據需求選擇工具即可:系統級處理優先過濾器,業務級處理優先攔截器。