SpringMVC之RequestMapping執行過程(HandlerMapping篇)
寫在前面
從前一篇引導篇 here 的分析來看,如果我們想弄清楚 請求物件 HttpServletRequest 和 方法處理器 HandlerMethod 的對應關係,我們可以去 RequestMappingHandlerMapping 中去尋找“真相”。
我們看待這個類,需要從兩個階段去分析:
-
預處理部分:HandlerMethod 是如何 掃描註冊 到 HandlerMapping 中去的?
-
執行部分:當一個請求 HttpServletRequest 到來,SpringMVC 又是如何 匹配獲取 到合適的 HandlerMethod 的?
!提醒:考慮到篇幅安排,執行部分還需要分多篇來講解,因此本文主要針對預處理部分進行講解
快速開始
我還是比較喜歡寫單元測試,一方面,單元測試的執行速度比啟動一個完整專案要快數十倍;另一方面,單元測試的書寫過程中,更容易讓我們記住我們忽略了那些細節。這是我掌握原始碼的一個方法,如果你不喜歡,可以跳過該小節,直接進入分析部分。
有需要的可以到 Gitee here 下載原始碼,以 maven 開啟專案,使用 handler-method-mapping 模組。
UserController.java
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; @Controller @RequestMapping("/user") public class UserController { @RequestMapping("/info") public ModelAndView user(String name, int age) { System.out.println("name=" + name); System.out.println("age=" + age); return null; } }
我們需要 RequestMappingHandlerMapping 來作為我們儲存 HandlerMethod 的容器,因此我們新建這個物件。
設計測試目標:假如 getHandler 能夠返回一個非空物件,那麼就說明註冊成功了。
Tips:getHandler 方法需要一個請求物件,來自 spring-test 的 MockHttpServletRequest 來測試最合適不過了。
第 1 次嘗試
import org.junit.Assert; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; public class UserControllerTest { private RequestMappingHandlerMapping handlerMapping; private MockHttpServletRequest request; @Test public void initTest() throws Exception { request = new MockHttpServletRequest("GET", "/user/info"); handlerMapping = new RequestMappingHandlerMapping(); HandlerExecutionChain chain = handlerMapping.getHandler(request); Assert.assertNotNull(chain); } }
測試結果:測試不通過
失敗原因:一通反向追蹤後發現,答案就在 initHandlerMethods() 這個方法中。節選程式碼片段如下:
如果不呼叫 afterPropertiesSet(),就不會初始化所有處理器。
第 2 次嘗試
在日常開發時,afterPropertiesSet() 都是 Spring Bean 的生命週期中呼叫的,現在我們自己來主動呼叫一下。
handlerMapping.afterPropertiesSet();
測試結果:測試不通過
ApplicationObjectSupport instance does not run in an ApplicationContext
失敗原因:我們需要給 HandlerMethod 設定應用上下文。
ctx = new StaticWebApplicationContext();
handlerMapping.setApplicationContext(ctx);
Tips:同樣使用來自 spring-test 的 StaticWebApplicationContext 會更加簡單。
第 3 次嘗試
現在的測試程式碼如下
UserControllerTest.java 點選展開
import org.junit.Assert; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
public class UserControllerTest {
private RequestMappingHandlerMapping handlerMapping; private MockHttpServletRequest request; private StaticWebApplicationContext ctx; @Test public void initTest() throws Exception { request = new MockHttpServletRequest("GET", "/user/info"); handlerMapping = new RequestMappingHandlerMapping(); ctx = new StaticWebApplicationContext(); // 在 afterPropertiesSet() 呼叫之前設定上下文 handlerMapping.setApplicationContext(ctx); handlerMapping.afterPropertiesSet(); HandlerExecutionChain chain = handlerMapping.getHandler(request); Assert.assertNotNull(chain); }
}
需要注意的是,setApplicationContext 的呼叫必須在 afterPropertiesSet 之前。
測試結果:測試不通過
失敗原因:Spring 容器中沒有相應的 Controller Bean,需要我們自己來註冊。
// 為程式上下文注入 UserController Bean
ctx.getBeanFactory().registerSingleton("userController", new UserController());
現在我們就測試通過了,現在我們再來研究一下初始化所有 HandlerMethod 的方法。
概覽初始化所有 HandlerMethod
初始化所有的 HandlerMethod 的過程:
-
獲取所有的 Bean:從 Spring 容器中獲取所有的 Bean,isHandler 方法篩選出帶 @RequestMapping 或者 @Controller 的 Bean。
-
獲取所有方法:從 Bean 中取出所有的方法,篩選出帶 @RequestMapping 的方法
-
封裝 RequestMappingInfo : 根據註解封裝對映條件
-
建立 HandlerMethod
-
儲存對映到 MappingRegistry
AbstractHandlerMethodMapping 實現了 InitializingBean 介面。
afterPropertiesSet() 觸發 AbstractHandlerMethodMapping 的初始化,掃描註冊了所有 HandlerMethod。
解析 detectHandlerMethods 核心原始碼
1.isHandler 方法
因此我們的 XXXController 必須有類註解 @Controller 或者 @RequestMapping。
2.ReflectionUtils.doWithMethods 靜態方法
這個方法遍歷的當前類,當前類的父類,(當前類的介面以及當前類所有父類介面)中的方法。這個功能主要依靠第一個引數 Class<?> clazz 中的成員方法。
找到了這麼多 Method,但並不是所有都有用,因此需要過濾不需要的方法。此時需要藉助第二個引數 MethodFilter mf,這是一個函式式介面,僅包含一個介面方法。
找到的每一個方法,都需要相同的處理策略。此時就需要藉助第三個引數 MethodCallback mc,這同樣是函式式介面。
總而言之,這個工具類把複雜的遍歷和遞迴方法封裝起來,呼叫者可以更專注於“要拿哪些 Method,做何種操作”的問題上。
3.ReflectionUtils.USER_DECLARED_METHODS 常量物件
對於第三個過濾引數 MethodFilter mf,原始碼中用到了這個:
public static final MethodFilter USER_DECLARED_METHODS =
(method -> !method.isBridge() && !method.isSynthetic() && method.getDeclaringClass() != Object.class);
這個方法過濾器物件,表示過濾橋接方法,合成方法以及Object的自帶方法。換言之,篩選出應用程式設計師寫的方法。
橋接方法,合成方法都是 JVM 編譯器編譯時的產物。橋接方法主要和泛型的編譯有關,合成方法主要和巢狀類和私有成員的編譯相關。隱祕而詭異的Java合成方法 here
4.MethodIntrospector.selectMethods 靜態方法
這個方法,是在上面第 2 個方法的基礎上,把 Java 動態代理生成的類考慮進去了。
- Proxy.isProxyClass
- ClassUtils.getMostSpecificMethod
- BridgeMethodResolver.findBridgedMethod
以上幾個方法雖然複雜,但是如果你沒有用動態代理來生成 Controller 物件,是不需要過分關注的,我這裡也就不過度研究了。
但是這個方法的第二個引數 MetadataLookup<T> metadataLookup 也是一個函式式介面。每找到一個應用程式設計師寫的 Controller 方法,就會回撥一次,詢問呼叫者要拿 Method 返回一個什麼物件。
5.getMappingForMethod
方法呼叫時機:上面第 4 個方法執行時,每找到一個“應用程式設計師”寫的 Controller Bean 中的 Method 就會回撥一次 getMappingForMethod 建立一個 RequestMappingInfo 物件。
getMappingForMethod 原始碼點選展開檢視
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) {
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
}
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).build().combine(info);
}
}
return info;
}
解析:
-
createRequestMappingInfo 使用 @RequestMapping 註解中的屬性填充 RequestMappingInfo 的成員變數。屬性一一對應成員變數。
-
Controller 類上的註解建立的 RequestMappingInfo 需要與方法上的註解建立的 RequestMappingInfo 合併(combine)後作為日後請求的匹配條件。
6.registerHandlerMethod
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
this.mappingRegistry.register(mapping, handler, method);
}
該方法向 MappingRegistry 註冊對映。
在註冊時會呼叫
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
createHandlerMethod 將 Controller Bean(即 handler)和 Bean 的每一個 Method 組合生成一個 HandlerMethod。
總結
在 Spring 容器建立 RequestMappingHandlerMapping Bean 的過程中,會執行初始化 afterPropertiesSet(),觸發初始化所有 HandlerMethod。
初始化 HandlerMethod 的過程:
- 掃描 Spring 容器中的所有 Controller Bean
- 找出 Controller Bean 中的所有方法
- 建立 RequestMappingInfo
- 建立 HandlerMethod
- 註冊到 MappingRegistry