iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
Software Development

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

Day 19: DispatcherServlet —— 請求分發與 Spring MVC 核心處理流程

  • 分享至 

  • xImage
  •  

到昨天為止,我們說完了跟 Spring 原生框架有關的相關內容。雖然還有些重要組件沒有提及,但大家已經可以透過之前的那些文章勾勒出 Spring 原生框架的大致輪廓了。從今天開始,我們把視野從 Spring 調整到 Spring MVC 上面,看一下 Spring 最最知名的 Web 模組底層是怎麼構建起一個完整的 Java 後端服務的。今天我們先把時間放在 DispatcherServlet 上。類似於 Spring 框架中的 ApplicationContext,Spring MVC 也有一個類似的控制組件。DispatcherServlet 是整個 Spring MVC 框架的核心入口與大腦,具體來說,當一條 HTTP 封包被送進服務,首先會由 Tomcat(或其他 Web 伺服器)接手資料,將資料轉變為 HttpServletRequest,後續經過 Filter chain 做初步的安全檢查、字編碼、日誌記錄 ...等,後續就會進入 Spring MVC 的領域,交由 DispatcherServlet 接手。

DispatcherServlet 本身是一個實體類別,繼承鏈的最頂層是 GenericServlet,定義了基本的 Servlet 骨架;下一層實作是 HttpServlet,專注在 Java 的 HTTP 協議規範實作與生命週期封裝,早期常見的 doGet, doPost 就是從這裡出來的;再下一層的 HttpServletBean 是把 Servlet 接入 Spring 框架的首個步驟,負責把一個 Servlet 物件轉換成 IoC 容器可以使用的 Bean,將 Servlet 的初始化參數轉化成帶有 Spring 風格的注入形式;再往下一層的 FrameworkServlet 專注於 Servlet 與 Spring IoC 容器深度綁定。這裡做的事情就有點多了:像是取得 Web 相關的 IoC 容器、封裝 doGet, doPost ...等不同的 http 請求、利用 ThreadLocal,將請求的相關資訊綁到當前的執行緒 ...等。直到最後的 DispatcherServlet,有了統一請求入口、有 IoC 容器的組件與環境資訊、也對每一條請求預備好了隔離的環境,因此 DispatcherServlet 的主要工作則聚焦在 MVC 層的相關方法上,在容器啟動時,初始化 Spring MVC 的 9 大組件,同時在運行時進行請求的調度。

此外,因為 DispatcherServlet 是 Spring MVC 框架(而非原生 Spring 框架)的系統組件,因此不同於原生組件的直接注入,DispatcherServlet 會透過一個叫做 DispatcherServletAutoConfiguration 的配置類別 @Import 進 IoC 容器的,整體邏輯類似於之前說過的 SPI 機制。DispatcherServlet 類別本身的程式內容不少,但大多都是一些 init 或者是 get 方法。具體來說,扣掉 initStrategies 會初始化 9 大組件以外,需要關注的邏輯就只有 2 個:doService 負責環境準備與狀態快照,它會把 Spring MVC 執行所需的上下文資料、屬性快照、與相關狀態塞進 request 裡面,同時做好環境隔離與清理;而 doDispatch 則是真正的調度方法,Spring MVC 的 9 大核心組件就會在這裡調用到的相關內容,因此,如果搞懂 doDispatch 的邏輯,就可以在一定的程度上,說是搞懂了 Spring MVC 的運作模式。

接下來我們說說 9 大組件:這個說法應該很常在其他地方查找到,它指的是會在 dispatcherServlet.initStrategies 裡進行初始化的 9 個 Spring MVC 相關元件。跟 BeanFactory 那裡比較不同的是,組件的初始化順序並不影響最後的功能,所以嚴格上來說誰先誰後都沒關係,後續的調用則定義在 doService 和 doDispatch 裡。它們分別是用來解析檔案請求並對其統一封裝的 MultipartResolver;解析與管理請求地區語系的 LocaleResolver;決定請求要使用哪一種佈景主題的 ThemeResolver;負責從 request 當中找到對應 controller 方法的 HandlerMapping;負責參數綁定、調用方法執行、和回傳值處理的 HandlerAdapter;統一處理請求異常的 HandlerExceptionResolver;從 Request URI 推斷視圖名稱的 RequestToViewNameTranslator;負責把視圖名稱解析成 View 物件的 ViewResolver;和管理 Redirect 時暫存資料的 FlashMapManager。

說完組件之後,我們把 doDispatch 的流程走一遍,整個方法的骨架其實並不複雜:doDispatch 首先會先執行一次 checkMultipart(),檢查 request 是不是一個附帶檔案的請求?如果是,這裡會呼叫 MultipartResolver,把原本的 request 包裝成一個 Multipart 類型的 request;接著會呼叫 getHandler(),依序遍歷 IoC 容器裡的 HandlerMapping,找出符合用來處理當前請求的 controller.method(),和執行方法之前需要經過的攔截器;接著透過 getHandlerAdapter() 找出對應的 HandlerAdapter;並依序執行攔截器鏈的前置方法、呼叫 ha.handle()、(走一遍 Controller, Service, DAO ...等我們最常接觸到的開發邏輯)、並回傳執行結果、回頭執行攔截器鏈的後置方法、把結果包裝成一個 ModelAndView,最後呼叫 processDispatchResult(),進行請求的收尾。過程中如果有例外被拋出,這個例外也進到 processDispatchResult() 並交給 HandlerExceptionResolver,如果一切正常(也就是沒有例外,或例外可以被預期的邏輯正確處理),就會進行 ModelAndView 的解析流程,使用 ViewResolver 完成畫面渲染,並回傳最後結果。

最後,簡單聊聊 9 大組件在現今環境下的存歿:扣除掉 ThemeResolver 已經在 6.x 系列的 Spring Framework 被標記棄用以外,在現代以前後端分離(或 Restful API)為主流的後端架構下,如果專案中沒有跟檔案相關的 API 邏輯,那麼 MultipartResolver 就不會用到;LocaleResolver 一般會配合 MessageSource 做 i18n 的配置跟解析,但和 MultipartResolver 有點類似,也是沒有國際化的需求就不會用到;FlashMapManager 和 RequestToViewNameTranslator 在實務上極度罕見,屬於雖然沒有被棄用但也名存實亡的情形;ViewResolver 也是在前後端分離的架構下完全棄用了,但因為我還是有一點 thymeleaf 的專案經驗,所以還是會拉一天的篇幅簡單介紹一下這個組件。總的來說,傳統定義的 9 大組件,扣掉前面的那 6 個,就只剩下 HandlerMapping, HandlerAdapter 和 HandlerExceptionResolver 這三個還活躍在目前的舞台上了。


上一篇
Day 18: ImportSelector —— 配置類別的動態導入橋樑與 SPI 機制
下一篇
Day 20: Filter —— Servlet 容器規範與請求前置攔截的責任鏈設計
系列文
30 篇淺談 Spring 框架的核心底層組件 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言