Spring HandlerInterceptor工作機制

来源:https://www.cnblogs.com/acelin/archive/2023/08/17/17637388.html
-Advertisement-
Play Games

> 本文以一個通過正常註冊攔截器流程註冊攔截器失敗的實際場景,來帶領大家閱讀源碼,體會Spring的HandlerInterceptor攔截器整個工作流程 ### 簡單認識 org.springframework.web.servlet.HandlerInterceptor是Spring框架中的一個 ...


本文以一個通過正常註冊攔截器流程註冊攔截器失敗的實際場景,來帶領大家閱讀源碼,體會Spring的HandlerInterceptor攔截器整個工作流程

簡單認識

org.springframework.web.servlet.HandlerInterceptor是Spring框架中的一個介面,用於攔截處理程式(Handler)的請求和響應。它允許開發人員在請求處理程式執行之前和之後執行自定義的預處理和後處理邏輯。 HandlerInterceptor介面定義了三個方法:

  1. preHandle:在請求處理程式執行之前調用。可以用於進行許可權驗證、日誌記錄等操作。如果該方法返回false,則請求將被中斷,後續的攔截器和處理程式將不會被執行。
  2. postHandle:在請求處理程式執行之後、視圖渲染之前調用。可以對請求的結果進行修改或添加額外的模型數據。
  3. afterCompletion:在整個請求完成之後調用,包括視圖渲染完畢。可用於進行資源清理等操作。 通過實現HandlerInterceptor介面,可以自定義攔截器,並將其註冊到Spring MVC的配置中。攔截器可以攔截指定的URL或者所有請求,併在請求的不同階段執行相應的邏輯。

正常的攔截器註冊流程

定義一個攔截器,實現org.springframework.web.servlet.HandlerInterceptor介面

如:

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
	// ...
        return true;
    }
   
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        // ...
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    	// ...
    }
}

定義配置類,實現WebMvcConfigurer,實現addInterceptors進行攔截器註冊

@Configuration(proxyBeanMethods = false)
public class WebMvcConfiguration implements WebMvcConfigurer {


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    	
    	// ...

        registry.addInterceptor(new LoginInterceptor())
            .addPathPatterns(HOTSWAP_ALL_SERVICE)
            .addPathPatterns(hsAddPaths)
            .excludePathPatterns(commonExcludePaths);
            
        // ...

    }
}

而有時候通過這種方式註冊不成功。

我們先來說一下說一下HandlerInterceptor的工作機制。首先以上描述的是攔截器的註冊過程。

然後再來看一下註冊的攔截器存儲在哪裡,以及如何被使用的。

存儲已註冊攔截器的位置

查看介面方法org.springframework.web.servlet.config.annotation.WebMvcConfigurer#addInterceptors的調用之處,位於類org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration裡面,其是WebMvcConfigurationSupport的子類

WebMvcConfigurationSupport的一個子類,它檢測並委托給所有WebMvcConfigurer類型的bean,允許它們自定義WebMvcConfigurationSupport提供的配置。這是@EnableWebMvc實際導入的類。

org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration#addInterceptors方法:

@Override
protected void addInterceptors(InterceptorRegistry registry) {
   this.configurers.addInterceptors(registry);
}

可以看出,這是從我們註冊的WebMvcConfigurer實現類中,通過傳入的InterceptorRegistry實例搜集攔截器,而調用該方法的地方位於

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#requestMappingHandlerMapping,實際是其預設子類DelegatingWebMvcConfiguration進行註冊RequestMappingHandlerMapping bean 的行為,在其方法中調用了org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getInterceptors方法,解析InterceptorRegistry裡面添加的攔截器。

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getInterceptors

* Provide access to the shared handler interceptors used to configure
 * {@link HandlerMapping} instances with.
 * <p>This method cannot be overridden; use {@link #addInterceptors} instead.
 */
protected final Object[] getInterceptors(
      FormattingConversionService mvcConversionService,
      ResourceUrlProvider mvcResourceUrlProvider) {
   if (this.interceptors == null) {
      InterceptorRegistry registry = new InterceptorRegistry();
      addInterceptors(registry);
      registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService));
      registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider));
      // 排序
      this.interceptors = registry.getInterceptors();
   }
   return this.interceptors.toArray();
}

整個過程就是,WebMvcConfigurationSupport在註冊RequestMappingHandlerMapping時候,會創建一個InterceptorRegistry實例, 被傳入了org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration#addInterceptors方法,進而傳入了org.springframework.web.servlet.config.annotation.WebMvcConfigurerComposite#addInterceptors方法,最後被每個WebMvcConfigurer的實現類的addInterceptors方法所接收,用於搜集用戶自定義的攔截器註冊。即幫助配置映射攔截器列表。

註冊失敗原因

我們來看一下下麵這個註冊失敗的場景:

@Configuration
@ConditionalOnProperty(name = "spring.mvc.configuration.custom", havingValue = "false", matchIfMissing = true)
public class CustomSpringMvcConfiguration extends WebMvcConfigurationSupport {
	// ...
}

原因是一個公共的jar包註冊了一個的子類,導致預設的註冊子類DelegatingWebMvcConfiguration沒有被註冊,而其對方法的重寫,也沒有覆寫DelegatingWebMvcConfiguration的預設實現。

@Override
protected void addInterceptors(InterceptorRegistry registry) {
    if (auditTrailLogInterceptor != null) {
        registry.addInterceptor(auditTrailLogInterceptor).addPathPatterns("/**");
        log.info("已註冊 AuditTrailLog 攔截器");
    }
    if (openApiLogInterceptor != null) {
        registry.addInterceptor(openApiLogInterceptor).addPathPatterns("/**");
        log.info("已註冊 openApiLog 攔截器");
    }
    super.addInterceptors(registry);
}

其中

super.addInterceptors(registry);

是個無效的語句,因為父類是一個空實現。相當於文首的註冊攔截器方法被堵死了,迫於無法直接修改jar包的情況,我們只能另尋方法。

初步嘗試

由於所有註冊的攔截器,最終都是放在org.springframework.web.servlet.handler.AbstractHandlerMapping#interceptors中,所以我們要想辦法拿到AbstractHandlerMapping的註冊bean,即上面提到的org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#requestMappingHandlerMapping方法註冊的bean,然後將我們的攔截器註冊並放置進去。

既然上述註冊方法行不通,那麼我們的配置類就無需實現WebMvcConfigurer介面了。

我們需要重寫一個配置類,註入RequestMappingHandlerMapping對象,然後進行手動註冊。直接在構造函數中註冊。

@Configuration(proxyBeanMethods = false)
public class WebMvcConfiguration {


    public WebMvcConfiguration(
        @Qualifier("requestMappingHandlerMapping") RequestMappingHandlerMapping mapping) {

        // 反射拿到已經註冊的攔截器集合
        List<Object> interceptors = ReflectionUtil.getField(mapping, "interceptors");
        InterceptorRegistry registry = new InterceptorRegistry();
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns(urlPatterns);
        List<Object> list = ReflectionUtil.invoke(registry, "getInterceptors", null, null);
        interceptors.addAll(list);

    }

}

先通過反射拿到interceptors變數,然後new一個InterceptorRegistry,將其添加進去後反射調用其getInterceptors,並添加到RequestMappingHandlerMapping的變數interceptors當中去。

這似乎完成了註冊,萬事大吉了,但當我啟動項目,發現還想沒有註冊成功。這就令我費解了。到底是哪裡出了問題?

攔截器的使用

後面經過大神的指點,可以從以下思路尋找答案:

因為我這個攔截器攔截了部分路徑而已,之所以說沒註冊成功,是因為攔截的這些路徑沒有被攔截到。那既然存放的地方已經解決了,那問題就應該從使用的地方尋找答案。

這些攔截器,都帶著器攔截的路徑信息,而sping的請求,都會進入一個Servlet,因此這些存儲的攔截器,必定是在這個Servlet中使用,因為Servlet能拿到請求的HttpServletRequest信息,請求的路徑正是從這裡拿到。

這個Servlet,就是DispatcherServlet

我們要查看DispatcherServlet是在哪裡使用到這些攔截器的

org.springframework.web.servlet.DispatcherServlet#doService中,會調用org.springframework.web.servlet.DispatcherServlet#doDispatch方法

/** 處理對處理程式的實際調度。 處理程式將通過按順序應用servlet的HandlerMappings來獲得。HandlerAdapter將通過查詢servlet已安裝的HandlerAdapter來獲得,以找到第一個支持處理程式類的HandlerAdapter。 所有HTTP方法都由這個方法處理。由HandlerAdapters或處理程式本身來決定哪些方法是可接受的。 
參數: 
request -當前HTTP請求 
response -當前HTTP響應 
拋出: 
異常——在任何處理失敗的情況下 */
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
   HttpServletRequest processedRequest = request;
   HandlerExecutionChain mappedHandler = null;
   boolean multipartRequestParsed = false;

   WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

   try {
      ModelAndView mv = null;
      Exception dispatchException = null;

      try {
         processedRequest = checkMultipart(request);
         multipartRequestParsed = (processedRequest != request);

         // Determine handler for the current request.
         mappedHandler = getHandler(processedRequest);
         if (mappedHandler == null) {
            noHandlerFound(processedRequest, response);
            return;
         }

         // Determine handler adapter for the current request.
         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 (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
               return;
            }
         }

         if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            return;
         }

         // Actually invoke the handler.
         mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

         if (asyncManager.isConcurrentHandlingStarted()) {
            return;
         }

         applyDefaultViewName(processedRequest, mv);
         mappedHandler.applyPostHandle(processedRequest, response, mv);
      }
      catch (Exception ex) {
         dispatchException = ex;
      }
      catch (Throwable err) {
         // As of 4.3, we're processing Errors thrown from handler methods as well,
         // making them available for @ExceptionHandler methods and other scenarios.
         dispatchException = new NestedServletException("Handler dispatch failed", err);
      }
      processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
   }
   catch (Exception ex) {
      triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
   }
   catch (Throwable err) {
      triggerAfterCompletion(processedRequest, response, mappedHandler,
            new NestedServletException("Handler processing failed", err));
   }
   finally {
      if (asyncManager.isConcurrentHandlingStarted()) {
         // Instead of postHandle and afterCompletion
         if (mappedHandler != null) {
            mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
         }
      }
      else {
         // Clean up any resources used by a multipart request.
         if (multipartRequestParsed) {
            cleanupMultipart(processedRequest);
         }
      }
   }
}

其中的

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);

就是確定這次請求的所有的匹配的HandlerInterceptor,也就是說,這裡會從RequestMappingHandlerMapping對象獲取所有註冊的HandlerInterceptor,然後根據request的路徑匹配,覺得哪些攔截器是該攔截該請求的,並根據順序形成攔截鏈。

這一關鍵操作的位置,位於org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandlerExecutionChain

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
   HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
         (HandlerExecutionChain) handler : new HandlerExecutionChain(handler));

   String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH);
   
   // 遍歷所有攔截器,進行匹配,匹配上的假如攔截鏈
   for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
      if (interceptor instanceof MappedInterceptor) {
         MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
         if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
            chain.addInterceptor(mappedInterceptor.getInterceptor());
         }
      }
      else {
         chain.addInterceptor(interceptor);
      }
   }
   return chain;
}

以上代碼大概的流程就是渠道所有的攔截器,和取到的請求路徑進行正則匹配,匹配上的話,就添加進入攔截鏈

org.springframework.web.servlet.handler.MappedInterceptor#matches

用到的工具類是org.springframework.util.AntPathMatcher,PathMatcher的預設實現類

ant風格路徑模式的PathMatcher實現。

部分映射代碼是從Apache Ant中借來的。

該映射使用以下規則匹配url:

? 匹配一個字元

*匹配零個或多個字元

** 匹配路徑中的零個或多個目錄

{spring:[a-z]+}匹配regexp [a-z]+作為名為"spring"的路徑變數

例子

com/t?st .jsp -匹配com/test.jsp,也匹配com/ taste .jsp或com/txst.jsp .jsp com/t?st.jsp — matches com/test.jsp but also com/tast.jsp or com/txst.jsp
com/
.jsp — 匹配所有在com文件夾中的 .jsp
com/ * * /test.jsp — 匹配com路徑下的所有 test.jsp文件
org/springframework/ * * /*.jsp —匹配org/springframework路徑下的所有.jsp

com/{filename:\w+}.jsp 匹配com/test.jsp 並將值 test 賦值給 filename 變數

註意:模式和路徑必須都是絕對的,或者都是相對的,以便兩者匹配。因此,建議使用此實現的用戶對模式進行消毒,以便在使用模式的上下文中使用“/”作為首碼。

重點來了,細心的朋友會發現, 其遍歷的攔截器列表,是org.springframework.web.servlet.handler.AbstractHandlerMapping#adaptedInterceptors,不是我們上面提及的org.springframework.web.servlet.handler.AbstractHandlerMapping#interceptors,這也就解釋了為啥我們的攔截器好像沒註冊成功的原因,原來還差一步沒銜接上。

因此我們要找一下org.springframework.web.servlet.handler.AbstractHandlerMapping#interceptors是如何轉換成org.springframework.web.servlet.handler.AbstractHandlerMapping#adaptedInterceptors

通過查看的其被調用add的方法,可以找到下麵轉換方法

org.springframework.web.servlet.handler.AbstractHandlerMapping#initInterceptors

protected void initInterceptors() {
   if (!this.interceptors.isEmpty()) {
      for (int i = 0; i < this.interceptors.size(); i++) {
         Object interceptor = this.interceptors.get(i);
         if (interceptor == null) {
            throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null");
         }
         this.adaptedInterceptors.add(adaptInterceptor(interceptor));
      }
   }
}

其轉換的時機是在應用上下文初始化成功後通知的時候

org.springframework.context.support.ApplicationObjectSupport#setApplicationContext

ApplicationObjectSupport實現了ApplicationContextAware介面

最終實現

至此攔截器的整個工作流程正式完成了閉環。由於initInterceptors是被保護的方法,我們同樣需要解決反射工具來完成調用。因此最終的攔截器註冊配置類實現如下

@Configuration(proxyBeanMethods = false)
public class WebMvcConfiguration {


    public WebMvcConfiguration(
        @Qualifier("requestMappingHandlerMapping") RequestMappingHandlerMapping mapping,
        Environment environment) {

		// 從配置文件中讀取攔截路徑的配置
        String patternConfig = environment.getProperty("app.config.client-filter-url-patterns");
        List<String> urlPatterns = new ArrayList<>();
        if (StringUtils.hasText(patternConfig)) {
            urlPatterns = Arrays.asList(patternConfig.split(SymbolConsts.COMMA));
        }
        // 反射拿到已經註冊的攔截器存放點
        List<Object> interceptors = ReflectionUtil.getField(mapping, "interceptors");
        InterceptorRegistry registry = new InterceptorRegistry();
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns(urlPatterns);
        List<Object> list = ReflectionUtil.invoke(registry, "getInterceptors", null, null);
        // 添加進去
        interceptors.addAll(list);

        // 防止有已經轉換完成的攔截器被覆蓋或者衝突,先清空已經init的adaptedInterceptor
        List<HandlerInterceptor> adaptedInterceptors = ReflectionUtil.getField(mapping, "adaptedInterceptors");
        adaptedInterceptors.clear();
        // 重新init
        ReflectionUtil.invoke(mapping, "initInterceptors", null, null);

    }

}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ![](https://cdn.nlark.com/yuque/0/2023/jpeg/28753938/1691067189459-f51a48da-0da6-4e6e-aeee-75b39662cd20.jpeg) ## 一、Lambda表達式 > Lambda 是一個匿名函數,我們可以把 La ...
  • 關於票據系統設計在之前的博客中也聊過,今天做一個補充 1、架構 票據系統主要就是和票交所進行交互,圍繞這一核心,我們把系統劃分為三大部分,分別是:票據網關服務、票據業務服務、票據庫存服務。 網關服務:對接票交所,負責和票交所的交互,主要是收發報文。 業務服務:負責票據業務的處理,比如出票、背書、貼現 ...
  • # 【狂神說Java】Java零基礎學習筆記-JavaSE總結 ## JavaSE總結: ![image](https://img2023.cnblogs.com/blog/3231511/202308/3231511-20230817171925456-1307925972.jpg) ## 🎉� ...
  • # 【狂神說Java】Java零基礎學習筆記-異常 ## 異常01:Error和Exception ### 什麼是異常 - 實際工作中,遇到的情況不可能是非常完美的。比如:你寫的某個模塊,用戶輸入不一定符合你的要求、你的程式要打開某個文件,這個文件可能不存在或者文件格式不對,你要讀取資料庫的數據,數 ...
  • ### 1. json.load(json_data)與json.dump(python_data) json.load()用來將讀取json文件,json.dump()用來將數據寫入json文件 ### 2. json.loads()與json.dumps() - json.dumps 將 Pyt ...
  • 使用python爬蟲爬取數據的時候,經常會遇到一些網站的反爬蟲措施,一般就是針對於headers中的User-Agent,如果沒有對headers進行設置,User-Agent會聲明自己是python腳本,而如果網站有反爬蟲的想法的話,必然會拒絕這樣的連接。 而修改headers可以將自己的爬蟲腳本 ...
  • C++ STL(Standard Template Library)是C++標準庫中的一個重要組成部分,提供了豐富的模板函數和容器,用於處理各種數據結構和演算法。在STL中,排序、算數和集合演算法是常用的功能,可以幫助我們對數據進行排序、統計、查找以及集合操作等。STL提供的這些演算法,能夠滿足各種數據處... ...
  • 為衡量個人能力水平自創的一套評分機制,根據時間、代碼行數、基礎理論三個變數生成。使用lua語言編寫,輸出成三個markdown表格。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...