• 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

  • 相关阅读:
    开启text汇聚排序
    sshpass安装使用
    pinpoint初始化hbase脚本报错
    ORA-19606: Cannot copy or restore to snapshot control file
    standby_file_management参数为MANUAL导致添加数据文件错误
    dataguard从库移动数据文件
    redis恢复(aof)
    redis集群部署+节点端口修改+数据恢复
    oracle传输表空间相关
    dataguard主库删除归档日志后从库恢复的方法
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/springMvc-RequestMapping-HandlerMapping.html
Copyright © 2020-2023  润新知