1. 程式人生 > >Spring Boot2(七):攔截器和過濾器

Spring Boot2(七):攔截器和過濾器

一、前言

過濾器和攔截器兩者都具有AOP的切面思想,關於aop切面,可以看上一篇文章。過濾器filter和攔截器interceptor都屬於面向切面程式設計的具體實現。

二、過濾器

過濾器工作原理

從上圖可以看出,當瀏覽器傳送請求到伺服器時,先執行過濾器,然後才訪問Web資源。伺服器響應Response,從Web資源抵達瀏覽器之前,也會途徑過濾器。

過濾器是一個實現javax.servlet.Filter介面的Java類。javax.servlet.Filter介面定義了三個方法

方法 描述
public void init(FilterConfig filterConfig) web 應用程式啟動時,web 伺服器將建立Filter 的例項物件,並呼叫其init方法,讀取web.xml配置,完成物件的初始化功能,從而為後續的使用者請求作好攔截的準備工作(filter物件只會建立一次,init方法也只會執行一次)。開發人員通過init方法的引數,可獲得代表當前filter配置資訊的FilterConfig物件。
public void doFilter (ServletRequest, ServletResponse, FilterChain) 該方法完成實際的過濾操作,當客戶端請求方法與過濾器設定匹配的URL時,Servlet容器將先呼叫過濾器的doFilter方法。FilterChain使用者訪問後續過濾器。
public void destroy() Servlet容器在銷燬過濾器例項前呼叫該方法,在該方法中釋放Servlet過濾器佔用的資源。

SpringBoot摒棄了繁瑣的xml配置的同時,提示了幾種註冊元件:ServletRegistrationBean,
FilterRegistrationBean,ServletListenerRegistrationBean,DelegatingFilterProxyRegistrationBean,用於註冊自對應的元件,如過濾器,監聽器等。

程式碼實現

1、新增maven依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!--web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--devtools熱部署-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
</dependency>

2、新增攔截器

@Configuration
public class WebConfig {
    @Bean
    public RemoteIpFilter remoteIpFilter() {
        return new RemoteIpFilter();
    }
    
    /**
     * 註冊第三方過濾器
     * 功能與spring mvc中通過配置web.xml相同
     * 可以新增過濾器鎖攔截的 URL,攔截更加精準靈活
     * @return
     */
    @Bean
    public FilterRegistrationBean testFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new MyFilter());
        // 過濾應用程式中所有資源,當前應用程式根下的所有檔案包括多級子目錄下的所有檔案,注意這裡*前有“/”
        registration.addUrlPatterns("/*");
        registration.addInitParameter("paramName", "paramValue");
        registration.setName("MyFilter");
        // 過濾器順序
        registration.setOrder(1);
        return registration;
    }

    // 定義過濾器
    public class MyFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            System.out.println("init");
        }

        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            System.out.println("this is MyFilter,url :" + request.getRequestURI());
            filterChain.doFilter(servletRequest, servletResponse);
        }

        @Override
        public void destroy() {
            System.out.println("destroy");
        }
    }
}

3、controller層

@RestController
public class HelloController {

    @GetMapping("/filter")
    public String testFilter(){
        return "filter is ok";
    }
}

4、測試

通過傳送post請求:127.0.0.1:8081/filter

檢視日誌可以看到過濾器已經開始工作了。

三、攔截器

攔截器概念

不同於過濾器,具體區別我們下面再將,先講一講攔截器實現的機制。

在AOP(Aspect-Oriented Programming)中用於在某個方法或欄位被訪問之前,進行攔截,然後在之前或之後加上某些操作。攔截是AOP的一種實現策略。

攔截器作用

有什麼作用呢?AOP面向切面有什麼作用,那麼攔截器就有什麼作用。

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

攔截器實現

攔截器整合介面HandlerInterceptor,實現攔截,介面方法有下面三種:

  1. preHandler(HttpServletRequest request, HttpServletResponse response, Object handler)
    方法將在請求處理之前進行呼叫。SpringMVC中的Interceptor同Filter一樣都是鏈式呼叫。每個Interceptor的呼叫會依據它的宣告順序依次執行,而且最先執行的都是Interceptor中的preHandle方法,所以可以在這個方法中進行一些前置初始化操作或者是對當前請求的一個預處理,也可以在這個方法中進行一些判斷來決定請求是否要繼續進行下去。該方法的返回值是布林值Boolean 型別的,當它返回為false時,表示請求結束,後續的Interceptor和Controller都不會再執行;當返回值為true時就會繼續呼叫下一個Interceptor 的preHandle 方法,如果已經是最後一個Interceptor 的時候就會是呼叫當前請求的Controller 方法。

  2. postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
    在當前請求進行處理之後,也就是Controller 方法呼叫之後執行,但是它會在DispatcherServlet 進行檢視返回渲染之前被呼叫,所以我們可以在這個方法中對Controller 處理之後的ModelAndView 物件進行操作。

  3. afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)
    該方法也是需要當前對應的Interceptor的preHandle方法的返回值為true時才會執行。顧名思義,該方法將在整個請求結束之後,也就是在DispatcherServlet 渲染了對應的檢視之後執行。這個方法的主要作用是用於進行資源清理工作的。

總結一點就是:

preHandle是請求執行前執行

postHandle是請求結束執行

afterCompletion是檢視渲染完成後執行

程式碼實現

1、新增Maven依賴

和過濾器一樣

2、新增攔截器類

其中LogInterceptor實現HandlerInterceptor介面的三個方法,同時需要preHandle返回true,該方法通常用於清理資源等工作。

主方法繼承WebMvcConfigurer

注意不用用WebMvcConfigurerAdapter,該方法已經被官方標註過時了,在java8是預設實現的。

所以我們需要使用的是WebMvcConfigurer進行靜態資源的配置。

配置的主要有兩項:一個是制定攔截器,第二個是指定攔截的URL

@Slf4j
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    /**
     * 攔截器註冊類
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor()).addPathPatterns("/**");
    }
    /**
     * 定義攔截器
     */
    public class LogInterceptor implements HandlerInterceptor {
        long start = System.currentTimeMillis();

        /**
         * 請求執行前執行
         */
        @Override
        public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
            log.info("preHandle");
            start = System.currentTimeMillis();
            return true;
        }
        /**
         * 請求結束執行
         */
        @Override
        public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
            log.info("Interceptor cost="+(System.currentTimeMillis()-start));
        }
        /**
         * 檢視渲染完成後執行
         */
        @Override
        public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
            log.info("afterCompletion");
        }
    }
}

3、controller層

@RestController
public class HelloController {

    @RequestMapping("/interceptor")
    public String home(){
        return "interceptor is ok";
    }
}

4、測試

可以看到,我們通過攔截器實現了同樣的功能。不過這裡還要說明一點的是,其實這個實現是有問題的,因為preHandle和postHandle是兩個方法,所以我們這裡不得不設定一個共享變數start來儲存開始值,但是這樣就會存線上程安全問題。當然,我們可以通過其他方法來解決,比如通過ThreadLocal就可以很好的解決這個問題,有興趣的同學可以自己實現。不過通過這一點我們其實可以看到,雖然攔截器在很多場景下優於過濾器,但是在這種場景下,過濾器比攔截器實現起來更簡單。

四、過濾器和攔截器的區別

Spring的攔截器與Servlet的Filter有相似之處,比如二者都是AOP程式設計思想的體現,都能實現許可權檢查、日誌記錄等。

不同的是:

  • 使用範圍不同:Filter是Servlet規範規定的,只能用於Web程式中。而攔截器既可以用於Web程式,也可以用於Application、Swing程式中。
  • 規範不同: Filter是在Servlet規範中定義的,是Servlet容器支援的。而攔截器是在Spring容器內的,是Spring框架支援的。
  • 使用的資源不同:同其他的程式碼塊一樣,攔截器也是一個Spring的元件,歸Spring管理,配置在Spring檔案中,因此能使用Spring裡的任何資源、物件,例如Service物件、資料來源、事務管理等,通過loC注入到攔截器即可:而Filter則不能。
  • 深度不同:Filter在只在Servlet前後起作用。而攔截器能夠深入到方法前後、異常丟擲前後等,因此攔截器的使用具有更大的彈性。所以在Spring構架的程式中,要優先使用攔截器。

五、總結

注意:過濾器的觸發時機是容器後,servlet之前,所以過濾器的doFilter(ServletRequest request, ServletResponse response, FilterChain chain)的入參是ServletRequest,而不是HttpServletRequest,因為過濾器是在HttpServlet之前。下面這個圖,可以讓你對Filter和Interceptor的執行時機有更加直觀的認識。

只有經過DispatcherServlet 的請求,才會走攔截器鏈,自定義的Servlet請求是不會被攔截的,比如我們自定義的Servlet地址。

過濾器依賴於Servlet容器,而Interceptor則為SpringMVC的一部分。過濾器能夠攔截所有請求,而Interceptor只能攔截Controller的請求,所以從覆蓋範圍來看,Filter應用更廣一些。但是在Spring逐漸一統Java框架、前後端分離越演越烈,實際上大部分的應用場景,攔截器都可以滿足了。

六、原始碼

SpringBoot-過濾器spring-boot-16-filter

SpringBoot-攔截器spring-boot-17-interceptor

七、參考

SpringBoot實現過濾器、攔截器與切片
Spring Boot實戰:攔截器與過濾器
Spring Boot使用過濾器和攔截器分別實現REST介面簡易安全認