前言
springmvc攔截器是我們項目開發中用到的一個功能,常常用於對Handler進行預處理和後處理。本案例來演示一個較簡單的springmvc攔截器的使用,並通過分析源碼來探究攔截器的執行順序是如何控制的。
1、springmvc攔截器使用
1.1 項目初始搭建
1.1.1 創建一個maven的war工程
該步驟不再截圖說明
1.1.2 引入maven依賴
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
1.2.3 配置web.xml
配置springmvc核心控制器DispatcherServlet,由於需要加載springmvc.xml,所以需要創建一個springmvc.xml文件(文件參考源碼附件)放到classpath下
<!-- 前端控制器(加載classpath:springmvc.xml 伺服器啟動創建servlet) -->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 配置初始化參數,創建完DispatcherServlet對象,加載springmvc.xml配置文件 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
1.2 攔截器開發
1.2.1 準備兩個攔截器
兩個攔截器分別命名為MyInterceptor1、MyInterceptor2
publicclassMyInterceptor1implementsHandlerInterceptor{
publicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
System.out.println("==1-1====前置攔截器1 執行======");
returntrue; //ture表示放行
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
System.out.println("==1-2=====後置攔截器1 執行======");
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
System.out.println("==1-3======最終攔截器1 執行======");
}
}public class MyInterceptor2 implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
System.out.println("==2-1====前置攔截器2 執行======");
return true; //ture表示放行
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
System.out.println("==2-2=====後置攔截器2 執行======");
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
System.out.println("==2-3======最終攔截器2 執行======");
}
}
1.2.2 在springmvc.xml中攔截器
<!--配置攔截器-->
<mvc:interceptors>
<!--配置攔截器-->
<mvc:interceptor>
<mvc:mappingpath="/**" />
<beanclass="com.itheima.interceptor.MyInterceptor1" />
</mvc:interceptor>
<mvc:interceptor>
<mvc:mappingpath="/**" />
<beanclass="com.itheima.interceptor.MyInterceptor2" />
</mvc:interceptor>
</mvc:interceptors>
這兩個攔截器攔截規則相同,並且配置順序攔截器1在攔截器2之前!
1.3 測試攔截器效果
1.3.1 準備測試Controller
@Controller
publicclassBizController{
@RequestMapping("testBiz")
public String showUserInfo(Integer userId, Model model){
System.out.println(">>>>>業務代碼執行-查詢用戶ID為:"+ userId);
User user = new User(userId);
user.setName("宙斯");
model.addAttribute("userInfo",user);
return"user_detail";
}
}
該controller會轉發到user_detail.jsp頁面
1.3.2 準備user_detail.jsp
<html>
<head>
<title>detail</title>
</head>
<body>
用戶詳情:
${userInfo.id}:${userInfo.name}
<%System.out.print(">>>>>jsp頁面的輸出為:");%>
<%System.out.println(((User)request.getAttribute("userInfo")).getName());%>
</body>
</html>
1.3.3 測試效果
啟動項目後,在地址欄訪問/testBiz?userId=1,然後查看IDE控制臺列印:
==1-1====前置攔截器1 執行======
==2-1====前置攔截器2 執行======
>>>>>業務代碼執行-查詢用戶ID為:1
==2-2=====後置攔截器2 執行======
==1-2=====後置攔截器1 執行======
>>>>>jsp頁面的輸出為:宙斯
==2-3======最終攔截器2 執行======
==1-3======最終攔截器1 執行======
通過列印日誌發現,攔截器執行順序是: 攔截器1的前置>攔截器2的前置>業務代碼>攔截器2後置>攔截器1後置>攔截器2最終>攔截器1最終
2、源碼分析
經過測試發現攔截器執行順序如下:
攔截器1的前置>攔截器2的前置>業務代碼>攔截器2後置>攔截器1後置>攔截器2最終>攔截器1最終
我們通過分析源碼來探究下攔截器是如何執行的
2.1 DispatcherServlet
當瀏覽器發送/testBiz?userId=1的請求時,會經過DispatcherServlet的doDispatch方法,我們將其取出並觀察其核心代碼(省略非關鍵代碼)
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
//...
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
//1.獲取執行鏈
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
//2.獲取處理器適配器
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
//...
//【3】.執行前置攔截器
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
//4.執行業務handler
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
//【5】.執行後置攔截器
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
//【6】.處理頁面響應,並執行最終攔截器
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
}finally {
//...
}
}
代碼中有關攔截器執行的位置我都添加了注釋,其中注釋中標識的步驟中,3、5、6步驟是攔截器的關鍵步驟
其中,第一步中"獲取執行鏈",執行鏈內容可以通過debug調試查看內容:
可以看到我們自定義的兩個攔截器按順序保存
2.2 攔截器步驟解析
在doDispatch方法中,我們添加的注釋的第【3】、【5】、【6】步驟是對攔截器的執行處理,現在分別來查看第【3】、【5】、【6】步驟執行的具體方法的源碼
2.2.1 第【3】步驟
//3.執行前置攔截器中的詳細代碼
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
//獲得本次請求對應的所有攔截器
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
//按照攔截器順序依次執行每個攔截器的preHandle方法.
//並且,interceptorIndex值會一次 + 1 (該值是給後面的最終攔截器使用的)
for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
HandlerInterceptorinterceptor = interceptors[/color][i][color=black];
//只要每個攔截器不返回false,則繼續執行,否則執行最終攔截器
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
returnfalse;
}
}
}
//最終返回true
returntrue;
}
我們可以看到攔截器的preHandler(前置處理)方法是按攔截器(攔截器1、攔截器2)順序執行的,然後我們再來看步驟【5】
2.2.2 第【5】步驟
//5.執行後置攔截器
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
//獲得本次請求對應的所有攔截器
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
//按倒敘執行每個攔截器的postHandle方法——所以我們看到先執行的攔截器2的postHandle,再執行攔截器1的postHandle
for(int i = interceptors.length - 1; i >= 0; --i) {
HandlerInterceptor interceptor = interceptors[/color][color=black];
interceptor.postHandle(request, response, this.handler, mv);
}
}
}
會發現,後置處理是按照攔截器順序倒敘處理的!
我們最後來看下最終攔截器
2.2.3 第【6】步驟
//執行的方法
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
//...
if (mv != null && !mv.wasCleared()) {
//處理響應
this.render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
} elseif (this.logger.isDebugEnabled()) {
this.logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + this.getServletName() + "': assuming HandlerAdapter completed request handling");
}
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
//6、執行攔截器的最終方法們
mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
}
}
}
其中,有一個render()方法,該方法會直接處理完response。再後則是觸發triggerAfterCompletion方法:
//6、執行攔截器的最終方法
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) throws Exception {
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
//倒敘執行每個攔截器(interceptorIndex為前置攔截器動態計算)的afterCompletion方法
for(int i = this.interceptorIndex; i >= 0; --i) {
HandlerInterceptor interceptor = interceptors[/color][/i][color=black][i];
try {
interceptor.afterCompletion(request, response, this.handler, ex);
} catch (Throwable var8) {
logger.error("HandlerInterceptor.afterCompletion threw exception", var8);
}
}
}
}
由此可以看到,攔截器的最終方法的執行也是按照倒敘來執行的,而且是在響應之後。
3、總結
攔截器常用於初始化資源,權限監控,會話設置,資源清理等的功能設置,就需要我們對它的執行順序完全掌握,我們通過源碼可以看到,攔截器類似於對我們業務方法的環繞通知效果,並且是通過循環收集好的攔截器集合來控制每個攔截器方法的執行順序,進而可以真正做到深入掌握攔截器的執行機制!