1. 程式人生 > 實用技巧 >SpringMVC之RequestMapping執行過程(HandlerMapping篇)

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 能夠返回一個非空物件,那麼就說明註冊成功了。

TipsgetHandler 方法需要一個請求物件,來自 spring-testMockHttpServletRequest 來測試最合適不過了。

第 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-testStaticWebApplicationContext 會更加簡單。

第 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 的過程:

  1. 獲取所有的 Bean:從 Spring 容器中獲取所有的 Bean,isHandler 方法篩選出帶 @RequestMapping 或者 @Controller 的 Bean。

  2. 獲取所有方法:從 Bean 中取出所有的方法,篩選出帶 @RequestMapping 的方法

  3. 封裝 RequestMappingInfo : 根據註解封裝對映條件

  4. 建立 HandlerMethod

  5. 儲存對映到 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);

createHandlerMethodController Bean(即 handler)和 Bean 的每一個 Method 組合生成一個 HandlerMethod

總結

在 Spring 容器建立 RequestMappingHandlerMapping Bean 的過程中,會執行初始化 afterPropertiesSet(),觸發初始化所有 HandlerMethod

初始化 HandlerMethod 的過程:

  1. 掃描 Spring 容器中的所有 Controller Bean
  2. 找出 Controller Bean 中的所有方法
  3. 建立 RequestMappingInfo
  4. 建立 HandlerMethod
  5. 註冊到 MappingRegistry