1. 程式人生 > >深入剖析Spring Web原始碼(九)

深入剖析Spring Web原始碼(九)

4.2.1.2 基於註解控制器流程的實現

上一節,我們詳細的分析了基於簡單控制器流程的實現,事實上,許多的簡單控制器的實現已經不被推薦使用。自從 2.5釋出以後, Spring開始鼓勵使用基於註解控制器的流程。基於註解的控制器流程具有實現方法簡單,程式程式碼清晰易讀等特點。

基於註解控制器的流程和基於簡單控制器的流程的實現非常相似,派遣器 Servlet在處理一個 HTTP請求的時候,它通過預設註解處理器對映 (DefaultAnnotationHandlerMapping) HTTP請求對映到響應的註解控制器 (@Contoller),然後,把控制流傳給註解方法處理器介面卡 (AnnotationMethodHandlerAdapter)

。註解方法處理器介面卡並不是簡單的傳遞控制流給註解控制器,而是以一定規則查詢註解控制器裡面的處理器方法,並且通過反射的方式對映 HTTP 請求資訊到方法引數,然後使用反射呼叫方法,得到方法的返回結果後,再根據一定的規則把返回結果對映到模型和檢視物件,進而返回給作為總控制器的派遣器 Servlet

事實上,預設註解處理器對映的實現重用了簡單控制器流程的處理器對映的實現體系結構。回顧上一小結中分析的 Bean URL處理器對映繼承自抽象探測 URL處理器對映,實現了其抽象方法 determineUrlsForHandler(),在這個方法實現中,把所有以左劃線 (/)開頭的 Bean名字註冊作為一個簡單的控制器。預設註解處理器對映的實現同樣也實現了抽象方法 determineUrlsForHandler()

,如果某個 Bean中使用了請求對映 (@RequestMappings)註解,則註冊這個 Bean作為一個註解控制器。如下類圖所示,

 

圖表 4 ‑20

如上圖所示,預設註解處理器對映實現了方法 determineUrlsForHandler(),查詢 Bean型別級別的請求對映註解和方法級別的請求對映註解,如果兩個級別的請求對映註解都存在,結合兩個級別的請求對映註解,否則使用方法級別的請求對映註解,構造出一個 URL Pattern集合,並且返回這個 URL Pattern集合,抽象探測 URL處理器對映就會使用這個集合的每一個元素作為關鍵字註冊當前的 Bean作為一個註解控制器。如下程式註釋,

在預設註解處理器對映中除了實現了提取註解處理器中配置的 URL Pattern 外,還改寫了一個校驗處理器的方法 validateHandler() ,這個方法的實現根據型別級別的請求對映的配置,校驗當前的請求是否能夠應用到這個處理器上。這個校驗方法是在派遣器 Servlet 將一個請求對映到響應的處理器的時候呼叫的。具體邏輯如下,

  • 如果處理器型別級別請求對映定義了 HTTP 方法,則當前 HTTP 請求方法必須是請求對映定義的這些 HTTP 方法之一。
  • 如果處理器型別級別請求對映定義了 HTTP 引數,則當前 HTTP 請求引數必須包含請求對映定義的所有的 HTTP 引數。
  • 如果處理器型別級別請求對映定義了 HTTP 頭,則 HTTP 請求頭必須包含請求對映定義的所有的 HTTP 頭。

否則,這個處理器不能處理當前的 HTTP 請求,丟擲異常,終止處理。可見,型別級別的請求對映是優先校驗的,方法級別的請求對映是後來校驗的,所以,我們得出一下結論。

  • 方法級別請求對映定義的 HTTP 方法必須是型別級別請求對映定義的 HTTP 方法的子集,否則沒有意義。
  • 方法級別請求對映定義的 HTTP 引數可以多餘型別級別請求對映定義的 HTTP 引數。
  • 方法級別請求對映定義的 HTTP 頭可以多餘型別級別請求對映定義的 HTTP 頭。

如下程式註釋,



通過以上我們的分析,我們理解在基於註解控制器流程的實現中,預設註解處理器對映是通過處理器中宣告的請求對映註解註冊註解控制器以及查詢註解控制器的。作為總控制器的派遣器 Servlet 通過 HTTP 請求得到一個註解控制器,將註解控制器等傳給註解方法處理器介面卡進行處理器方法的呼叫,對處理器方法的呼叫是通過反射實現的,在呼叫之前,需要通過反射從請求引數,請求頭等探測所有需要的引數,再呼叫後返回方法結果後,再通過一定規則對映結果到模型和檢視物件。這個流程是在註解方法處理器介面卡類和相關的支援類實現的,如下類圖所示,

 

圖表 4 ‑21

如上圖所示,註解方法處理器介面卡類實現了處理器介面卡介面。在 handle() 方法的實現中,使用兩個輔助類 Servlet 處理器方法解析器 (ServletHandlerMethodResolver) Servlet 處理器方法呼叫器 (ServletHandlerMethodInvoker) 利用反射的原理呼叫註解處理器中的處理器方法。在處理器方法呼叫之前,通過引數註解從 HTTP 請求中提取引數值,在處理器方法呼叫之後通過註解對映方法的返回值到模型和檢視物件,最後返回給派遣器 Servlet 進行檢視解析和檢視顯示。如下流程圖所示,

 

圖表 4 ‑22

根據上面流程圖顯示的順序,我們將深入的對程式碼進行剖析,派遣器 Servlet 從預設註解處理器對映得到了註解控制器後,將控制器傳遞給註解方法處理器介面卡的 handle() 方法, handle() 方法在進行通用的 HTTP 請求方法檢查和設定 HTTP 響應快取資訊後,根據需要對處理器方法進行同步或者非同步的呼叫,如下程式碼註釋,

 

如上程式碼註釋,對於如何解析處理器方法,如何解析引數,如何呼叫處理器方法以及如果對映返回值等的實現都是封裝在處理器方法解析器和處理器方法呼叫器的實現中的,我們稍後會深入剖析這些邏輯的實現。

註解方法處理器介面卡也對處理器介面卡介面的另外兩個方法進行實現,如下程式碼註釋,

  

如上程式註釋可見, supports() getLastModified() 的實現是非常簡單的,這裡不再詳細分析。通過上面的流程圖和程式碼註釋,我們也已經大體的瞭解了通過反射呼叫處理器方法的總體步驟,現在我們開始深入剖析註解方法處理器介面卡是如何實現方法解析,方法呼叫以及模型結果資料對映的。

如何解析處理器方法呢?

正如註解控制器的名字所示,它是基於註解資訊的控制器,這個控制器是一個普通的 Bean, 不需要實現任何介面或者繼承抽象類。一個註解控制器可能包含一個或者更多的處理器方法,這些處理器方法是用請求對映註解 (@RequestMapping) 標誌的,請求對映註解包含著用於匹配 HTTP 請求的 URI Pattern, 請求方法,請求引數,請求頭的資訊。這些在請求對映中宣告的資訊會用於匹配 HTTP 請求,如果匹配成功,則會使用匹配的處理器方法處理請求。我們首先分析請求對映註解 (@RequestMapping) 都包含哪些屬性資訊。

 

如上程式碼所示,宣告在一個處理器方法或者處理器型別級別的請求對映註解可以包含 URI Pattern, 請求 方法,請求 引數, 請求頭等資訊。在匹配的時候, URI Pattern 是最重要的匹配資訊,如果沒有指定 URI Pattern ,則使用其餘資訊匹配。下面我們分析註解方法處理器介面卡是如何使用這些資訊匹配一個 HTTP 請求到一個處理器方法的。

首先,註解方法處理器介面卡為每一個處理器型別建立一個處理器方法解析器,處理器方法解析器通過反射分析處理器型別,並且取得所有聲明瞭請求對映註解的處理器方法,初始化繫結方法,模型屬性方法。如下程式碼所示,

 

我們看到,對於一個處理器型別初始化一個處理器方法解析器,處理器方法解析器在解析處理器方法時使用了一個複雜的邏輯決定這些方法中的哪些方法可以處理當前 HTTP 請求。如果多個處理器方法可以處理當前的請求,那麼選擇最佳匹配的處理器方法。以下詳細分析這個工作流程。

在初始化階段,我們已經儲存了所有聲明瞭請求對映註解的處理器方法。現在我們遍歷所有的處理器方法,判斷是否此方法支援當前的 HTTP 請求。

如果請求對映註解的處理器方法包含 URI Pattern 的資訊,那麼對於每一個 URI Pattern ,則檢視是否存在型別級別的 URI Pattern ,如果存在,則結合型別級別的 URI Pattern 。否則,如果最佳匹配的 URI Pattern 存在,則結合最佳匹配的 URI Pattern 。否則單獨使用處理器方法級別的 URI Pattern 匹配當前的查詢路徑。如果 URI Pattern 匹配成功,檢視是否 HTTP 請求匹配宣告的請求方法,請求引數和請求頭。如果這些資訊都匹配成功,則添加當前 URI Pattern 到匹配路徑集合中,並通過路徑匹配對比器對匹配路徑集合進行排序,同事標識當前處理器方法為匹配。

如果請求對映註解的處理器方法不包含 URI Pattern 的資訊,則只需要檢視是否匹配宣告的請求方法,請求引數和請求頭。如果這些資訊匹配,則認為當前處理器方法為匹配。一種特殊情況是,如果請求對映註解中沒有宣告 HTTP 方法和引數,那麼首先使用處理器方法名解析器解析處理器方法名,如果解析的方法名和當前處理器方法相同,則認為當前處理器方法為匹配。預設的方法名解析器是通過去掉 URI 最後一部分的檔名副檔名得到的。

如果某一個處理器方法匹配,儲存這個處理器方法到匹配的處理器方法集合中。如果處理器集合中已經存在一個處理器方法,而且已存處理器方法和現在處理器方法不是同一個處理器方法,那麼我們需要解析衝突。在這種情況下,如果沒有路徑資訊,我們需要使用方法名解析器解析最佳處理器方法。處理規則如下,

1.        如果已存處理器方法和當前處理器方法名相同,則使用後解析的方法。

2.        否則,如果解析的方法名和已存處理器方法同名,繼續使用已存處理器方法。如果解析的方法名和當前處理器方法同名,則使用當前處理器方法。

3.        如果解析的方法名既不等於當前處理器方法也不等於已存的處理器方法,則丟擲異常,終止處理。

根據上面的邏輯分析,最終如果有一個或者多個處理器方法匹配當前 HTTP 請求,則通過請求對映資訊對比器找到最佳匹配的處理器方法。如下程式碼所示,

  

如何解析處理器方法引數呢?

註解方法處理器介面卡是通過請求對映註解解析處理器方法的。解析得到了處理器方法,在呼叫處理器方法之前我們必須首先解析所有的處理器方法引數。處理器方法引數也是通過各種註解標記的,不同的註解包含著資訊指導註解方法處理器介面卡從不同的資料來源取得資料。下面我們詳細分析,處理器方法引數所支援的所有註解。

最常用的應用在處理器方法引數上的註解是請求引數註解 (@RequestParam) 。 請求引數註解指導註解方法處理器介面卡通過引數名字找到請求引數值,並且賦值給當前方法引數。如下程式碼註釋,

  

請求頭註解 (@RequestHeader) 指導註解方法處理器介面卡通過頭名字找到請求頭的值,並且賦值給當前方法引數。如下程式碼註釋,

 

請求體註解 (@RequestBody) 指導註解方法處理器介面卡通過訊息轉換器將請求體轉換成 Java 物件作為當前方法引數的值。但是請求註解並沒有宣告任何屬性資訊,它只是個標誌,指導註解方法處理器介面卡為當前引數解析請求體。如下程式碼註釋,

 

Cookie 值註解 (@CookieValue) 指導註解方法處理器介面卡通過 Cookie 名字找到 Cookie 的值,並且賦值給當前方法引數。如下程式碼註釋,

 

路徑變數註解 (@PathVariable) 指導註解方法處理器介面卡通過路徑變數名字找到路徑變數的值 ( 路徑變數通常被稱為模板變數 ) ,並且賦值給當前方法引數。如下程式碼註釋,

 

模型屬性註解 (@ModelAttribute) 指導註解方法處理器介面卡通過模型屬性名字找到模型屬性的值,並且賦值給當前方法引數。模型屬性註解也可以用於宣告在方法上,這種情況下把方法的返回值作為模型資料值放入隱式模型中。如下程式碼註釋,

 

對於一個處理器方法引數,只能宣告上面的註解中的一個,或者不宣告註解。如果聲明瞭上面註解中的一個,則根據註解解析處理器方法引數的值。如果一個處理器方法引數沒有宣告任何註解,則檢視是否配置有客戶化 Web 引數解析器,如果存在則使用客戶化 Web 引數解析器進行解析。如果沒有配置客戶化 Web 引數解析器或者客戶化 Web 引數解析器不能解析當前引數,則判斷是不是標準的 WebRequest 型別,如果是