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