今天來講講 HandlerAdapter,研究一下 Spring MVC 在拿到請求對應的處理方法之後做了什麼事情:前面有說過,當一個請求進到 DispatcherServlet 之後,會先呼叫 getHandler 方法,拿到對應 HandlerExecutionChain,裡面則包含一個 handler 和攔截器鏈。但如果我們仔細觀察,可以發現這個 handler 回傳的是 Object,並不是其他相對精確的類別。這樣做的原因有幾個:最明顯的原因是,handler 物件除了是我們最常見的 HandlerMethod 類別以外,早期 的 Spring 並不以方法作為回傳,它回傳的是「實作 Controller 介面的實體類別」、或如果我們整合了其他套件,這裡的 handler 也可以變成第三方的 Servlet 物件、或 WebSocket 的 Endpoint 實例 ...等。如果強制定義一個 Handler 介面,其實無法歸納介面內的通用方法簽名,而單純做成標記類別也只是方便閱讀,更遑論底層還會額外多出一些轉換成本。
為此,Spring MVC 的解決辦法是在 HandlerMapping 的後面做個統一的規格定義。無論 handler 是封裝過的 Method、Controller 類別、還是第三方的 Servlet(或其他物件),透過 HandlerAdapter 的適配器轉換,Spring MVC 都能實現六角架構中的防腐操作,在盡可能支持外部輸入類型的情況下,保持內部的後續類別只需要處理統一的介面契約即可,降低了後續開發與維護的任務難度。HandlerAdapter 本身是一個介面,內部定義了 3 個契約方法:supports 用來判斷 getHandler 拿到的 handler 實例可否被當前的 HandlerAdapter 處理;handle 是真正的調用方法,負責處理參數的綁定與型態轉換、調用真正的業務邏輯,並將回傳結果統一封裝為 ModelAndView(或直接 Response 寫入);getLastModified 則負責檢查請求可不可以快速返回 304 的 status code,在現今的 Spring MVC 已經很少使用。
實務上,目前最常用到的 HandlerAdapter 叫做 RequestMappingHandlerAdapter(簡稱 RMHA)。我們前一天有說過:如果我們使用 @RestController + @GetMapping(或 @PostMapping ...等)標記 Controller 的方法,最後就會被 HandlerMapping 封裝成一個 HandlerMethod 物件。RMHA 就是專門用來處理 HandlerMethod 的適配器,它可以做的事情很多,基本包辦了例外處理和視圖解析以外,所有請求響應需要做到(或提供)的所有事情。像是全域的 Spring MVC 切面通知與資料前置處理、異步請求的資料管控、HTTP 快取控制與驗證、Session 的狀態管理跟資料拷貝 ...等,但在初步認識上,我們可以先不用管那麼多,只要記得以下的 4 種核心工作就好,分別是參數解析、資料的校驗與綁定、調用 HandlerMethod、和返回值處理。
參數解析上,RMHA 會委託一個叫做 HandlerMethodArgumentResolver(簡稱 Resolver)的組件協同完成。Resolver 的用途是把請求資料映射成 Controller 方法中的各種參數。一般我們在方法當中寫到的 @RequestParam, @PathVariable, @RequestBody ...等,就是由 Resolver 負責處理。這裡也是 Spring MVC 的預設流程中,Request Body 的輸入串流(Stream)真正被讀取和處理的地方。Resolver 的實例也很多,分別用來處理不同的註解、Header 資料、或 multipart 檔案 ...等,容器內部也是採取一貫的責任鏈模式,把解析器排序,並按照順序嘗試處理。此外,Resolver 的執行粒度以參數為單位,這代表當請求走到這裡之後,RMHA 會先把 Controller 方法裡的每一個輸入參數都拿出來,再按照順序依次調用完整的 resolver chain。這也是為什麼如果我們在同一個方法上掛載不同的註解(例如同時標了 @RequestHeader, @PathVariable 和 @RequestParam ...等),Spring 都能從正確的地方解析資料的原因。
在參數解析完成之後,通常就會進到資料驗證的相關邏輯。這裡可以注意的地方是:如果我們回頭檢查 RMHA 的原始程式,會發現到裡面並沒有一個 doDataValidate 之類的模板方法。具體來說,參數驗證其實是部分特定 Resolver 內部在完成參數解析之後,會自主呼叫的下一層邏輯。以解析 @RequestBody 的 Resolver 來說,處理資料驗證的相關邏輯就是寫在 validateIfApplicable 這個方法裡面。在這裡,Resolver 會委託 Spring MVC 的其他組件(像是 WebDataBinder 或 Validator ...等)負責對參數進行校驗與檢查,這裡也是 @Min, @Max ...等註解的真實觸發位置。這裡可以提的小東西是:因為 @RequestBody 和 @ModelAttribute 的 Resolver 本來就有資料驗證的相關邏輯,所以這兩種類型的資料天然支援註解驗證;但 @RequestParam 或 @PathVariable 的 Resolver 在預設上並不實作資料驗證的相關邏輯,因此如果我們只單純把 @Min 寫在標記 @RequestParam 參數上,校驗的工作是不會被正確觸發的,會需要額外的調整方式才能執行。
最後,返回值處理指的是當 controller 方法已經把所有業務邏輯執行完成後,負責處置 Controller 方法回傳值的過程,主要由 HandlerMethodReturnValueHandler(簡稱 ReturnHandler)負責。單以現今的角度來看,如果程式掛上 @RestController(或 @ResponseBody)註解,這裡就可以把 ReturnHandler 簡單理解為負責把 Controller 的回傳值轉換成 response data 的 MVC 組件;如果是前後端耦合的 Spring 專案,ReturnHandler 也會解析視圖名稱或協同渲染頁面。也跟其他組件一樣,RMHA 有一個 ReturnHandler 責任鏈,並在 Conreoller 回傳時遍歷處理。ReturnHandler 的輸入參數有一個 mavContainer。如果是 @RestController 或 @ResponseBody 這類的架構,預設的處理器就會在處理過程將 mavContainer 的 requestHandled 屬性設為 true,進而跳過後續的視圖相關流程。
期待後續各位的閱讀與分享,我是 Pax,我們明天見。