小白都能看懂的 Spring 原始碼揭祕之Spring MVC

雙子孤狼發表於2022-03-27

前言

對於 Web 應用程式而言,我們從瀏覽器發起一個請求,請求經過一系列的分發和處理,最終會進入到我們指定的方法之中,這一系列的的具體流程到底是怎麼樣的呢?

Spring MVC 請求流程

記得在初入職場的時候,面試前經常會背一背 Spring MVC 流程,印象最深的就是一個請求最先會經過 DispatcherServlet 進行分發處理,DispatcherServlet 就是我們 Spring MVC 的入口類,下面就是一個請求的大致流轉流程(圖片參考自 Spring In Action):

  1. 一個請求過來之後會到達 DispatcherServlet,但是 DispatcherServlet 也並不知道這個請求要去哪裡。
  2. DispatcherServlet 收到請求之後會去查詢處理器對映(HandlerMapping),從而根據瀏覽器傳送過來的 URL 解析出請求最終應該呼叫哪個控制器。
  3. 到達對應控制器(Controller)之後,會完成一些邏輯處理,而且在處理完成之後會生成一些返回資訊,也就是 Model,然後還需要選擇對應的檢視名。
  4. 將模型(Model)和檢視(View)傳遞給對應的檢視解析器(View Resolver),檢視解析器會將模型和檢視進行結合。
  5. 模型和檢視結合之後就會得到一個完整的檢視,最終將檢視返回前端。

上面就是一個傳統的完整的 Spring MVC 流程,為什麼要說這是傳統的流程呢?因為這個流程是用於前後端沒有分離的時候,後臺直接返回頁面給瀏覽器進行渲染,而現在大部分應用都是前後端分離,後臺直接生成一個 Json 字串就直接返回前端,不需要經過檢視解析器進行處理,也就是說前後端分離之後,流程就簡化成了 1-2-3-4-7(其中第四步返回的一般是 Json 格式資料)。

Spring MVC 兩大階段

Spring MVC主要可以分為兩大過程,一是初始化,二就是處理請求。初始化的過程主要就是將我們定義好的 RequestMapping 對映路徑和 Controller 中的方法進行一一對映儲存,這樣當收到請求之後就可以處理請求呼叫對應的方法,從而響應請求。

初始化

初始化過程的入口方法是 DispatchServletinit() 方法,而實際上 DispatchServlet 中並沒有這個方法,所以我們就繼續尋找父類,會發現 init 方法在其父類(FrameworkServlet)的父類 HttpServletBean 中。

HttpServletBean#init()

在這個方法中,首先會去家在一些 Servlet 相關配置(web.xml),然後會呼叫 initServletBean() 方法,這個方法是一個空的模板方法,業務邏輯由子類 FrameworkServlet 來實現。

FrameworkServlet#initServletBean

這個方法本身沒有什麼業務邏輯,主要是初始化 WebApplicationContext 物件,WebApplicationContext 繼承自 ApplicationContext,主要是用來處理 web 應用的上下文。

FrameworkServlet#initWebApplicationContext

initWebApplicationContext() 方法主要就是為了找到一個上下文,找不到就會建立一個上下文,建立之後,最終會呼叫方法 configureAndRefreshWebApplicationContext(cwac) 方法,而這個方法最終在設定一些基本容器標識資訊之後會去呼叫 refresh() 方法,也就是初始化 ioc 容器

當呼叫 refresh() 方法初始化 ioc 容器之後,最終會呼叫方法 onRefresh(),這個方法也是一個模板鉤子方法,由子類實現,也就是回到了我們 Spring MVC 的入口類 DispatcherServlet

DispatchServlet#onRefresh

onRefresh() 方法就是 Spring MVC 初始化的最後一個步驟,在這個步驟當中會初始化 Spring MVC 流程中可能需要使用到的九大元件。

Spring MVC 九大元件

MultipartResolver

這個元件比較熟悉,主要就是用來處理檔案上傳請求,通過將普通的 Request 物件包裝成 MultipartHttpServletRequest 物件來進行處理。

LocaleResolver

LocaleResolver 用於初始化本地語言環境,其從 Request 物件中解析出當前所處的語言環境,如中國大陸則會解析出 zh-CN 等等,模板解析以及國際化的時候都會用到本地語言環境。

ThemeResolver

這個主要是使用者主題解析,在 Spring MVC 中,一套主題對應一個 .properties 檔案,可以存放和當前主題相關的所有資源,如圖片,css樣式等。

HandlerMapping

用於查詢處理器(Handler),比如我們 Controller 中的方法,這個其實最主要就是用來儲存 url 和 呼叫方法的對映關係,儲存好對映關係之後,後續有請求進來,就可以知道呼叫哪個 Controller 中的哪個方法,以及方法的引數是哪些。

HandlerAdapter

這是一個介面卡,因為 Spring MVC 中支援很多種 Handler,但是最終將請求交給 Servlet 時,只能是 doService(req,resp) 形式,所以 HandlerAdapter 就是用來適配轉換格式的。

HandlerExceptionResolver

這個元件主要是用來處理異常,不過看名字也很明顯,這個只會對處理 Handler 時產生的異常進行處理,然後會根據異常設定對應的 ModelAndView,然後交給 Render 渲染成頁面。

RequestToViewNameTranslator

這個主鍵主要是從 Request 中獲取到檢視名稱。

ViewResolver

這個元件會依賴於 RequestToViewNameTranslator 元件獲取到的檢視名稱,因為檢視名稱是字串格式,所以這裡會將字串格式的檢視名稱轉換成為 View 型別檢視,最終經過一系列解析和變數替換等操作返回一個頁面到前端。

FlashMapManager

這個主鍵主要是用來管理 FlashMap,那麼 FlashMap 又有什麼用呢?要明白這個那就不得不提到重定向了,有時候我們提交一個請求的時候會需要重定向,那麼假如引數過多或者說我們不想把引數拼接到 url 上(比如敏感資料之類的),這時候怎麼辦呢?因為引數不拼接在 url 上重定向是無法攜帶引數的。

FlashMap 就是為了解決這個問題,我們可以在請求發生重定向之前,將引數寫入 request 的屬性 OUTPUT_FLASH_MAP_ATTRIBUTE 中,這樣在重定向之後的 handler 中,Spring 會自動將其設定到 Model 中,這樣就可以從 Model 中取到我們傳遞的引數了。

處理請求

在九大元件初始化完成之後,Spring MVC 的初始化就完成了,接下來就是接收並處理請求了,那麼處理請求的入口在哪裡呢?處理請求的入口方法就是 DispatcherServlet 中的 doService 方法,而 doService 方法又會呼叫 doDispatch 方法。

DispatcherServlet#doDispatch

這個方法最關鍵的就是呼叫了 getHandler 方法,這個方法就是會獲取到前面九大元件中的 HandlerMapping,然後進行反射呼叫對應的方法完成請求,完成請求之後後續還會經過檢視轉換之類的一些操作,最終返回 ModelAndView,不過現在都是前後端分離,基本也不需要用到檢視模型,在這裡我們就不分析後續過程,主要就是分析 HandlerMapping 的初始化和查詢過程。

DispatcherServlet#getHandler

這個方法裡面會遍歷 handllerMappings,這個 handllerMappings 是一個 List 集合,因為 HandlerMapping 有多重實現,也就是 HandlerMapping 不止一個實現,其最常用的兩個實現為 RequestMappingHandlerMappingBeanNameUrlHandlerMapping

AbstractHandlerMapping#getHandler

AbstractHandlerMapping 是一個抽象類,其 getHandlerInternal 這個方法也是一個模板方法:

getHandlerInternal 方法最終其會呼叫子類實現,而這裡的子類實現會有多個,其中最主要的就是 AbstractHandlerMethodMappingAbstractUrlHandlerMapping 兩個抽象類,那麼最終到底會呼叫哪個實現類呢?

這時候如果拿捏不準我們就可以看一下類圖,上面我們提到,HandlerMapper 有兩個非常主要的實現類:RequestMappingHandlerMappingBeanNameUrlHandlerMapping。那麼我們就分別來看一下這兩個類的類圖關係:

可以看到,這兩個實現類的抽象父類正好對應了 AbstractHandlerMapping 的兩個子類,所以這時候具體看哪個方法,那就看我們想看哪種型別了。

  • RequestMappingHandlerMapping:主要用來儲存 RequestMapping 註解相關的控制器和 url 的對映關係。

  • BeanNameUrlHandlerMapping:主要用來處理 Bean name 直接以 / 開頭的控制器和 url 的對映關係。

其實除了這兩種 HandlerMapping 之外,Spring 中還有其他一些 HandllerMapping,如 SimpleUrlHandlerMapping 等。

提到的這幾種 HandlerMapping,對我們來說最常用,最熟悉的那肯定就是 RequestMappingHandlerMapping ,在這裡我們就以這個為例來進行分析,所以我們應該

AbstractHandlerMethodMapping#getHandlerInternal

這個方法本身也沒有什麼邏輯,其主要的核心查詢 Handler 邏輯在 lookupHandlerMethod 方法中,這個方法主要是為了獲取一個 HandlerMethod 物件,前面的方法都是 Object,而到這裡變成了 HandlerMethod 型別,這是因為 Handler 有各種型別,目前我們已經基本跟到了具體型別之下,所以型別就變成了具體型別,而如果我們看的的另一條分支線,那麼返回的就會是其他物件,正是因為支援多種不同型別的 HandlerMapping 物件,所以最終為了統一執行,才會需要在獲得 Hanlder 之後,DispatcherServlet 中會再次通過呼叫 getHandlerAdapter 方法來進一步封裝成 HandlerAdapter 物件,才能進行方法的呼叫

AbstractHandlerMethodMapping#lookupHandlerMethod

這個方法主要會從 mappingRegistry 中獲取命中的方法,獲取之後還會經過一系列的判斷比較判斷比較,因為有些 url 會對應多個方法,而方法的請求型別不同,比如一個 GET 方法,一個 POST 方法,或者其他一些屬性不相同等等,都會導致最終命中到不同的方法,這些邏輯主要都是在 addMatchingMappings 方法去進一步實現,並最終將命中的結果加入到 matches 集合內。

在這個方法中,有一個物件非常關鍵,那就是 mappingRegistry,因為最終我們根據 url 到這裡獲取到對應的 HandlerMtthod,所以這個物件很關鍵:

看這個物件其實很明顯可以看出來,這個物件其實只是維護了一些 Map 物件,所以我們可以很容易猜測到,一定在某一個地方,將 urlHandlerMapping 或者 HandlerMethod 的對映關係存進來了,這時候其實我們可以根據 getMappingsByUrl 方法來進行反推,看看 urlLookup 這個 Map 是什麼時候被存入的,結合上面的類圖關係,一路反推,很容易就可以找到這個 Map 中的對映關係是 AbstractHandlerMethodMapping 物件的 afterPropertiesSet 方法實現的(AbstractHandlerMethodMapping 實現了 InitializingBean 介面),也就是當這個物件初始化完成之後,我們的 urlHandler 對映關係已經存入了 MappingRegistry 物件中的集合 Map 中。

AbstractHandlerMethodMapping 的初始化

afterPropertiesSet 方法中並沒有任何邏輯,而是直接呼叫了 initHandlerMethods

AbstractHandlerMethodMapping#initHandlerMethods

initHandlerMethods 方法中,首先還是會從 Spring 的上下文中獲取所有的 Bean,然後會進一步從帶有 RequestMapping 註解和 Controller 註解中的 Bean 去解析並獲得 HandlerMethod

AbstractHandlerMethodMapping#detectHandlerMethods

這個方法中,其實就是通過反射獲取到 Controller 中的所有方法,然後呼叫 registerHandlerMethod 方法將相關資訊註冊到 MappingRegistry 物件中的各種 Map 集合之內:

AbstractHandlerMethodMapping#register

registerHandlerMethod 方法中會直接呼叫 AbstractHandlerMethodMapping 物件持有的 mappingRegistry 物件中的 regidter 方法,這裡會對 Controller 中方法上的一些元資訊進行各種解析,比如引數,路徑,請求方式等等,然後會將各種資訊註冊到對應的 Map 集合中,最終完成了整個初始化。

總結

本文重點以 RequestMappingHandlerMapping 為例子分析了在 Spring 當中如何初始化 HandlerMethod,並最終在呼叫的時候又是如何根據 url 獲取到對應的方法並進行執行最終完成整個流程。

相關文章