spring中的過濾器與攔截器

yuanLier發表於2019-11-10

過濾器 Filter

什麼是過濾器?

與 Servlet 相似,過濾器是一些 web 應用程式元件,可以繫結到一個 web 應用程式中。但是與其他 web 應用元件不同的是,過濾器是“鏈”在容器的處理過程中的。這就意味著它們可以在請求達到 servlet 之前對其進行訪問,也可以在響應資訊返回到客戶端之前對其進行攔截。這種訪問使得過濾器可以檢查並修改請求和響應的內容。

Filter 的介面方法有哪些?

init :

Filter 的初始化,在 Servlet 容器建立過濾器例項的時候呼叫,以確保過濾器能夠正常工作。在 init() 方法執行過程中遇到如下問題時,web 容器將不會配置其可用 :

  • 丟擲 ServletException
  • 沒有在 web 容器設定的時間內返回

doFilter :

Filter 的核心方法,用於對每個攔截到的請求做一些設定好的操作。

典型用途如下:

  • 在 HttpServletRequest 到達 Servlet 之前,攔截客戶的 HttpServletRequest。
  • 根據需要檢查 HttpServletRequest ,也可以修改 HttpServletRequest 頭和資料。
  • 在 HttpServletResponse 到達客戶端之前,攔截 HttpServletResponse。
  • 根據需要檢查 HttpServletResponse,可以修改 HttpServletResponse 頭和資料。

destory :

Filter 的銷燬,在 Servlet 容器銷燬過濾器例項時呼叫,以釋放其佔用的資源。只有在 doFilter() 方法中的所有執行緒退出或超時後,web 容器才會呼叫此方法。

如何實現一個自己的 Filter

首先我們需要建立一個類,讓它實現 Filter 介面,然後重寫介面中的方法:

package com.demo.demofilter.demofilter.filter;

import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Component
@Order(1)   // 過濾順序,值越小越先執行
@WebFilter(urlPatterns = "/demoFilter", filterName = "filterTest")
public class DemoFilter 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("\ndoFilter()開始執行:發往 " + ((HttpServletRequest) servletRequest).getRequestURL().toString() + " 的請求已被攔截");

        System.out.println("檢驗介面是否被呼叫,嘗試獲取contentType如下: " + servletResponse.getContentType());
        // filter的鏈式呼叫;將請求轉給下一條過濾鏈
        filterChain.doFilter(servletRequest, servletResponse);
        System.out.println("檢驗介面是否被呼叫,嘗試獲取contentType如下: " + servletResponse.getContentType());

        System.out.println("doFilter()執行結束。\n");
    }

    @Override
    public void destroy() {
        System.out.println("filter銷燬中...");
    }
}
複製程式碼

然後建立一個 Controller,對外提供兩條請求路徑:

package com.demo.demofilter.demofilter.filter;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("demoFilter")
public class FilterController {

    @GetMapping("hello")
    public String hello() {
        System.out.println("介面被呼叫:hello() ");
        return "hello filter";
    }

    @GetMapping("hi")
    public String hi() {
        System.out.println("介面被呼叫:hi()");
        return "hi filter";
    }
}
複製程式碼

啟動專案,可以看到我們的過濾器已經隨著程式的啟動被成功初始化了

spring中的過濾器與攔截器

分別對這兩個介面傳送請求:

spring中的過濾器與攔截器

最後使專案停止執行,則過濾器隨之銷燬

spring中的過濾器與攔截器

什麼是 Filter 的鏈式呼叫?

當我們配置了多個 filter,且一個請求能夠被多次攔截時,該請求將沿著 客戶端 -> 過濾器1 -> 過濾器2 -> servlet -> 過濾器2 -> 過濾器1 -> 客戶端 鏈式流轉,如下圖所示 :

spring中的過濾器與攔截器

以上面的程式碼為例,由於我們只定義了一個過濾器,在執行到 filterChain.doFilter(servletRequest, servletResponse); 的時候,請求就會被直接轉送到 servlet 中進行呼叫。

所以我們需要稍微給它改造一下,看看再新增一個 DemoFilter2 會發生什麼:

package com.demo.demofilter.demofilter.filter;

import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Component
@Order(2)   // 過濾順序,值越小越先執行,值相同或不指定時按filterName排序
// 注意這裡的urlPatterns要與前面保持一致
@WebFilter(urlPatterns = "/demoFilter", filterName = "filterTest2") 
public class DemoFilter2 implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("filter2初始化中...");
    }

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

        System.out.println("\ndoFilter2()開始執行:發往 " + ((HttpServletRequest) servletRequest).getRequestURL().toString() + " 的請求已被攔截");

        System.out.println("檢驗介面是否被呼叫,嘗試獲取contentType如下: " + servletResponse.getContentType());
        filterChain.doFilter(servletRequest, servletResponse);
        System.out.println("檢驗介面是否被呼叫,嘗試獲取contentType如下: " + servletResponse.getContentType());

        System.out.println("doFilter2()執行結束。\n");
    }

    @Override
    public void destroy() {
        System.out.println("filter2銷燬中...");
    }
}
複製程式碼

執行結果如下:

spring中的過濾器與攔截器

可以看出,當請求同時滿足多個過濾器的過濾條件時,filterChain.doFilter() 會將其按一定順序(可以通過 @Order 指定)依次傳遞到下一個 filter,直到進入 servlet 進行介面的實際呼叫。呼叫完成後,響應結果將沿著原路返回,並在再一次經過各個 filter 後,最終抵達客戶端。

攔截器 Interceptor

什麼是攔截器?

攔截器是 AOP 的一種實現策略,用於在某個方法或欄位被訪問前對它進行攔截,然後在其之前或之後加上某些操作。同 filter 一樣,interceptor 也是鏈式呼叫。每個 interceptor 的呼叫會依據它的宣告順序依次執行。一般來說攔截器可以用於以下方面 :

  • 日誌記錄 :機率請求資訊的日誌,以便進行資訊監控、資訊統計等等
  • 許可權檢查 :對使用者的訪問許可權,認證,或授權等進行檢查
  • 效能監控 :通過攔截器在進入處理器前後分別記錄開始時間和結束時間,從而得到請求的處理時間
  • 通用行為 :讀取 cookie 得到使用者資訊並將使用者物件放入請求頭中,從而方便後續流程使用

攔截器的介面方法有哪些?

preHandler:

方法的前置處理,將在請求處理之前被呼叫。一般用它來對方法進行一些前置初始化操作,或是對當前請求做一些預處理;此外也可以用來進行許可權校驗之類的判斷,來決定請求是否要繼續進行下去。

該方法返回一個布林值,若該值為 false,則請求結束,後續的 Interceptor 和 Controller 都不會再執行;若該值為 true,則會繼續呼叫下一個 Interceptor 的 preHandler() 方法,如果已經到達最後一個 interceptor 了,就會呼叫當前請求的 Controller。

postHandler:

方法的後置處理,將在請求處理之後被呼叫。雖然是在 Controller 方法呼叫後再執行,但它的呼叫依然在 DispatcherServlet 進行檢視渲染並返回之前,所以一般可以通過它對 Controller 處理之後的 ModelAndView 物件進行操作。

afterCompletion:

在整個請求處理完成(包括檢視渲染)後執行,主要用來進行一些資源的清理工作。

如何實現一個自己的攔截器

同樣,首先建立一個類,讓它實現 HandlerInterceptor 介面,然後重寫介面中的方法 :

package com.demo.demofilter.demofilter.interceptor;

import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class DemoInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        System.out.println("afterHandle");
    }
}
複製程式碼

緊接著需要對攔截器進行註冊,指明使用哪個攔截器,及該攔截器對應攔截的 URL :

package com.demo.demofilter.demofilter.interceptor;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 如果有多個攔截器,繼續registry.add往下新增就可以啦
        registry.addInterceptor(new DemoInterceptor()).addPathPatterns("/demoInterceptor/**");
    }

}
複製程式碼

最後是 Controller

package com.demo.demofilter.demofilter.interceptor;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("demoInterceptor")
public class InterceptorController {

    @GetMapping("hello")
    public String hello() {
        System.out.println("介面被呼叫:hello() ");
        return "hello interceptor";
    }

    @GetMapping("hi")
    public String hi() {
        System.out.println("介面被呼叫:hi()");
        return "hi interceptor";
    }
}
複製程式碼

執行結果如下 :

spring中的過濾器與攔截器

其他注意事項

在 Http 的請求執行過程中,要經過以下幾個步驟 :

  1. 由 DispatcherServlet 捕獲請求
  2. DispatcherServlet 將接收到的 URL 和對應的 Controller 進行對映
  3. 在請求到達相應的 Controller 之前,由攔截器對請求進行處理
  4. 處理完成之後,進行檢視的解析
  5. 返回檢視

所以,只有經過 DispatcherServlet 的請求才會被攔截器捕獲,而我們自定義的 Servlet 請求則不會被攔截的。

過濾器與攔截器

作為AOP思想的兩種典型實現,過濾器與攔截器有著許多相似的地方。而兩者最大的區別在於 :過濾器是在 Servlet 規範中定義的,是由 Servlet 容器支援的;攔截器是在 Spring 容器內的,由 Spring 框架支援。

因此,作為 Spring 的一個元件,攔截器可以通過IOC容器進行管理,獲取其中的各個 bean 例項,對 spring 中的各種資源、物件,如 Service 物件、資料來源、事務管理等進行呼叫;而過濾器則不能。

總的來說,兩者主要在如下方面存在著差異 :

  • 過濾器是基於函式的回撥,而攔截器是基於 Java 反射機制的
  • 過濾器可以修改 request,而攔截器則不能
  • 過濾器需要在 servlet 容器中實現,攔截器可以適用於 JavaEE、JavaSE 等各種環境
  • 攔截器可以呼叫 IOC 容器中的各種依賴,而過濾器不能
  • 過濾器只能在請求的前後使用,而攔截器可以詳細到每個方法

最後補兩張圖嘿嘿 :

  1. filter、servlet、interceptor 觸發時機

    spring中的過濾器與攔截器
    過濾器的觸發時機是在容器後,servlet 之前,所以過濾器的 doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) 的入參是 ServletRequest,而不是 HttpServletRequest,因為過濾器是在 HttpServlet 之前。

  2. 過濾器攔截器執行先後步驟

    spring中的過濾器與攔截器
    第二步中,SpringMVC 的機制是由 DispaterServlet 來分發請求給不同的 Controller,其實這一步是在 Servlet 的 service() 方法中執行的。

相關文章