1 概述
在JavaWeb階段我們學習了Filter過濾器,提出Filter的概念一開始我們?yōu)榱诉^濾字符集亂碼,在Servlet體系中攔截目標請求,而攔截器是在SpringMVC中定義的概念名叫HandlerInteceptor。
在開發(fā)過程中,使用攔截器的配置更為靈活,其API接口更豐富,他們的目的都可以達到對請求的前置和后置處理,其本質(zhì)上區(qū)別不大,但由于攔截器可以被Spring容器管理,從而獲得被容器賦予的能力,而filter功能單一,所以后期大家都習慣使用攔截器完成某項特定的功能。
2 過濾器(Filter)
2.1 過濾器定義
一個實現(xiàn)了特殊接口(Filter)的Java類,實現(xiàn)對請求資源(jsp、servlet、html)的過濾功能。過濾器是一個運行在服務器的程序,優(yōu)先于請求資源(jsp、servlet、html)之前執(zhí)行, 過濾器是javaweb技術中最為實用的技術之一。
2.2 過濾器作用
Filter的作用是對目標資源(servlet、jsp)進行過濾,其應用場景有:登錄權限檢查,解決網(wǎng)站亂碼,過濾敏感字符等等。
Filter 接口中定義了三個方法:
- init() :該方法在容器啟動初始化過濾器時被調(diào)用,它在 Filter 的整個生命周期只會被調(diào)用一次,這個方法必須執(zhí)行成功,否則過濾器會不起作用。
- doFilter() :容器中的每一次請求都會調(diào)用該方法, FilterChain 用來調(diào)用下一個過濾器 Filter。
- destroy(): 當容器銷毀 過濾器實例時調(diào)用該方法,一般在方法中銷毀或關閉資源,在過濾器 Filter 的整個生命周期也只會被調(diào)用一次。
2.3 過濾器實現(xiàn)
@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 攔截器定義
攔截器它是鏈式調(diào)用,一個應用中可以同時存在多個攔截器Interceptor, 一個請求也可以觸發(fā)多個攔截器 ,而每個攔截器的調(diào)用會依據(jù)它的聲明順序依次執(zhí)行。
3.2 攔截器的核心API
SpringMVC攔截器提供三個方法分別是preHandle、postHandle、afterCompletion,我們就是通過這幾個方法來對用戶的請求進行攔截處理的。
- preHandle() :這個方法將在請求處理之前進行調(diào)用。「注意」:如果該方法的返回值為false ,將視為當前請求結束,不僅自身的攔截器會失效,還會導致其他的攔截器也不再執(zhí)行。
- postHandle():只有在 preHandle() 方法返回值為true 時才會執(zhí)行。會在Controller 中的方法調(diào)用之后,DispatcherServlet 返回渲染視圖之前被調(diào)用。「有意思的是」:postHandle() 方法被調(diào)用的順序跟 preHandle() 是相反的,先聲明的攔截器 preHandle() 方法先執(zhí)行,而postHandle()方法反而會后執(zhí)行。
- afterCompletion():只有在 preHandle() 方法返回值為true 時才會執(zhí)行,在整個請求結束之后, DispatcherServlet 渲染了對應的視圖之后執(zhí)行。
3.3 攔截器的實現(xiàn)
SpringMVC攔截器有兩種實現(xiàn)方式:
第一種方式是要定義的Interceptor類要實現(xiàn)了Spring的HandlerInterceptor 接口;
第二種方式是繼承實現(xiàn)了HandlerInterceptor接口的類,比如Spring已經(jīng)提供的實現(xiàn)了HandlerInterceptor接口的抽象類HandlerInterceptorAdapter;
以下是實現(xiàn)一個登錄攔截過程
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";
/**
* 在業(yè)務處理器處理請求之前被調(diào)用
* 如果返回false
* 從當前的攔截器往回執(zhí)行所有攔截器的afterCompletion(),再退出攔截器鏈
*
* 如果返回true
* 執(zhí)行下一個攔截器,直到所有的攔截器都執(zhí)行完畢
* 再執(zhí)行被攔截的Controller
* 然后進入攔截器鏈,
* 從最后一個攔截器往回執(zhí)行所有的postHandle()
* 接著再從最后一個攔截器往回執(zhí)行所有的afterCompletion()
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
if ("GET".equalsIgnoreCase(request.getMethod())) {
RequestUtil.saveRequest();
}
log.info("==============執(zhí)行順序: 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;
}
}
/**
* 在業(yè)務處理器處理請求執(zhí)行完成后,生成視圖之前執(zhí)行的動作
* 可在modelAndView中加入數(shù)據(jù),比如當前時間
*/
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
log.info("==============執(zhí)行順序: 2、postHandle================");
if(modelAndView != null){ //加入當前時間
modelAndView.addObject("haha", "測試postHandle");
}
}
/**
* 在DispatcherServlet完全處理完請求后被調(diào)用,可用于清理資源等
* 當有攔截器拋出異常時,會從當前攔截器往回執(zhí)行所有的攔截器的afterCompletion()
*/
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
log.info("==============執(zhí)行順序: 3、afterCompletion================");
}
}
spring-MVC.xml的相關配置
<!--配置攔截器, 多個攔截器,順序執(zhí)行 -->
<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>
<!-- 當設置多個攔截器時,先按順序調(diào)用preHandle方法,然后逆序調(diào)用每個攔截器的postHandle和afterCompletion方法 -->
</mvc:interceptors>
web.xml
<!-- 不攔截靜態(tài)文件 -->
<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 這樣寫
<!-- 對靜態(tài)資源文件的訪問-->
<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的配置,我們大致也能上手寫項目,下面就針對他們的主要區(qū)別展開敘述
4 過濾器和攔截器的區(qū)別
過濾器和攔截器均體現(xiàn)了AOP的編程思想,都可以實現(xiàn)諸如日志,登錄鑒權等功能,但二者的不同點也是比較多的
- 攔截器是基于Java的反射機制,而過濾器是基于函數(shù)回調(diào)
- 攔截器不依賴與Servlet容器,而過濾器依賴Servlet容器
- 攔截器只能對Controller請求起作用,而過濾器可以對幾乎所有請求起作用
- 攔截器可以訪問Controller上下文,值棧里的對象,而過濾器不能
- 在Spring容器的生命周期中,攔截器可以多次調(diào)用,而過濾器只能在容器初始化時被調(diào)用一次
4.1 實現(xiàn)原理不同
過濾器和攔截器底層實現(xiàn)方式大不相同,過濾器是基于函數(shù)回調(diào)的,攔截器則是基于Java的反射機制(動態(tài)代理)實現(xiàn)的。
在我們自定義的過濾器中都會實現(xiàn)一個 doFilter()方法,這個方法有一個FilterChain 參數(shù),而實際上它是一個回調(diào)接口。ApplicationFilterChain是它的實現(xiàn)類, 這個實現(xiàn)類內(nèi)部也有一個 doFilter() 方法就是回調(diào)方法。
public interface FilterChain {
void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}
ApplicationFilterChain里面能拿到我們自定義的xxxFilter類,在其內(nèi)部回調(diào)方法doFilter()里調(diào)用各個自定義xxxFilter過濾器,并執(zhí)行 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 會先執(zhí)行自身的 doFilter() 過濾邏輯,最后在執(zhí)行結束前會執(zhí),filterChain.doFilter(servletRequest,servletResponse),也就是回調(diào)ApplicationFilterChain的doFilter() 方法,以此循環(huán)執(zhí)行實現(xiàn)函數(shù)回調(diào)。
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
filterChain.doFilter(servletRequest, servletResponse);
}
4.2 使用范圍不同
我們看到過濾器 實現(xiàn)的是 javax.servlet.Filter 接口,而這個接口是在Servlet規(guī)范中定義的,也就是說過濾器Filter 的使用要依賴于Tomcat等容器,導致它只能在web程序中使用。
而攔截器(Interceptor) 它是一個Spring組件,并由Spring容器管理,并不依賴Tomcat等容器,是可以單獨使用的。不僅能應用在web程序中,也可以用于Application、Swing等程序中。
4.3 觸發(fā)時機不同
過濾器 和 攔截器的觸發(fā)時機也不同,我們看下邊這張圖。
過濾器Filter是在請求進入容器后,但在進入servlet之前進行預處理,請求結束是在servlet處理完以后。
攔截器 Interceptor 是在請求進入servlet后,在進入Controller之前進行預處理的,Controller 中渲染了對應的視圖之后請求結束。
4.4 攔截的請求范圍不同
在上邊我們已經(jīng)同時配置了過濾器和攔截器,再建一個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 后置");
}
}
項目啟動過程中發(fā)現(xiàn),過濾器的init()方法,隨著容器的啟動進行了初始化
此時瀏覽器發(fā)送請求,F(xiàn)12 看到居然有兩個請求,一個是我們自定義的 Controller 請求,另一個是訪問靜態(tài)圖標資源的請求。
看到控制臺的打印日志如下:
執(zhí)行順序 :Filter 處理中 -> Interceptor 前置 -> 我是controller -> Interceptor 處理中 -> Interceptor 處理后
Filter 處理中
Interceptor 前置
我是controller
Interceptor 處理中
Interceptor 后置
Filter 處理中
過濾器Filter執(zhí)行了兩次,攔截器Interceptor只執(zhí)行了一次。這是因為過濾器幾乎可以對所有進入容器的請求起作用,而攔截器只會對Controller中請求或訪問static目錄下的資源請求起作用。
4.5 注入bean情況不同
在實際的業(yè)務場景中,應用到過濾器或攔截器,為處理業(yè)務邏輯難免會引入一些service服務。
下邊我們分別在過濾器和攔截器中都注入service,看看有什么不同?
@Component
public class TestServiceImpl implements TestService {
@Override
public void a() {
System.out.println("我是方法A");
}
}
過濾器中注入service,發(fā)起請求測試一下 ,日志正常打印出“我是方法A”。
@Autowired
private TestService testService;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Filter 處理中");
testService.a(); // 調(diào)用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 ,發(fā)起請求測試,竟然會報錯,別急,原因是加載順序?qū)е碌膯栴}
@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 控制執(zhí)行順序不同
實際開發(fā)過程中,會出現(xiàn)多個過濾器或攔截器同時存在的情況,不過,有時我們希望某個過濾器或攔截器能優(yōu)先執(zhí)行,就涉及到它們的執(zhí)行順序。
過濾器用@Order注解控制執(zhí)行順序,通過@Order控制過濾器的級別,值越小級別越高越先執(zhí)行。
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public class MyFilter2 implements Filter {
攔截器默認的執(zhí)行順序,就是它的注冊順序,也可以通過Order手動設置控制,值越小越先執(zhí)行。
@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);
}
看到輸出結果發(fā)現(xiàn),先聲明的攔截器 preHandle() 方法先執(zhí)行,而postHandle()方法反而會后執(zhí)行。
postHandle() 方法被調(diào)用的順序跟 preHandle() 居然是相反的!如果實際開發(fā)中嚴格要求執(zhí)行順序,那就需要特別注意這一點。
Interceptor1 前置
Interceptor2 前置
Interceptor 前置
我是controller
Interceptor 處理中
Interceptor2 處理中
Interceptor1 處理中
Interceptor 后置
Interceptor2 處理后
Interceptor1 處理后
看源碼,我們要知道controller 中所有的請求都要經(jīng)過核心組件DispatcherServlet路由,都會執(zhí)行它的 doDispatch() 方法,而攔截器postHandle()、preHandle()方法便是在其中調(diào)用的。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
...........
try {
// 獲取可以執(zhí)行當前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;
}
}
// 注意: 執(zhí)行Interceptor中PreHandle()方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 注意:執(zhí)行Handle【包括我們的業(yè)務邏輯,當拋出異常時會被Try、catch到】
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// 注意:執(zhí)行Interceptor中PostHandle 方法【拋出異常時無法執(zhí)行】
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
}
...........
}
看看兩個方法applyPreHandle()、applyPostHandle()具體是如何被調(diào)用的,就明白為什么postHandle()、preHandle() 執(zhí)行順序是相反的了。
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);
}
}
}
發(fā)現(xiàn)兩個方法中在調(diào)用攔截器數(shù)組 HandlerInterceptor[] 時,循環(huán)的順序竟然是相反的,導致postHandle()、preHandle() 方法執(zhí)行的順序相反。