過濾器 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";
}
}
複製程式碼
啟動專案,可以看到我們的過濾器已經隨著程式的啟動被成功初始化了
分別對這兩個介面傳送請求:
最後使專案停止執行,則過濾器隨之銷燬
什麼是 Filter 的鏈式呼叫?
當我們配置了多個 filter,且一個請求能夠被多次攔截時,該請求將沿著 客戶端 -> 過濾器1 -> 過濾器2 -> servlet -> 過濾器2 -> 過濾器1 -> 客戶端
鏈式流轉,如下圖所示 :
以上面的程式碼為例,由於我們只定義了一個過濾器,在執行到 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銷燬中...");
}
}
複製程式碼
執行結果如下:
可以看出,當請求同時滿足多個過濾器的過濾條件時,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";
}
}
複製程式碼
執行結果如下 :
其他注意事項
在 Http 的請求執行過程中,要經過以下幾個步驟 :
- 由 DispatcherServlet 捕獲請求
- DispatcherServlet 將接收到的 URL 和對應的 Controller 進行對映
- 在請求到達相應的 Controller 之前,由攔截器對請求進行處理
- 處理完成之後,進行檢視的解析
- 返回檢視
所以,只有經過 DispatcherServlet 的請求才會被攔截器捕獲,而我們自定義的 Servlet 請求則不會被攔截的。
過濾器與攔截器
作為AOP思想的兩種典型實現,過濾器與攔截器有著許多相似的地方。而兩者最大的區別在於 :過濾器是在 Servlet 規範中定義的,是由 Servlet 容器支援的;攔截器是在 Spring 容器內的,由 Spring 框架支援。
因此,作為 Spring 的一個元件,攔截器可以通過IOC容器進行管理,獲取其中的各個 bean 例項,對 spring 中的各種資源、物件,如 Service 物件、資料來源、事務管理等進行呼叫;而過濾器則不能。
總的來說,兩者主要在如下方面存在著差異 :
- 過濾器是基於函式的回撥,而攔截器是基於 Java 反射機制的
- 過濾器可以修改 request,而攔截器則不能
- 過濾器需要在 servlet 容器中實現,攔截器可以適用於 JavaEE、JavaSE 等各種環境
- 攔截器可以呼叫 IOC 容器中的各種依賴,而過濾器不能
- 過濾器只能在請求的前後使用,而攔截器可以詳細到每個方法
最後補兩張圖嘿嘿 :
-
filter、servlet、interceptor 觸發時機
過濾器的觸發時機是在容器後,servlet 之前,所以過濾器的doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
的入參是 ServletRequest,而不是 HttpServletRequest,因為過濾器是在 HttpServlet 之前。 -
過濾器攔截器執行先後步驟
第二步中,SpringMVC 的機制是由 DispaterServlet 來分發請求給不同的 Controller,其實這一步是在 Servlet 的 service() 方法中執行的。