• SpringBoot源码解读与原理分析(三十六)SpringBoot整合WebMvc(一)@Controller控制器装配原理


    前言

    SpringBoot经常整合的其中一个核心场景是Web开发。

    SpringFramework 5.x中对于Web场景的开发提供了两套实现方案:WebMvc与WebFlux。

    第12章 SpringBoot整合WebMvc

    12.1 SpringBoot整合WebMvc案例

    导入依赖:

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    编写Controller类:

    @Controller
    @RequestMapping("/user")
    public class UserController {
    
        @RequestMapping(method = RequestMethod.GET, value = "/test")
        @ResponseBody
        public String test(String name) {
            System.out.println("请求参数 name = " + name);
            return name;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    编写路径配置类:

    @Configuration
    public class PathConfig implements WebMvcConfigurer {
    
        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
            configurer.addPathPrefix("/api", c -> c.isAnnotationPresent(Controller.class));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    SpringBoot给Controller类的方法的访问路径添加前缀的方式有多种,编写配置类是其中一种。这里编写这个配置类是为了方便下文分析源码,因此是可选的。

    编写主启动类:

    @SpringBootApplication
    public class WebMvcApp {
    
        public static void main(String[] args) {
            SpringApplication.run(WebMvcApp.class, args);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    执行主启动类的main方法,启动服务。

    HTTP客户端使用GET方式访问 http://127.0.0.1:8080/user/test?name=齐天大圣,得到结果如下:

    请求执行结果
    说明SpringBoot整合WebMvc成功。

    12.2 整合WebMvc的组件自动装配

    SpringBoot源码解读与原理分析(六)WebMvc场景的自动装配 中,已经对SpringBoot整合WebMvc时注册的组件进行了梳理,总结如下:

    • WebMvcAutoConfiguration
      • WebMvcAutoConfigurationAdapter
        • 消息转换器HttpMessageConverter
        • 视图解析器ContentNegotiatingViewResolver
        • 国际化组件LocaleResolver
        • 异步支持AsyncSupportConfigurer
      • EnableWebMvcConfiguration
        • RequestMappingHandlerMapping
        • RequestMappingHandlerAdapter
        • 静态资源加载配置
    • DispatcherServletAutoConfiguration
      • DispatcherServlet
    • ServletWebServerFactoryAutoConfiguration
      • 嵌入式Web容器EmbeddedTomcat、EmbeddedJetty、EmbeddedUndertow
      • 后置处理器的注册器BeanPostProcessorsRegistrar

    12.3 WebMvc的核心组件

    12.3.1 DispatcherServlet

    DispatcherServlet是WebMvc的核心前端控制器,统一接收客户端(浏览器)的所有请求,并根据请求URI转发给项目中编写好的Controller方法。Controller方法处理完毕后,将处理结果返回给DispatcherServlet,由DispatcherServlet响应到客户端(浏览器)。

    但要注意,匹配寻找Controller方法、请求转发以及响应数据等工作,都不是DispatcherServlet亲自完成的,而是委托给其他组件,这些组件与DispatcherServlet共同协作完成整个MVC的工作。

    DispatcherServlet的工作流程如下图所示:

    DispatcherServlet的工作流程1

    12.3.2 Handler

    在项目开发中编写的Controller方法中,一个标注了@RequestMapping注解的方法就是一个Handler。因此,Handler要完成的工作就是处理客户端发送的请求,并响应视图/JSON数据。

    DispatcherServlet接收到请求后,会根据URI匹配到标注了@RequestMapping注解的Controller方法(即Handler),并将这些请求转发给这个Handler。

    DispatcherServlet的工作流程可以做如下优化:

    DispatcherServlet的工作流程2

    12.3.3 HandlerMapping

    DispatcherServlet根据是否标注@RequestMapping注解去匹配Handler的工作,DispatcherServlet自己也是不做的,而是委托给HandlerMapping来完成。

    HandlerMapping意为处理器映射器,它的作用是根据URI,去匹配查找能处理当前请求的Handler。

    加入HandlerMapping,DispatcherServlet的工作流程如下:

    DispatcherServlet的工作流程3

    源码1HandlerMapping.java
    
    public interface HandlerMapping {
        HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    由 源码1 可知,HandlerMapping通过getHandler方法查找Handler,返回一个HandlerExecutionChain对象。

    HandlerExecutionChain意为Handler执行链,这是因为一个请求除了被Handler处理,还有可能被拦截器拦截处理,在执行Handler之前要先执行拦截器。基于此,HandlerMapping将当前请求会涉及到的拦截器和Handler一起封装起来,组合成一个HandlerExecutionChain对象,交给DispatcherServlet。

    HandlerMapping接口有几个主要的落地实现类:

    • RequestMappingHandlerMapping

    支持@RequestMapping注解的处理器映射器,是实际项目开发中最重要、最常用的。

    • BeanNameUrlHandlerMapping

    使用bean对象的名称作为Handler接收请求路径的处理器映射器。

    这种方式需要编写Controller类实现WebMvc定义的Controller接口,并重写handleRequest方法,如:

    public class TestController implements Controller {
    
        @Override
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    使用该方式编写的Controller,一个类只能接收一个请求路径,因此局限性较大,目前已被淘汰

    • SimpleUrlHandlerMapping

    这种处理器映射器会在配置文件中统一配置请求路径和Controller的对应关系,同时仍要编写Controller类实现WebMvc定义的Controller接口。该方式也已被淘汰。

    12.3.4 HandlerAdapter

    DispatcherServlet获取到HandlerExecutionChain后,就要开始执行这些拦截器和Handler,DispatcherServlet会选择交给HandlerAdapter去执行。

    HandlerAdapter意为处理器适配器,它的作用是执行HandlerMapping封装好的HandlerExecutionChain。

    加入HandlerAdapter后DispatcherServlet的工作流程如下:

    DispatcherServlet的工作流程4
    HandlerMapping接口有几个主要的落地实现类:

    • RequestMappingHandlerAdapter:基于@RequestMapping注解的处理器适配器,底层使用反射机制调用Handler的方法(最常用)。
    • SimpleControllerHandlerAdapter:基于Controller接口的处理器适配器,底层会将Handler强转为Controller,调用其handleRequest方法。
    • SimpleServletHandlerAdapter:基于Servlet的处理器适配器,底层会将Handler强转为Servlet,调用其service方法。

    由此可以发现,WebMvc兼顾多种编写Handler的方式,但最常用的是基于@RequestMapping注解的方式

    12.3.5 ViewResolver

    DispatcherServlet获取到HandlerAdapter返回的ModelAndView之后,需要进行响应视图,这部分工作DispatcherServlet将会委托给ViewResolver来处理。

    ViewResolver意为视图解析器,它的作用是根据ModelAndView中存放的视图名称到预先配置好的位置去查找对应的视图文件(.jsp、.html等),并进行实际的视图渲染。渲染完成后,将视图响应给DispatcherServlet。

    加入ViewResolver后DispatcherServlet的工作流程如下:

    DispatcherServlet的工作流程5
    默认情况下,ViewResolver只会初始化一个实现类InternalResourceViewResolver,它继承自UrlBasedViewResolver类,可以方便地声明页面路径的前后缀,以便开发中在返回视图(JSP页面)时编写视图名称。

    除了InternalResourceViewResolver,WebMvc还为一些模板引擎提供了支持类,例如支持FreeMarker的FreeMarkerViewResolver、支持Groovy XML/XHTML的GroovyMarkupViewResolver等。

    至此,DispatcherServlet的核心工作流程梳理完毕,这样看来DispatcherServlet其实没有做具体的工作,而是扮演了一个“调度者”的角色,在不同的环节分发不同的工作给其他核心组件

    12.4 @Controller控制器装配原理

    当编写的Controller类标注了@Controller注解(或派生注解),方法标注了@RequestMapping注解(或派生注解)时,即可装载到WebMvc中完成视图跳转/数据响应的功能。

    12.4.1 初始化@RequestMapping的入口

    使用@RequestMapping注解的方式声明Handler,一定会与RequestMappingHandlerMapping有关联。由 12.1 节的分析可知,自动配置类WebMvcAutoConfiguration中就已经初始化了RequestMappingHandlerMapping。

    借助IDE打开RequestMappingHandlerMapping类的源码,发现该类重写了其父类InitializingBean的afterPropertiesSet方法,这说明在RequestMappingHandlerMapping对象的初始化阶段有额外的扩展处理。

    源码2RequestMappingHandlerMapping.java
    
    @Override
    @SuppressWarnings("deprecation")
    public void afterPropertiesSet() {
        this.config = new RequestMappingInfo.BuilderConfiguration();
        this.config.setUrlPathHelper(getUrlPathHelper());
        // this.config.set...
    
        super.afterPropertiesSet();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    源码3AbstractHandlerMethodMapping.java
    
    @Override
    public void afterPropertiesSet() {
        initHandlerMethods();
    }
    
    private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";
    protected void initHandlerMethods() {
        // 获取IOC容器中所有bean对象的名称
        for (String beanName : getCandidateBeanNames()) {
            if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
                // 如果bean对象的名称不以“scopedTarget.”开头,则执行
                processCandidateBean(beanName);
            }
        }
        handlerMethodsInitialized(getHandlerMethods());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    由 源码2-3 可知,afterPropertiesSet方法一直向下调用到initHandlerMethods方法。从方法名可以推断,该方法会初始化所有的Handler方法。在该方法的实现中,会获取IOC容器中所有bean对象的名称,找出其中不以“scopedTarget.”开头的bean对象名称,并执行processCandidateBean方法。

    12.4.2 processCandidateBean

    源码4AbstractHandlerMethodMapping.java
    
    protected void processCandidateBean(String beanName) {
        Class<?> beanType = null;
        try {
            beanType = obtainApplicationContext().getType(beanName);
        } // catch ...
        }
        // 判断bean对象的类型是否是Handler
        if (beanType != null && isHandler(beanType)) {
            detectHandlerMethods(beanName);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    由 源码4 可知,processCandidateBean首先会根据bean对象的名称获取到bean对象的类型,再判断这个类型是否是Handler,如果是Handler,则继续向下执行detectHandlerMethods方法。

    源码5RequestMappingHandlerMapping.java
    
    @Override
    protected boolean isHandler(Class<?> beanType) {
        return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
                AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    由 源码5 可知,判断一个类是否是Handler的方法是:判断类上是否标注了@Controller注解(或派生注解)或者@RequestMapping注解(或派生注解)。只要二者存在一个,就可以判定该类是一个Handler。

    如UserController类标注了@Controller注解,则会被判定为是一个Handler:

    UserController类被判定Handler
    在实际的项目开发中,通常是同时标注@Controller注解和@RequestMapping注解。但 源码5 提示我们,实际上,在类上只标注@RequestMapping注解就可以判定该类是一个Controller类。

    12.4.3 detectHandlerMethods

    源码6AbstractHandlerMethodMapping.java
    
    protected void detectHandlerMethods(Object handler) {
        Class<?> handlerType = (handler instanceof String ?
                obtainApplicationContext().getType((String) handler) : handler.getClass());
    
        if (handlerType != null) {
            Class<?> userType = ClassUtils.getUserClass(handlerType);
            // 筛选Handler方法,创建RequestMappingInfo
            Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
                    (MethodIntrospector.MetadataLookup<T>) method -> {
                        try {
                            return getMappingForMethod(method, userType);
                        } // catch ...
                    });
            // logger ...
            // 遍历方法,注册方法映射
            methods.forEach((method, mapping) -> {
                Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
                registerHandlerMethod(handler, invocableMethod, mapping);
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    由 源码6 可知,detectHandlerMethods方法会先根据bean对象名称找到对应的bean对象类型,筛选出该类型下的全部Handler方法,并一一遍历和注册这些方法。

    12.4.3.1 筛选Handler方法,创建RequestMappingInfo

    利用MethodIntrospector的selectMethods方法进行方法的遍历。在selectMethods方法内部会利用反射机制逐个遍历类中的方法,并执行selectMethods方法参数中的lambda表达式。

    lambda表达式中的getMappingForMethod方法源码如下:

    源码7RequestMappingHandlerMapping.java
    
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        // 创建方法级的RequestMappingInfo
        RequestMappingInfo info = createRequestMappingInfo(method);
        if (info != null) {
            // 创建类级的RequestMappingInfo
            RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
            if (typeInfo != null) {
                // 拼接类级和方法级的请求映射路径
                info = typeInfo.combine(info);
            }
            // 拼接路径前缀
            String prefix = getPathPrefix(handlerType);
            if (prefix != null) {
                info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
            }
        }
        return info;
    }
    
    private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
        // 检查类或方法上是否标注了@RequestMapping注解
        RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
        RequestCondition<?> condition = (element instanceof Class ?
                getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
        // 如果标注了@RequestMapping注解,则创建RequestMappingInfo,否则返回空
        return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    由 源码7 可知,getMappingForMethod方法的逻辑如下:

    首先,会检查方法上是否标注了@RequestMapping注解,如果有则创建方法级的RequestMappingInfo对象,否则直接返回null;

    然后再检查类上是否标注了@RequestMapping注解,如果有则创建类级的RequestMappingInfo对象;

    借助Debug可知,RequestMappingInfo对象中保存了Controller类的映射URI,或者Handler方法的请求路径和请求方式,如下图:

    RequestMappingInfo对象保存了请求路径和请求方式
    其次,调用combine方法把方法级的RequestMappingInfo对象和类级的合并到一起,合并之后类级别的RequestMappingInfo对象中保存的映射URI和方法级别中保存的合并到了一起,如图:

    合并RequestMappingInfo对象
    最后一步是拼接路径前缀。这个路径前缀是自定义的,即 xxx 节整合项目中的PathConfig配置类。如果项目中没有设置,则不需要处理这一步。通过拼接路径前缀,访问Handler方法的URI完整了,如图:

    拼接路径前缀

    12.4.3.2 遍历方法,注册方法映射

    经过getMappingForMethod方法的处理,获得一个Map集合,该集合的value值就是包含映射URI、请求方式等信息的RequestMappingInfo对象。

    源码8AbstractHandlerMethodMapping.java
    
    protected void detectHandlerMethods(Object handler) {
        Class<?> handlerType = (handler instanceof String ?
                obtainApplicationContext().getType((String) handler) : handler.getClass());
    
        if (handlerType != null) {
            Class<?> userType = ClassUtils.getUserClass(handlerType);
            // 筛选Handler方法,创建RequestMappingInfo
            Map<Method, T> methods = ...
            
            // ......
            
            // 遍历方法,注册方法映射
            methods.forEach((method, mapping) -> {
                Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
                registerHandlerMethod(handler, invocableMethod, mapping);
            });
        }
    }
    
    protected void registerHandlerMethod(Object handler, Method method, T mapping) {
        this.mappingRegistry.register(mapping, handler, method);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    由 源码8 可知,detectHandlerMethods方法的下半段会遍历Map集合,将Handler方法与RequestMappingInfo对象一一映射,并注册到MappingRegistry中。

    因此,MappingRegistry中存放的是Handler方法与RequestMappingInfo对象的映射关系。

    Handler方法与URI的映射关系
    其中,key值是RequestMappingInfo对象,value值Handler方法。

    在DispatcherServlet接收到客户端请求时,则可以根据URI去MappingRegistry中寻找,如果找到匹配的Handler方法,则定位到可以处理请求的Handler方法,并将请求转发。

    ······

    本节完,更多内容请查阅分类专栏:SpringBoot源码解读与原理分析

  • 相关阅读:
    h3c 网络设备清理所有配置
    Mysql集群及高可用-并行复制5
    docker&kubernets篇(十六)
    18:第三章:开发通行证服务:1:短信登录&注册流程,简介;(这儿使用短信验证码)
    emqx 集群搭建
    Java核心编程(20)
    Spring Boot 下载文件(word/excel等)文件名中文乱码问题|构建打包不存在模版文件(templates等)
    RNA-seq 详细教程:分析流程介绍(1)
    springboot+影院售票小程序 毕业设计-附源码111154
    STM32建立工程问题汇总
  • 原文地址:https://blog.csdn.net/weixin_42739799/article/details/136329704