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

文章目录

  • 前言
  • [第12章 SpringBoot整合WebMvc](#第12章 SpringBoot整合WebMvc)
    • [12.1 SpringBoot整合WebMvc案例](#12.1 SpringBoot整合WebMvc案例)
    • [12.2 整合WebMvc的组件自动装配](#12.2 整合WebMvc的组件自动装配)
    • [12.3 WebMvc的核心组件](#12.3 WebMvc的核心组件)
      • [12.3.1 DispatcherServlet](#12.3.1 DispatcherServlet)
      • [12.3.2 Handler](#12.3.2 Handler)
      • [12.3.3 HandlerMapping](#12.3.3 HandlerMapping)
      • [12.3.4 HandlerAdapter](#12.3.4 HandlerAdapter)
      • [12.3.5 ViewResolver](#12.3.5 ViewResolver)
    • [12.4 @Controller控制器装配原理](#12.4 @Controller控制器装配原理)
      • [12.4.1 初始化@RequestMapping的入口](#12.4.1 初始化@RequestMapping的入口)
      • [12.4.2 processCandidateBean](#12.4.2 processCandidateBean)
      • [12.4.3 detectHandlerMethods](#12.4.3 detectHandlerMethods)
        • [12.4.3.1 筛选Handler方法,创建RequestMappingInfo](#12.4.3.1 筛选Handler方法,创建RequestMappingInfo)
        • [12.4.3.2 遍历方法,注册方法映射](#12.4.3.2 遍历方法,注册方法映射)

前言

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

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

第12章 SpringBoot整合WebMvc

12.1 SpringBoot整合WebMvc案例

导入依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

编写Controller类:

java 复制代码
@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;
    }
}

编写路径配置类:

java 复制代码
@Configuration
public class PathConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("/api", c -> c.isAnnotationPresent(Controller.class));
    }
}

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

编写主启动类:

java 复制代码
@SpringBootApplication
public class WebMvcApp {

    public static void main(String[] args) {
        SpringApplication.run(WebMvcApp.class, args);
    }

}

执行主启动类的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的工作流程如下图所示:

12.3.2 Handler

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

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

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

12.3.3 HandlerMapping

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

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

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

java 复制代码
源码1:HandlerMapping.java

public interface HandlerMapping {
    HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

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

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

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

  • RequestMappingHandlerMapping

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

  • BeanNameUrlHandlerMapping

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

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

java 复制代码
public class TestController implements Controller {

    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        return null;
    }
}

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

  • SimpleUrlHandlerMapping

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

12.3.4 HandlerAdapter

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

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

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

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的工作流程如下:

默认情况下,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对象的初始化阶段有额外的扩展处理。

java 复制代码
源码2:RequestMappingHandlerMapping.java

@Override
@SuppressWarnings("deprecation")
public void afterPropertiesSet() {
    this.config = new RequestMappingInfo.BuilderConfiguration();
    this.config.setUrlPathHelper(getUrlPathHelper());
    // this.config.set...

    super.afterPropertiesSet();
}
java 复制代码
源码3:AbstractHandlerMethodMapping.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());
}

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

12.4.2 processCandidateBean

java 复制代码
源码4:AbstractHandlerMethodMapping.java

protected void processCandidateBean(String beanName) {
    Class<?> beanType = null;
    try {
        beanType = obtainApplicationContext().getType(beanName);
    } // catch ...
    }
    // 判断bean对象的类型是否是Handler
    if (beanType != null && isHandler(beanType)) {
        detectHandlerMethods(beanName);
    }
}

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

java 复制代码
源码5:RequestMappingHandlerMapping.java

@Override
protected boolean isHandler(Class<?> beanType) {
    return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
            AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

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

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


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

12.4.3 detectHandlerMethods

java 复制代码
源码6:AbstractHandlerMethodMapping.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);
        });
    }
}

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

12.4.3.1 筛选Handler方法,创建RequestMappingInfo

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

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

java 复制代码
源码7:RequestMappingHandlerMapping.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);
}

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

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

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

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

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

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

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

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

java 复制代码
源码8:AbstractHandlerMethodMapping.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);
}

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

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

其中,key值是RequestMappingInfo对象,value值Handler方法。

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

······

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

相关推荐
N维世界5 分钟前
Mybatis-XML映射文件
xml·java·mybatis
码出极致7 分钟前
Redisson 分布式锁自动续期机制解析
后端
小塵8 分钟前
【DeepSeek 聊天】五分钟部署本地 DeepSeek
人工智能·后端·deepseek
土拨鼠的旅程10 分钟前
Go map 源码详解【2】—— map 插入
后端
泊浮目14 分钟前
生产级Rust代码品鉴(一)RisingWave一条SQL到运行的流程
大数据·后端·rust
弹简特28 分钟前
【Java SE】Arrays工具类
java·开发语言
Touper.28 分钟前
JavaSE -- Lambda表达式
java·开发语言
秋也凉28 分钟前
redis的命令集合
数据库·redis·缓存
estarlee31 分钟前
通用图片搜索-搜狗源免费API接口使用指南
后端
前端极客探险家35 分钟前
Spring Boot + Vue.js 全栈开发:从前后端分离到高效部署,打造你的MVP利器!
vue.js·spring boot·后端