iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
Software Development

30 篇淺談 Spring 框架的核心底層組件 系列 第 22 篇

Day 22: HandlerInterceptor —— Controller 調用鏈的三階段攔截機制

  • 分享至 

  • xImage
  •  

昨天我們講完了 HandlerMapping 的 HTTP 請求映射,也簡單帶了一些 Interceptor 的相關內容,今天我們就來集中說說 Spring MVC 裡這個很容易跟 Servlet Filter 搞混的另一種攔截機制。Interceptor 的全名叫做 HandlerInterceptor,同樣也是一個在 Spring MVC 裡面定義的核心介面,介面內不定義了 3 個攔截方法,分別是 preHandle(在請求進到 handler 之前進行介入)、postHandle(在請求從 handler 離開之後進行介入)、和 afterCompletion(在請求完成視圖渲染、或完成統一的異常處理之後進行介入)。這邊可以注意的一個小東西是:afterCompletion 因為是在異常處理完成之後才會進行呼叫,所以如果 afterCompletion 裡有拋出 Exception 的話,@ControllerAdvice 是無法處理的。

HandlerInterceptor 和 Filter 的調用邏輯有點類似:兩者都可以同時介入請求進來之前和請求響應之後的相關處理,在 Spring 框架下也都可以排序調用鏈,且一個請求如果在進到 handler 之前,依序調用的是 Interceptor(或 Filter)A, B, C,那麼在請求從 handler 響應之後,依序經過的 Interceptor(或 Filter)就會倒過來變成 C, B, A。但對於這點,兩者的實現方式有一點點細微的差別:Filter 是採用呼叫堆疊的方式實現這種正反兩向的傳播,某種程度上可以想像成一種 Decorator pattern 的應用;但 Interceptor 的做法是維護一個索引指針,透過獨立的 for 迴圈分別對 preHandle 進行正向輪詢、再對 postHandle / afterCompletion 進行反向輪詢。也因為實現的方式不同,真正的 request / response 只是一個被傳進 Interceptor 的參數而已,間接導致它無法像 Filter 一樣傳遞客製化的 Wrapper 來替換底層的物件本體,自然也就無法重寫 getInputStream() 以實現對 Body 的進階操作。

接著講講 Interceptor 的異常處理機制,在 Spring MVC 的底層架構中,所有攔截器的調用都是由 DispatcherServlet 配合 HandlerExecutionChain 來驅動的。這裡我有幾個覺得採了很多坑之後,值得被提出來的細節。第一個是不管請求在進入 Handler 之前通過了多少個 preHandle,一旦請求在執行 ha.handle(也就是 Controller 業務邏輯)的過程中有異常被拋出,所有攔截器的 postHandle 就完全不會被執行到。在框架的源碼上,preHandle, ha.handle, 和 postHandle 是被包覆在同一個 try-catch 區塊的。所以無論請求是在執行 preHandle 的途中出錯、或調用 ha.handle 到一半出錯、或者是執行 postHandle 的時候出錯,都會在這個最外層的框架(其實就是 doDispatch 的 try-catch)捕獲異常並進行處理。

第二個值得被提及的細節是:假設一個請求正確經過了攔截器鏈的 preHandle 方法和 ha.handle 方法,但在 postHandle 執行到一半的時候拋出異常,這個異常會有很高的機率無法被 @ControllerAdvice 捕獲。原因是執行異常處理的其中一個條件是「Response 的 Outputstream 要還沒 commit。」但以現今前後端分離為主流的架構來說,當一個類別被標記為 @RestController,方法就會資料回傳的時候,將 return value 做一次 json 的序列化,並寫入 Response 的 Outputstream 當中。因此後續的 postHandle 如果遇到新的 Exception,只要 Outputstream 已經在 ha.handle 被寫到 commit,後續的異常處理的流程自然無法對 Outputstream 進行更動,就會理所當然地跳過 @ControllerAdvice 的處理流程。

最後一個可以聊聊的細節是:無論是 preHandle, ha.handle, 或 postHandle 的執行過程發生什麼問題,在大多數的情況下,afterCompletion 都有辦法在最後進行正確的兜底執行。這裡唯一的例外是,afterCompletion 只會針對「有正確執行過 preHandle 的攔截器」進行呼叫,因此如果 A, B, C 三個 Interceptor 裡,攔截器 A, B 的 preHandle 都已執行完成,但 C 的 preHandle 出現異常。程式後續直接走到 try-catch 的 finally 區塊,就只會調用 A 跟 B 的 afterCompletion。且不同於 postHandle,的執行結果 afterCompletion 不會互相干擾,前者如果執行出錯,不會連帶後面的 afterCompletion 一同終止。同樣用剛才 A, B, C 的場景舉例:如果 B 在調用 afterCompletion 又拋出什麼例外,這個例外只會被迴圈自己包的 try-catch 接住,就會接著執行 A 的 afterCompletion。

也因為 afterCompletion 相對而言比 postHandle 可靠很多,因此在現今的場景之下,絕大多數我們都會使用 afterCompletion 完成 handler 的後置處理邏輯。但這也不是說 postHandle 已經沒有用武之地了:如果公司的系統還是使用 Thymeleaf, JSP 等分離不完全的架構,我們可以在 postHandle 這裡做一些全站屬性的統一注入,抑或是依據客戶端的特徵參數,抽換渲染的頁面模板 ...等。其次,即便是在分離完整的架構之下,如果我們需要針對 Controller 的方法本身進行單純的耗時監控、或者是統一對 Response 追加或微調 HTTP Header,postHandle 也是一個很好的介入點位。簡而言之,如果我們想要做一些業務完成時的資料補充或調整,那 postHandle 是個相對適合的處理位置,但如果我們需要完成那些成功與否都得進行的工作,那 afterCompletion 會是一個更好的擴充空間。

期待後續各位的閱讀與分享,我是 Pax,我們明天見。


上一篇
Day 21: HandlerMapping —— Spring MVC 的路由與執行鏈組裝機制
下一篇
Day 23: HandlerAdapter —— Handler 的統一調度與適配器模式實踐
系列文
30 篇淺談 Spring 框架的核心底層組件 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言