1 概述
在JavaWeb階段我們學習了Filter過濾器,提出Filter的概念一開始我們為了過濾字符集亂碼,在Servlet體系中攔截目標請求,而攔截器是在SpringMVC中定義的概念名叫HandlerInteceptor。
在開發過程中,使用攔截器的配置更為靈活,其API接口更豐富,他們的目的都可以達到對請求的前置和后置處理,其本質上區別不大,但由于攔截器可以被Spring容器管理,從而獲得被容器賦予的能力,而filter功能單一,所以后期大家都習慣使用攔截器完成某項特定的功能。
2 過濾器(Filter)
2.1 過濾器定義
一個實現了特殊接口(Filter)的Java類,實現對請求資源(jsp、servlet、html)的過濾功能。過濾器是一個運行在服務器的程序,優先于請求資源(jsp、servlet、html)之前執行, 過濾器是javaweb技術中最為實用的技術之一。
2.2 過濾器作用
Filter的作用是對目標資源(servlet、jsp)進行過濾,其應用場景有:登錄權限檢查,解決網站亂碼,過濾敏感字符等等。
Filter 接口中定義了三個方法:
- init() :該方法在容器啟動初始化過濾器時被調用,它在 Filter 的整個生命周期只會被調用一次,這個方法必須執行成功,否則過濾器會不起作用。
- doFilter() :容器中的每一次請求都會調用該方法, FilterChain 用來調用下一個過濾器 Filter。
- destroy(): 當容器銷毀 過濾器實例時調用該方法,一般在方法中銷毀或關閉資源,在過濾器 Filter 的整個生命周期也只會被調用一次。
2.3 過濾器實現
@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 后置");
}
}
3 攔截器 (Interceptor)
3.1 攔截器定義
攔截器它是鏈式調用,一個應用中可以同時存在多個攔截器Interceptor, 一個請求也可以觸發多個攔截器 ,而每個攔截器的調用會依據它的聲明順序依次執行。
3.2 攔截器的核心API
SpringMVC攔截器提供三個方法分別是preHandle、postHandle、afterCompletion,我們就是通過這幾個方法來對用戶的請求進行攔截處理的。
- preHandle() :這個方法將在請求處理之前進行調用。「注意」:如果該方法的返回值為false ,將視為當前請求結束,不僅自身的攔截器會失效,還會導致其他的攔截器也不再執行。
- postHandle():只有在 preHandle() 方法返回值為true 時才會執行。會在Controller 中的方法調用之后,DispatcherServlet 返回渲染視圖之前被調用。「有意思的是」:postHandle() 方法被調用的順序跟 preHandle() 是相反的,先聲明的攔截器 preHandle() 方法先執行,而postHandle()方法反而會后執行。
- afterCompletion():只有在 preHandle() 方法返回值為true 時才會執行,在整個請求結束之后, DispatcherServlet 渲染了對應的視圖之后執行。
3.3 攔截器的實現
SpringMVC攔截器有兩種實現方式:
第一種方式是要定義的Interceptor類要實現了Spring的HandlerInterceptor 接口;
第二種方式是繼承實現了HandlerInterceptor接口的類,比如Spring已經提供的實現了HandlerInterceptor接口的抽象類HandlerInterceptorAdapter;
以下是實現一個登錄攔截過程
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
public class CommonInterceptor extends HandlerInterceptorAdapter{
private final Logger log = LoggerFactory.getLogger(CommonInterceptor.class);
public static final String LAST_PAGE = "lastPage";
/**
* 在業務處理器處理請求之前被調用
* 如果返回false
* 從當前的攔截器往回執行所有攔截器的afterCompletion(),再退出攔截器鏈
*
* 如果返回true
* 執行下一個攔截器,直到所有的攔截器都執行完畢
* 再執行被攔截的Controller
* 然后進入攔截器鏈,
* 從最后一個攔截器往回執行所有的postHandle()
* 接著再從最后一個攔截器往回執行所有的afterCompletion()
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
if ("GET".equalsIgnoreCase(request.getMethod())) {
RequestUtil.saveRequest();
}
log.info("==============執行順序: 1、preHandle================");
String requestUri = request.getRequestURI();
String contextPath = request.getContextPath();
String url = requestUri.substring(contextPath.length()); if ("/userController/login".equals(url)) {
return true;
}else {
String username = (String)request.getSession().getAttribute("user");
if(username == null){
log.info("Interceptor:跳轉到login頁面!");
request.getRequestDispatcher("/page/index.html").forward(request, response);
return false;
}else
return true;
}
}
/**
* 在業務處理器處理請求執行完成后,生成視圖之前執行的動作
* 可在modelAndView中加入數據,比如當前時間
*/
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
log.info("==============執行順序: 2、postHandle================");
if(modelAndView != null){ //加入當前時間
modelAndView.addObject("haha", "測試postHandle");
}
}
/**
* 在DispatcherServlet完全處理完請求后被調用,可用于清理資源等
* 當有攔截器拋出異常時,會從當前攔截器往回執行所有的攔截器的afterCompletion()
*/
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
log.info("==============執行順序: 3、afterCompletion================");
}
}
spring-MVC.xml的相關配置
<!--配置攔截器, 多個攔截器,順序執行 -->
<mvc:interceptors>
<mvc:interceptor>
<!--
/**的意思是所有文件夾及里面的子文件夾
/*是所有文件夾,不含子文件夾
/是web項目的根目錄
-->
<mvc:mapping path="/**" />
<!-- 需排除攔截的地址 -->
<!-- <mvc:exclude-mapping path="/userController/login"/> -->
<bean id="commonInterceptor" class="org.atguigu.interceptor.CommonInterceptor"></bean> <!--這個類就是我們自定義的Interceptor -->
</mvc:interceptor>
<!-- 當設置多個攔截器時,先按順序調用preHandle方法,然后逆序調用每個攔截器的postHandle和afterCompletion方法 -->
</mvc:interceptors>
web.xml
<!-- 不攔截靜態文件 -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/js/*</url-pattern>
<url-pattern>/css/*</url-pattern>
<url-pattern>/images/*</url-pattern>
<url-pattern>/fonts/*</url-pattern>
</servlet-mapping>
以上代碼也可以在springmvc.xml 這樣寫
<!-- 對靜態資源文件的訪問-->
<mvc:resources mapping="/images/**" location="/images/"/>
<mvc:resources mapping="/css/**" location="/css/" />
<mvc:resources mapping="/js/**" location="/js/" />
<mvc:resources mapping="/favicon.ico" location="favicon.ico" />
通過以上兩個對Filter和Interceptor的配置,我們大致也能上手寫項目,下面就針對他們的主要區別展開敘述
4 過濾器和攔截器的區別
過濾器和攔截器均體現了AOP的編程思想,都可以實現諸如日志,登錄鑒權等功能,但二者的不同點也是比較多的
- 攔截器是基于Java的反射機制,而過濾器是基于函數回調
- 攔截器不依賴與Servlet容器,而過濾器依賴Servlet容器
- 攔截器只能對Controller請求起作用,而過濾器可以對幾乎所有請求起作用
- 攔截器可以訪問Controller上下文,值棧里的對象,而過濾器不能
- 在Spring容器的生命周期中,攔截器可以多次調用,而過濾器只能在容器初始化時被調用一次
4.1 實現原理不同
過濾器和攔截器底層實現方式大不相同,過濾器是基于函數回調的,攔截器則是基于Java的反射機制(動態代理)實現的。
在我們自定義的過濾器中都會實現一個 doFilter()方法,這個方法有一個FilterChain 參數,而實際上它是一個回調接口。ApplicationFilterChain是它的實現類, 這個實現類內部也有一個 doFilter() 方法就是回調方法。
public interface FilterChain {
void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}
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);
}
4.2 使用范圍不同
我們看到過濾器 實現的是 javax.servlet.Filter 接口,而這個接口是在Servlet規范中定義的,也就是說過濾器Filter 的使用要依賴于Tomcat等容器,導致它只能在web程序中使用。
而攔截器(Interceptor) 它是一個Spring組件,并由Spring容器管理,并不依賴Tomcat等容器,是可以單獨使用的。不僅能應用在web程序中,也可以用于Application、Swing等程序中。
4.3 觸發時機不同
過濾器 和 攔截器的觸發時機也不同,我們看下邊這張圖。
過濾器Filter是在請求進入容器后,但在進入servlet之前進行預處理,請求結束是在servlet處理完以后。
攔截器 Interceptor 是在請求進入servlet后,在進入Controller之前進行預處理的,Controller 中渲染了對應的視圖之后請求結束。
4.4 攔截的請求范圍不同
在上邊我們已經同時配置了過濾器和攔截器,再建一個Controller接收請求測試一下。
@Controller
@RequestMapping()
public class Test {
@RequestMapping("/test1")
@ResponseBody
public String test1(String a) {
System.out.println("我是controller");
return null;
}
}
過濾器
@Autowired
private TestService testService;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Filter 處理中");
filterChain.doFilter(servletRequest, servletResponse);
}
攔截器
@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 后置");
}
}
項目啟動過程中發現,過濾器的init()方法,隨著容器的啟動進行了初始化
此時瀏覽器發送請求,F12 看到居然有兩個請求,一個是我們自定義的 Controller 請求,另一個是訪問靜態圖標資源的請求。
看到控制臺的打印日志如下:
執行順序 :Filter 處理中 -> Interceptor 前置 -> 我是controller -> Interceptor 處理中 -> Interceptor 處理后
Filter 處理中
Interceptor 前置
我是controller
Interceptor 處理中
Interceptor 后置
Filter 處理中
過濾器Filter執行了兩次,攔截器Interceptor只執行了一次。這是因為過濾器幾乎可以對所有進入容器的請求起作用,而攔截器只會對Controller中請求或訪問static目錄下的資源請求起作用。
4.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(); // 調用service方法
filterChain.doFilter(servletRequest, servletResponse);
}
攔截器中
@Component
public class MyInterceptor implements HandlerInterceptor {
// @Autowired
// private TestService testService;
@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 {
// testService.a();
System.out.println("Interceptor 處理中");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("Interceptor 后置");
}
}
結果:
Filter 處理中
我是方法A
Interceptor 前置
我是controller
Interceptor 處理中
Interceptor 后置
但是在攔截器中注入service ,發起請求測試,竟然會報錯,別急,原因是加載順序導致的問題
@Component
public class MyInterceptor implements HandlerInterceptor {
@Autowired
private TestService testService;
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
testService.a();
System.out.println("Interceptor 處理中");
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("Interceptor 前置");
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("Interceptor 后置");
}
}
攔截器加載的時間點是SpringContext之前,而Bean又是由Spring容器管理的, 所以在當然不能獲取到bean對象
解決辦法:
我們在注冊攔截器之前,手動將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("/**");
}
}
這樣就可以在MyInterceptor攔截器類中使用service的bean
4.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() 方法執行的順序相反。