天天看點

過濾器 和 攔截器 6個差別,别再傻傻分不清了

本文收錄在個人部落格: www.chengxy-nds.top ,技術資料共享,同進步

周末有個小夥伴加我微信,向我請教了一個問題:老哥,過濾器 (

Filter

) 和 攔截器 (

Interceptor

) 有啥差別啊? 聽到題目我的第一感覺就是:簡單!

畢竟這兩種工具開發中用到的頻率都相當高,應用起來也是比較簡單的,可當我準備回複他的時候,竟然不知道從哪說起,支支吾吾了半天,場面炒雞尴尬有木有,工作這麼久一個基礎問題答成這樣,丢了大人了。

過濾器 和 攔截器 6個差別,别再傻傻分不清了

平時覺得簡單的知識點,但通常都不會太關注細節,一旦被别人問起來,反倒說不出個是以然來。

歸根結底,還是對這些知識了解的不夠,一直停留在會用的階段,以至于現在一看就會一說就廢!這是典型基礎不紮實的表現,哎·~,其實我也就是個虛胖!

知恥而後勇,下邊結合實踐,更直覺的來感受一下兩者到底有什麼不同?

準備環境

我們在項目中同時配置

攔截器

過濾器

1、過濾器 (Filter)

過濾器的配置比較簡單,直接實作

Filter

接口即可,也可以通過

@WebFilter

注解實作對特定

URL

攔截,看到

Filter

接口中定義了三個方法。

  • init()

    :該方法在容器啟動初始化過濾器時被調用,它在

    Filter

    的整個生命周期隻會被調用一次。注意:這個方法必須執行成功,否則過濾器會不起作用。
  • doFilter()

    :容器中的每一次請求都會調用該方法,

    FilterChain

    用來調用下一個過濾器

    Filter

  • destroy()

    : 當容器銷毀 過濾器執行個體時調用該方法,一般在方法中銷毀或關閉資源,在過濾器

    Filter

    的整個生命周期也隻會被調用一次
@Component
public class MyFilter implements Filter {
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

        System.out.println("Filter 前置");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        System.out.println("Filter 進行中");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

        System.out.println("Filter 後置");
    }
}           

2、攔截器 (Interceptor)

攔截器它是鍊式調用,一個應用中可以同時存在多個攔截器

Interceptor

, 一個請求也可以觸發多個攔截器 ,而每個攔截器的調用會依據它的聲明順序依次執行。

首先編寫一個簡單的攔截器處理類,請求的攔截是通過

HandlerInterceptor

來實作,看到

HandlerInterceptor

接口中也定義了三個方法。

  • preHandle()

    :這個方法将在請求處理之前進行調用。注意:如果該方法的傳回值為

    false

    ,将視為目前請求結束,不僅自身的攔截器會失效,還會導緻其他的攔截器也不再執行。
  • postHandle()

    :隻有在

    preHandle()

    方法傳回值為

    true

    時才會執行。會在Controller 中的方法調用之後,DispatcherServlet 傳回渲染視圖之前被調用。 有意思的是:

    postHandle()

    方法被調用的順序跟

    preHandle()

    是相反的,先聲明的攔截器

    preHandle()

    方法先執行,而

    postHandle()

    方法反而會後執行。
  • afterCompletion()

    preHandle()

    true

    時才會執行。在整個請求結束之後, DispatcherServlet 渲染了對應的視圖之後執行。
@Component
public class MyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        System.out.println("Interceptor 前置");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

        System.out.println("Interceptor 進行中");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

        System.out.println("Interceptor 後置");
    }
}           

将自定義好的攔截器處理類進行注冊,并通過

addPathPatterns

excludePathPatterns

等屬性設定需要攔截或需要排除的

URL

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
        registry.addInterceptor(new MyInterceptor1()).addPathPatterns("/**");
    }
}           

我們不一樣

過濾器 和 攔截器 均展現了

AOP

的程式設計思想,都可以實作諸如日志記錄、登入鑒權等功能,但二者的不同點也是比較多的,接下來一一說明。

1、實作原理不同

過濾器和攔截器 底層實作方式大不相同,

過濾器

是基于函數回調的,

攔截器

則是基于Java的反射機制(動态代理)實作的。

這裡重點說下過濾器!

在我們自定義的過濾器中都會實作一個

doFilter()

方法,這個方法有一個

FilterChain

參數,而實際上它是一個回調接口。

ApplicationFilterChain

是它的實作類, 這個實作類内部也有一個

doFilter()

方法就是回調方法。

public interface FilterChain {
    void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}           
過濾器 和 攔截器 6個差別,别再傻傻分不清了

ApplicationFilterChain

裡面能拿到我們自定義的

xxxFilter

類,在其内部回調方法

doFilter()

裡調用各個自定義

xxxFilter

過濾器,并執行

doFilter()

方法。

public final class ApplicationFilterChain implements FilterChain {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response) {
            ...//省略
            internalDoFilter(request,response);
    }
 
    private void internalDoFilter(ServletRequest request, ServletResponse response){
    if (pos < n) {
            //擷取第pos個filter    
            ApplicationFilterConfig filterConfig = filters[pos++];        
            Filter filter = filterConfig.getFilter();
            ...
            filter.doFilter(request, response, this);
        }
    }
 
}
           

而每個

xxxFilter

會先執行自身的

doFilter()

過濾邏輯,最後在執行結束前會執行

filterChain.doFilter(servletRequest, servletResponse)

,也就是回調

ApplicationFilterChain

doFilter()

方法,以此循環執行實作函數回調。

@Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        filterChain.doFilter(servletRequest, servletResponse);
    }           

2、使用範圍不同

我們看到過濾器 實作的是

javax.servlet.Filter

接口,而這個接口是在

Servlet

規範中定義的,也就是說過濾器

Filter

的使用要依賴于

Tomcat

等容器,導緻它隻能在

web

程式中使用。

過濾器 和 攔截器 6個差別,别再傻傻分不清了

而攔截器(

Interceptor

) 它是一個

Spring

元件,并由

Spring

容器管理,并不依賴

Tomcat

等容器,是可以單獨使用的。不僅能應用在

web

程式中,也可以用于

Application

Swing

等程式中。

過濾器 和 攔截器 6個差別,别再傻傻分不清了

3、觸發時機不同

過濾器

攔截器

的觸發時機也不同,我們看下邊這張圖。

過濾器 和 攔截器 6個差別,别再傻傻分不清了

過濾器

Filter

是在請求進入容器後,但在進入

servlet

之前進行預處理,請求結束是在

servlet

處理完以後。

攔截器

Interceptor

是在請求進入

servlet

後,在進入

Controller

之前進行預處理的,

Controller

中渲染了對應的視圖之後請求結束。

4、攔截的請求範圍不同

在上邊我們已經同時配置了過濾器和攔截器,再建一個

Controller

接收請求測試一下。

@Controller
@RequestMapping()
public class Test {

    @RequestMapping("/test1")
    @ResponseBody
    public String test1(String a) {
        System.out.println("我是controller");
        return null;
    }
}           

項目啟動過程中發現,過濾器的

init()

方法,随着容器的啟動進行了初始化。

過濾器 和 攔截器 6個差別,别再傻傻分不清了

此時浏覽器發送請求,F12 看到居然有兩個請求,一個是我們自定義的

Controller

請求,另一個是通路靜态圖示資源的請求。

過濾器 和 攔截器 6個差別,别再傻傻分不清了

看到控制台的列印日志如下:

執行順序 :

Filter 進行中

->

Interceptor 前置

我是controller

Interceptor 進行中

Interceptor 處理後

Filter 進行中
Interceptor 前置
Interceptor 進行中
Interceptor 後置
Filter 進行中
           

Filter

執行了兩次,攔截器

Interceptor

隻執行了一次。這是因為過濾器幾乎可以對所有進入容器的請求起作用,而攔截器隻會對

Controller

中請求或通路

static

目錄下的資源請求起作用。

5、注入Bean情況不同

在實際的業務場景中,應用到過濾器或攔截器,為處理業務邏輯難免會引入一些

service

服務。

下邊我們分别在過濾器和攔截器中都注入

service

,看看有什麼不同?

@Component
public class TestServiceImpl implements TestService {

    @Override
    public void a() {
        System.out.println("我是方法A");
    }
}           

過濾器中注入

service

,發起請求測試一下 ,日志正常列印出

“我是方法A”

@Autowired
    private TestService testService;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        System.out.println("Filter 進行中");
        testService.a();
        filterChain.doFilter(servletRequest, servletResponse);
    }           
Filter 進行中
我是方法A
Interceptor 前置
我是controller
Interceptor 進行中
Interceptor 後置           

在攔截器中注入

service

,發起請求測試一下 ,竟然TM的報錯了,

debug

跟一下發現注入的

service

怎麼是

Null

啊?

過濾器 和 攔截器 6個差別,别再傻傻分不清了

這是因為加載順序導緻的問題,

攔截器

加載的時間點在

springcontext

之前,而

Bean

又是由

spring

進行管理。

攔截器:老子今天要進洞房;

Spring:兄弟别鬧,你媳婦我還沒生出來呢!

解決方案也很簡單,我們在注冊攔截器之前,先将

Interceptor

手動進行注入。注意:在

registry.addInterceptor()

注冊的是

getMyInterceptor()

執行個體。

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Bean
    public MyInterceptor getMyInterceptor(){
        System.out.println("注入了MyInterceptor");
        return new MyInterceptor();
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(getMyInterceptor()).addPathPatterns("/**");
    }
}           

6、控制執行順序不同

實際開發過程中,會出現多個過濾器或攔截器同時存在的情況,不過,有時我們希望某個過濾器或攔截器能優先執行,就涉及到它們的執行順序。

過濾器用

@Order

注解控制執行順序,通過

@Order

控制過濾器的級别,值越小級别越高越先執行。

@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public class MyFilter2 implements Filter {           

攔截器預設的執行順序,就是它的注冊順序,也可以通過

Order

手動設定控制,值越小越先執行。

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor2()).addPathPatterns("/**").order(2);
        registry.addInterceptor(new MyInterceptor1()).addPathPatterns("/**").order(1);
        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**").order(3);
    }           

看到輸出結果發現,先聲明的攔截器

preHandle()

postHandle()

postHandle()

preHandle()

居然是相反的!如果實際開發中嚴格要求執行順序,那就需要特别注意這一點。

Interceptor1 前置
Interceptor2 前置
Interceptor 前置
我是controller
Interceptor 進行中
Interceptor2 進行中
Interceptor1 進行中
Interceptor 後置
Interceptor2 處理後
Interceptor1 處理後
           

那為什麼會這樣呢? 得到答案就隻能看源碼了,我們要知道

controller

中所有的請求都要經過核心元件

DispatcherServlet

路由,都會執行它的

doDispatch()

方法,而攔截器

postHandle()

preHandle()

方法便是在其中調用的。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    
        try {
         ...........
            try {
           
                // 擷取可以執行目前Handler的擴充卡
                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;
                    }
                }
                // 注意: 執行Interceptor中PreHandle()方法
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // 注意:執行Handle【包括我們的業務邏輯,當抛出異常時會被Try、catch到】
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }
                applyDefaultViewName(processedRequest, mv);

                // 注意:執行Interceptor中PostHandle 方法【抛出異常時無法執行】
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
        }
        ...........
    }
           

看看兩個方法

applyPreHandle()

applyPostHandle()

具體是如何被調用的,就明白為什麼

postHandle()

preHandle()

執行順序是相反的了。

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HandlerInterceptor[] interceptors = this.getInterceptors();
        if(!ObjectUtils.isEmpty(interceptors)) {
            for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
                HandlerInterceptor interceptor = interceptors[i];
                if(!interceptor.preHandle(request, response, this.handler)) {
                    this.triggerAfterCompletion(request, response, (Exception)null);
                    return false;
                }
            }
        }

        return true;
    }           
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
        HandlerInterceptor[] interceptors = this.getInterceptors();
        if(!ObjectUtils.isEmpty(interceptors)) {
            for(int i = interceptors.length - 1; i >= 0; --i) {
                HandlerInterceptor interceptor = interceptors[i];
                interceptor.postHandle(request, response, this.handler, mv);
            }
        }
    }           

發現兩個方法中在調用攔截器數組

HandlerInterceptor[]

時,循環的順序竟然是相反的。。。,導緻

postHandle()

preHandle()

方法執行的順序相反。

過濾器 和 攔截器 6個差別,别再傻傻分不清了

總結

我相信大部分人都能熟練使用濾器和攔截器,但兩者的差别還是需要多了解下,不然開發中使用不當,時不時就會出現奇奇怪怪的問題,以上内容比較簡單,新手學習老鳥複習,有遺漏的地方還望大家積極補充,如有了解錯誤之處,還望不吝賜教。

原創不易,燃燒秀發輸出内容

整理了幾百本各類技術電子書, 送給小夥伴們, 我的同名公衆号自行領取。和一些小夥伴們建了一個技術交流群,一起探讨技術、分享技術資料,旨在共同學習進步,如果感興趣就加入我們吧!