SpringMVC之RequestMapping执行过程(HandlerMapping上篇)

写在前面

从前一篇引导篇 here 的分析来看,如果我们想弄清楚 请求对象 HttpServletRequest方法处理器 HandlerMethod 的对应关系,我们可以去 RequestMappingHandlerMapping 中去寻找“真相”。

我们看待这个类,需要从两个阶段去分析:

  • 预处理部分:HandlerMethod 是如何 扫描注册 到 HandlerMapping 中去的?

  • 执行部分:当一个请求 HttpServletRequest 到来,SpringMVC 又是如何 匹配获取 到合适的 HandlerMethod 的?

!提醒:考虑到篇幅安排,执行部分还需要分 2 篇来讲解,因此本文主要针对预处理部分进行讲解。 下篇 here 讲解执行部分。

快速开始

我还是比较喜欢写单元测试,一方面,单元测试的执行速度比启动一个完整项目要快数十倍;另一方面,单元测试的书写过程中,更容易让我们记住我们忽略了那些细节。这是我掌握源码的一个方法,如果你不喜欢,可以跳过该小节,直接进入分析部分。
有需要的可以到 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

isHandler 方法是用来判断 Bean 是否算是 “Handler Bean” 的。

我们的 XXXController 必须有类注解 @Controller 或者 @RequestMapping

只有加上类注解的类,才可以继续去探查该类的方法。正如 processCandidateBean 方法中这段源码:

if (beanType != null && isHandler(beanType)) {
      detectHandlerMethods(beanName);
}

2.ReflectionUtils.doWithMethods

ReflectionUtils.doWithMethods 是一个反射工具类的方法。

这个静态方法会去递归遍历当前类,当前类的父类(当前类的接口以及当前类所有父类接口)中的方法。这个功能主要依靠第一个参数 Class<?> clazz 中的成员方法的调用来实现:

  • getDeclaredMethods(),获取类对象的声明方法。

  • getSuperClass(),获取当前类的父类。

  • getInterfaces(),获取当前类的接口。

找到了许多 Method,但并不是所有都有用,因此需要过滤不需要的方法。此时,需要借助第二个参数 MethodFilter mf 来实现过滤,这是一个函数式接口,仅包含一个接口方法。

每找到一个方法,都需要相同的处理策略。此时,就需要借助第三个参数 MethodCallback mc,这同样也是函数式接口。

总而言之,这个 ReflectionUtils.doWithMethods 把复杂的遍历递归逻辑封装起来,调用者可以更专注于“要拿哪些 Method,做何种操作”的问题上。

3.ReflectionUtils.USER_DECLARED_METHODS

ReflectionUtils.USER_DECLARED_METHODS 是一个常量对象,它的类型是 MethodFilter

它可以用在 ReflectionUtils.doWithMethods 作为第 3 个参数。

public static final MethodFilter USER_DECLARED_METHODS =
      (method -> !method.isBridge() && !method.isSynthetic() && method.getDeclaringClass() != Object.class);

这个方法的功能:过滤桥接方法合成方法以及Object的自带方法。换言之,筛选出应用程序员写的方法,排除编译器生成的方法。

桥接方法合成方法都是 JVM 编译器编译时的产物。桥接方法主要和泛型的编译有关,合成方法主要和嵌套类和私有成员的编译相关。隐秘而诡异的Java合成方法 了解更多 here

4.MethodIntrospector.selectMethods

MethodIntrospector.selectMethods 静态方法。它有两个参数:

  • Class<?> targetType,指定检查哪个类的方法。

  • MetadataLookup<T> metadataLookup,函数式接口,每找到一个应用程序员在 XxxController 中写的方法,就会回调一次,询问调用者要拿 Method 返回一个什么对象。比如 RequestMappingInfo

这个方法,是在 ReflectionUtils.doWithMethods 的基础上,把 Java 动态代理生成的类考虑进去了。

  • Proxy.isProxyClass
  • ClassUtils.getMostSpecificMethod
  • BridgeMethodResolver.findBridgedMethod
    以上几个方法虽然复杂,但是如果你没有用动态代理来生成 Controller 对象,是不需要过分关注的,我这里也就不过度研究了。

5.getMappingForMethod

getMappingForMethod 方法在 RequestMappingHandlerMapping 中实现的。

方法调用时机MethodIntrospector.selectMethods 执行时,每找到一个“应用程序员”写的 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

registerHandlerMethod

  • 第一个参数 Object handler,传递的是 Controller Bean 对象的 name 或者是对象实例

  • 第二个参数 Method method,是 Controller Bean 对象中的反射方法

  • 第三个参数 mapping,目前也就只有 RequestMappingInfo

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

思考题

我们知道加上 @RequestMapping 或者 @Controller 注解的类才能算是 Controller Bean,才会继续扫描它的方法。

那么,是不是所有的方法都不会被扫描到呢?

@Controller
@RequestMapping("/user")
public class UserController {

    public ModelAndView user(String name, int age) {
        System.out.println("name=" + name);
        System.out.println("age=" + age);
        return null;
    }
}

比如这个 user 方法没有 @RequestMapping 注解还会被扫描到并创建 HandlerMethod 吗?

点击展开答案

答案:No

UserController.user 方法是会被 ReflectionUtils.doWithMethods 扫描出并回调 Lambda 表达式 MethodCallback.doWith

metadataLookup.inspect(specificMethod) 其实会调用 getMappingForMethod

但是,由于方法上面没有 @RequestMapping 注解,所以结果为 null;

因此,没有 @RequestMapping 注解的方法,不会被添加到 methodMap

也就不会 detectHandlerMethods 方法中调用 registerHandlerMethod 完成注册。

结论:没有 @RequestMapping 注解的方法不会创建对应的 HandlerMethod

posted @ 2020-11-29 12:30  极客子羽  阅读(861)  评论(0编辑  收藏  举报