小白都能看懂的 Spring 源碼揭秘之Spring MVC

前言

對於 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 獲取到對應的方法並進行執行最終完成整個流程。

Tags: