美文网首页js css html
RequestMapping执行过程-2

RequestMapping执行过程-2

作者: 程序员札记 | 来源:发表于2023-04-16 07:35 被阅读0次

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

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

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

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

单元测试

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() 这个方法中。节选代码片段如下:

image.png

如果不调用 afterPropertiesSet(),就不会初始化所有处理器。

第 2 次尝试

在日常开发时,afterPropertiesSet() 都是 Spring Bean 的生命周期中调用的,现在我们自己来主动调用一下。

handlerMapping.afterPropertiesSet();

测试结果测试不通过

ApplicationObjectSupport instance does not run in an ApplicationContext


image.png

失败原因:我们需要给 HandlerMethod 设置应用上下文。

ctx = new StaticWebApplicationContext();
handlerMapping.setApplicationContext(ctx);

Tips:同样使用来自 spring-test 的 StaticWebApplicationContext 会更加简单。

第 3 次尝试

现在的测试代码如下
UserControllerTest.java 点击展开<

需要注意的是,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

image.png

AbstractHandlerMethodMapping 实现了 InitializingBean 接口。

afterPropertiesSet() 触发 AbstractHandlerMethodMapping 的初始化,扫描注册了所有 HandlerMethod。

解析 detectHandlerMethods 核心源码

1.isHandler

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

image.png

我们的 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 把复杂的遍历递归逻辑封装起来,调用者可以更专注于<mark style="margin: 0px; padding: 0px; box-sizing: border-box;">“要拿哪些 Method,做何种操作”</mark>的问题上。

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合成方法,后续会介绍 。

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 对象。

<details style="margin: 0px; padding: 0px; box-sizing: border-box;"><summary style="margin: 0px; padding: 0px; box-sizing: border-box;">getMappingForMethod 源码点击展开查看</summary></details>

解析:

  • 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);

createHandlerMethod 将 Controller 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

image.png

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

metadataLookup.inspect(specificMethod) 其实会调用 getMappingForMethod。但是,由于方法上面没有 @RequestMapping 注解,所以结果为 null;

因此,没有 @RequestMapping 注解的方法,不会被添加到 methodMap。也就不会 detectHandlerMethods 方法中调用 registerHandlerMethod 完成注册。

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

相关文章

网友评论

    本文标题:RequestMapping执行过程-2

    本文链接:https://www.haomeiwen.com/subject/oljfjdtx.html