springmvc攔截器及源碼分析

2020-12-12 黑馬程式設計師

前言

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、總結

攔截器常用於初始化資源,權限監控,會話設置,資源清理等的功能設置,就需要我們對它的執行順序完全掌握,我們通過源碼可以看到,攔截器類似於對我們業務方法的環繞通知效果,並且是通過循環收集好的攔截器集合來控制每個攔截器方法的執行順序,進而可以真正做到深入掌握攔截器的執行機制!

相關焦點

  • 五大框架之SpringMVC
    參數中的Object handler是下一個攔截器。十、如何使用攔截器?<mvc:interceptors/>會為每一 個HandlerMapping,注入一個攔截器。總有一個HandlerMapping是可以找到處理器的,最多也只找到一個處理器,所以這個攔截器總會被執行的。起到了總攔截器的作用。方案二, (近似) 總攔截器, 攔截匹配的URL。
  • SpringMvc @Validated註解執行原理
    2|0SpringMvc接口參數校驗原理springmvc接口方法中注有@Validated或@Valid參數是如何校驗的呢?怎麼就能把參數綁定之後的校驗結果給到BindingResult實例呢?其實如果你對springmvc的方法參數解析器(HandlerMethodArgumentResolver)了解一些,就應該知道參數校驗這塊肯定是在對應的方法參數解析器裡執行的。如下是@RequestBody註解對應的參數解析器(RequestResponseBodyMethodProcessor)。
  • SSM框架-SpringMVC詳解
    springmvc概述Springmvc是spring框架的一個模塊,spring和springmvc無需中間整合層整合。-- 配置springmvc前端控制器, 將所有請求交給springmvc來處理 --> <servlet> <servlet-name>springmvc</servlet-name> <servletclass>org.springframework.web.servlet.DispatcherServlet
  • 「原創」007|搭上SpringBoot攔截器源碼分析專車
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫專車介紹該趟專車是開往SpringBoot攔截器源碼分析的專車什麼是攔截器?專車分析在SpringBoot請求處理源碼分析一文中有提到創建RequestMappingHandlerMapping對象,下面來回顧一下。
  • SpringMVC的常用註解
    springmvc原理圖第一個:@RequestParam註解作用:是將請求參數綁定到你的控制器的方法參數上,是springmvc中的接收普通參數的註解we'b第三個:@PathVaribale註解作用:該註解是用於綁定url中的佔位符,但是注意,spring3.0以後,url才開始支持佔位符的,它是springmvc
  • 短連結服務Octopus的實現與源碼開放
    當時為了快速推廣,使用了一些比較知名的第三方短鏈壓縮平臺,存在一些問題:收費貴一些情況下,短鏈域名在部分第三方平臺例如微信會被封殺回源數據沒有辦法定製處理方案,無法打通整個業務鏈路進行數據分析和跟蹤基於此類問題,決定自研一個(長連結壓縮為)短連結服務,當時剛好同步進行微服務拆分,內部很多微服務需要重新命名,組內的一個妹子說不如就用Github的吉祥物去命名octopus cat(章魚貓
  • 面試必備SpringMVC工作原理,漲薪5K就靠它!
    DispatcherServlet收到請求調用HandlerMapping處理器映射器處理器映射器找到具體的處理器(註解或者xml配置),生成處理器對象以及處理器攔截器(若有),返回給DispatcherServletDispatcherServlet
  • 阿里P8架構師用項目經驗編寫SpringMVC+MyBatis從入門到項目實戰
    第2篇MyBatis 技術入門本篇通過分析傳統JDBC開發模式的缺陷,引出MyBatis框架誕生的背景及特點。然後通過一個入門程序讓讀者對MyBatis有一個大致的了解,繼而展開對MyBatis配置文件、高級映射及緩存結構等知識點的講解。
  • springMVC框架之最後一天的學習
    映射器會將查詢結果返回給控制器,返回結果就包含了一個攔截器集合。1自定義一個攔截器實現HandlerInterceptor接口,Interceptor,本身也就是攔截器的意思。2攔截器配置interceptors標籤裡面放interceptor標籤,說明攔截器是可以配置多個的。 例子中只配置了一個。mapping path:表示攔截的請求,兩個*號代表0個或者多個路徑,這個昨天也詳講過。
  • Spring Cloud Eureka Server源碼分析
    總覽昨天文章分析介紹可Eureka Client的流程,今天接著昨天的將分析Eureka Server的一個流程。除此之外,將持續更新關於Spring Cloud的源碼文章,歡迎大家關注,源碼的學習在於持續行動,刻意學習!
  • SpringMVC是圍繞DispatcherServlet設計的,你都理解了嗎?
    它的作用相當於spring web mvc的集中訪問點,負責分派,很好的一點就是很Spring ioc容器無縫集成,這就意味spring提供的任何特性,springmvc都可以使用。DispatcherServlet應該在哪裡配置?
  • 001|搭上SpringBoot自動注入源碼分析專車
    專車介紹該趟專車是開往Spring Boot自動注入原理源碼分析的專車專車問題Spring Boot何時注入@Autowired標註的屬性?接下來我們就開始對源碼進行分析專車分析在分析代碼之前我們先回憶一下操作對象的步驟:首先我們會實例化一個對象然後調用對象的set方法來設置對象的屬性有了上面的基礎知識
  • 乾貨|新手也能看懂的源碼閱讀技巧
    :【原創】001 | 搭上 SpringBoot 自動注入源碼分析專車【原創】002 | 搭上 SpringBoot 事務源碼分析專車【原創】003 | 搭上基於 SpringBoot 事務思想實戰專車
  • 看到Mybatis源碼就感到煩躁,怎麼辦?
    背景最近,聽到很多吐槽:看到源碼,心中就感到十分糾結、特別煩惱。為什麼糾結?因為面試的時候,面試官很喜歡問:你看過什麼框架源碼?JDK源碼也行。這時候,如果回答沒有看過,雖然沒讓你立馬回去等通知。確實很忙,經常看了一部分後,天天加班,沒時間看,忙完後又忘了之前正在看的某某源碼。個人建議對於一般java程式設計師來說,閱讀源碼之前到底需要些什麼技能呢?
  • Spring源碼解析之源碼的下載編譯
    老粉應該知道了,筆者最近剛上架技術生涯的第一本書《Java源碼模擬面試解析指南 》。於是趁著年輕時還有時間修福報,準備再做一個 Spring源碼面試指南。相比於之前是直接閱讀引入jar包的方式來研讀 JDK 源碼,由於框架源碼的複雜性及強可插拔性,一般將源碼編譯到本地,由此便可以邊研讀源碼邊加自由地注釋,方便調試程序等。首先進入 spring 官方倉庫,fork 一份倉庫到自己帳號下,方便自己提交。
  • Spring Boot+Redis+攔截器+自定義Annotation實現接口自動冪等
    這個異常信息可以被攔截器捕捉到,然後返回給前端。throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200); } return true; }}四:攔截器的配置
  • Google限制廣告攔截器使用的Chrome功能
    毫不奇怪,此舉激怒了那些製造廣告攔截器的人。uBlock Origin的開發者雷蒙德·希爾(Raymond Hill)在論壇上聲稱,谷歌的真正意圖是投放更多廣告。其他人則說他們計劃放棄Chrome。「這是Google的一個令人失望的決定。
  • 短視頻直播源碼和直播短視頻源碼,區別是什麼?
    短視頻直播源碼和直播短視頻源碼都是近兩年比較火熱的音視頻源碼,他們共同的特點在於,都具備「直播」和「短視頻」兩種功能,都具有「社交」屬性,基本功能重疊度大等相似點,當然,既然被分為兩種源碼,那必然會有其不同之處,讓我們一起來看看吧。
  • 教育類APP源碼,在線教育源碼,網校源碼開發模式主要有這五大類
    教育類APP源碼開發優勢: 1、可以讓學生隨時隨地學習,節約資金 與傳統教育模式相比,在線教育源碼搭建的在線教育系統可以打破空間和時間限制,讓學生隨時隨地進行學習,而且還可以多終端進行登錄學習,給學生提供了大的便利,還節省了學校的學校場地費