今天我們來講講 HandlerMapping,看看 DispatcherServlet 在收到一個 HTTP 請求之後,首先做了哪些事情。HandlerMapping 本身是一個 Spring MVC 的核心介面,具體的功能是協助 DispatcherServlet 找到負責處理請求的 Handler method,可以簡單理解它是負責建立外部 HTTP 請求與後端處理程式(通常是 Controller 方法)的一個橋樑。與 DispatcherServlet 不同,實務上一個 IoC 容器裡面會存在多個不同的 HandlerMapping 實例,它們分別用來處理一般的 http 請求、css 或 javascript 等靜態資源請求、WebSocket 握手、或 Welcome Page ...等不同的路由解析。HandlerMapping 掛載到 IoC 容器的方式也跟 DispatcherServlet 很類似:都是先透過 main 方法定義的 @SpringBootApplication 註解、解析 @EnableAutoConfiguration 註解、發現 AutoConfigurationImportSelector 類別,透過 xxx.imports 載入配置類別(WebMvcAutoConfiguration),最後取出 HandlerMapping 物件注入到 IoC 容器。也因為大部分的 MVC 組件都是這麼被注入的,後續的 MVC 組件如果沒有特別提及,就可以自動視為是相同的流程。
HandlerMapping 介面內有非常多的常數定義,但具體需要實作的方法只有 getHandler 一個。getHandler 會由 DispatcherServlet 在收到請求之後首先呼叫,方法的參數是 HttpServletRequest,這個應該不陌生,就是經由 Servlet 包裝過後的 http 請求,預期的回傳則是 HandlerExecutionChain,這裡有一個稍微反直覺的地方:因為攔截器的查找邏輯通常跟請求的 URL 是強綁定的,加上 Handler 的查找也是對 URL 強綁定,因此 HandlerMapping 在 getHandler 並不會單純回傳一個 Handler 物件,它回傳的是包含 Handler(負責處理請求的對應物件,通常就是 Controller 的某一個方法)和攔截器鏈(負責在請求進出 Controller 前後做切面控制」)的包裝類別。實作細節可以翻翻 AbstractHandlerMapping 的 getHandler 方法,這裡因為篇幅的原因就不多贅述。
實務上,我們最常用的 HandlerMapping 叫做 RequestMappingHandlerMapping,它的生命週期跟一般的 Bean 大致相同,我們可以把它簡單想成就是個普通的 Bean。在屬性注入完成後的初始化階段,它會向 IoC 容器索取一個 Bean 的清單,並從中找出所有標記了 @Controller 或 @RequestMapping 註解的 Bean 類別。找到 Controller 的目的是要解析出裡面的 HandlerMethod —— 透過掃描類別的每一個方法,抓出所有 @GetMapping、@PostMapping ...等註解,同時解析註解的請求方式、URL 路徑、或參數與 Header 條件 ...等,再把這些 Metadata 封裝成一條條路徑的匹配規則,最終建立一套可供查詢的映射索引表。後續當 DispatcherServlet 收到 HTTP 請求,HandlerMapping 就可以透過映射表快速回覆可否處理。
再來講講 HandlerMapping 本身的責任鏈模式:前面的段落有說過,在正常的情況下,IoC 容器內部會同時存在多個不同的 HandlerMapping 實例。具體來說,當 DispatcherServlet 在初始化的時候,它會向 IoC 容器取得所有實作 HandlerMapping 介面的 Bean,並存成一個 List 的執行鏈清單。後續當 HTTP 請求進到 Servlet 容器,就會按照這個清單的先後順序詢問 HandlerMapping 是否可以處理這條請求。大方向來說,只要前面的 Mapping 可以處理請求,並回傳一組 HandlerExecutionChain,DispatcherServlet 就會中斷輪詢並回傳路徑的匹配結果。HandlerMapping 的順序嚴格來說並不隨機,是依據 Spring 框架的 Ordered 介面(或 @Order 註解)所定義的優先順序進行排列,上一段說到的 RequestMappingHandlerMapping 在所有預設的 HandlerMapping 中,就是處於最高的優先級別。
最後,我們簡單講講 404 和 405 的差異:在 DispatcherServlet 的視角裡,如果所有責任鏈上的 HandlerMapping 都無法對請求做到認領匹配,這時 DispatcherServlet 就判定為此次請求所對應的系統資源並不存在,同時回傳 404 的 status code,告知調用者 Not Found(這時如果系統有設定 404 頁面的自動跳轉,就會回傳系統對應的 error page)。但如果 HandlerMapping 其實對 URL 的匹配成立,只是請求方法對應不上(例如某 API 限定用 GET 的方式詢問,但打進來的 API 請求是 POST),這時的 DispatcherServlet 並不會把它當作不存在的資源處理,而是改丟 405 的 Method Not Allowed,告知前端「路徑其實是對的,只是方法不對。」後續只要前端調整請求的呼叫方式,就可以訪問到正確的資源,並運行 Controller 內的後續邏輯。
期待後續各位的閱讀與分享,我是 Pax,我們明天見。