《学会 SpringMVC 系列 · 剖析初始化》

devtools/2024/10/19 5:30:02/

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍

文章目录

CSDN.gif

写在前面的话

前几篇博文,大致了解了SpringMVC请求流程中的源码分析和扩展运用,为知识连贯性,本篇介绍一下 SpringMVC 初始化过程中的相关源码解读。
和请求流程相比,初始化流程更为简单,主要就是初始化得到一些准备数据,为后续请求过程服务。

相关博文
《学会 SpringMVC 系列 · 基础篇》
《学会 SpringMVC 系列 · 剖析篇(上)》
《学会 SpringMVC 系列 · 剖析入参处理》
《学会 SpringMVC 系列 · 剖析出参处理》
《学会 SpringMVC 系列 · 返回值处理器》
《学会 SpringMVC 系列 · 消息转换器 MessageConverters》
《学会 SpringMVC 系列 · 写入拦截器 ResponseBodyAdvice》
《程序猿入职必会(1) · 搭建拥有数据交互的 SpringBoot 》


SpringMVC 初始化

前文提要

前面请求篇介绍的时候,提到 DispatcherServlet 是 SpringMVC 的入口,不管是否为 SpringBoot 项目,都是如此。
从下面这张图很明显可以看出 DispatcherServlet 和 Servlet 的父子关系。
image.png
有过 JavaWeb 开发经验的人应该了解,Servlet 的初始化是 inti 方法。这边先不展开细节,后续再按专栏展开,总之是在 DispatcherServlet、FrameworkServlet、HttpServlet 等类之间反复横跳。
可以先将入口判定为 HttpServletBean#initServletBean。

启动流程

Tips:DispatcherServlet 本质就是一个 Servlet,遵循Servlet的初始化和请求规范。

1、启动 Tomcat
2、解析 web.xml
3、创建并初始化 DispatcherServlet
4、触发 DispatcherServlet 父类的init方法(比如 HttpServletBean#initServletBean)
5、FrameworkServlet#initWebApplicationContext(这步接下来是核心,创建Web上下文)
6、FrameworkServlet#configureAndRefreshWebApplicationContext(配置文件的解析,扫描和加载Bean)
7、DispatcherServlet#initStrategies(初始化HandleMapping等内容,代码如下)

Tips:创建容器的过程类似Spring,没什么特殊的,这边暂不展开。

java">@Override
protected void onRefresh(ApplicationContext context) {initStrategies(context);
}
/*** Initialize the strategy objects that this servlet uses.* <p>May be overridden in subclasses in order to initialize further strategy objects.*/
protected void initStrategies(ApplicationContext context) {initMultipartResolver(context);initLocaleResolver(context);initThemeResolver(context);initHandlerMappings(context);initHandlerAdapters(context);initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);
}

最后一步用到了Spring的发布订阅,FrameworkServlet 内部有如下代码,最终触发到了 DispatcherServlet 的 onRefresh。

java">private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {FrameworkServlet.this.onApplicationEvent(event);}
}

initHandlerMappings

Tips:nitStrategies 里面方法很多,比较重要的,或者说和后续请求流程关联较多的,这里挑选介绍。

如方法的名字,是初始化 HandlerMappings,会加载自定义的,也会加载默认的 HandlerMappings。
自定义场景较少,默认的话是读取 DispatcherServlet.properties 文件里面的,如下所示。

java">private void initHandlerMappings(ApplicationContext context) {this.handlerMappings = null;if (this.detectAllHandlerMappings) {// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.Map<String, HandlerMapping> matchingBeans =BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);if (!matchingBeans.isEmpty()) {this.handlerMappings = new ArrayList<>(matchingBeans.values());// We keep HandlerMappings in sorted order.AnnotationAwareOrderComparator.sort(this.handlerMappings);}}else {try {HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);this.handlerMappings = Collections.singletonList(hm);}catch (NoSuchBeanDefinitionException ex) {// Ignore, we'll add a default HandlerMapping later.}}// Ensure we have at least one HandlerMapping, by registering// a default HandlerMapping if no other mappings are found.if (this.handlerMappings == null) {this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);if (logger.isTraceEnabled()) {logger.trace("No HandlerMappings declared for servlet '" + getServletName() +"': using default strategies from DispatcherServlet.properties");}}for (HandlerMapping mapping : this.handlerMappings) {if (mapping.usesPathPatterns()) {this.parseRequestPath = true;break;}}
}private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";

image.png
由于 RequestMappingHandlerMapping 的父类,实现了 InitializingBean 接口,如下所示(圈出来的几个东西后续要关注的)。
所以会被在创建 RequestMappingHandlerMapping 的 bean 对象的时候,系统会触发其 afterPropertiesSet 方法
image.png
几经辗转,走到父类AbstractHandlerMethodMapping的如下方法,这里判断isHandler方法,代表符合要求才处理。

java">protected void processCandidateBean(String beanName) {Class<?> beanType = null;try {beanType = obtainApplicationContext().getType(beanName);}catch (Throwable ex) {// An unresolvable bean type, probably from a lazy bean - let's ignore it.if (logger.isTraceEnabled()) {logger.trace("Could not resolve type for bean '" + beanName + "'", ex);}}if (beanType != null && isHandler(beanType)) {detectHandlerMethods(beanName);}
}

可以看到RequestMappingHandlerMapping的isHandler是判断包含Controller注解才处理。

java">protected boolean isHandler(Class<?> beanType) {return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
}

如果类符合要求,进一步就是处理该类的方法,这边会继续判断方法是否包含RequestMapping注解,然后解析注解信息,参考如下代码。
不继续深入展开,有需要再深入。

java">protected void detectHandlerMethods(Object handler) {Class<?> handlerType = (handler instanceof String beanName ?obtainApplicationContext().getType(beanName) : handler.getClass());if (handlerType != null) {Class<?> userType = ClassUtils.getUserClass(handlerType);// 得到一个Map,Key是Method,Value是RequestMappingInfo(注解信息)Map<Method, T> methods = MethodIntrospector.selectMethods(userType,(MethodIntrospector.MetadataLookup<T>) method -> {try {return getMappingForMethod(method, userType);}catch (Throwable ex) {throw new IllegalStateException("Invalid mapping on handler class [" +userType.getName() + "]: " + method, ex);}});if (logger.isTraceEnabled()) {logger.trace(formatMappings(userType, methods));}else if (mappingsLogger.isDebugEnabled()) {mappingsLogger.debug(formatMappings(userType, methods));}//遍历上面的Map,得到新的Map,Key是URL,Value是Method(包含很多信息,MappingRegistry)methods.forEach((method, mapping) -> {Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);registerHandlerMethod(handler, invocableMethod, mapping);});}
}

initHandlerAdapters

前面逻辑基本和HandleMappings一致,触发RequestMappingHandlerAdapter的afterPropertiesSet方法。
这个方法就加载了很多东西,方法子方法的名字大概也可以猜到。
请求流程里面涉及的@ControllerAdvice、消息转换器、参数解析器、返回值处理器等等,都涉及到了,应有尽有。

java">@Override
public void afterPropertiesSet() {//处理加了@ControllerAdvice注解的initControllerAdviceCache();//初始化消息转换器initMessageConverters();if (this.argumentResolvers == null) {List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);}if (this.initBinderArgumentResolvers == null) {List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);}if (this.returnValueHandlers == null) {List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);}if (BEAN_VALIDATION_PRESENT) {List<HandlerMethodArgumentResolver> resolvers = this.argumentResolvers.getResolvers();this.methodValidator = HandlerMethodValidator.from(this.webBindingInitializer, this.parameterNameDiscoverer,methodParamPredicate(resolvers, ModelAttributeMethodProcessor.class),methodParamPredicate(resolvers, RequestParamMethodArgumentResolver.class));}
}

回到请求流程

初始化流程没有什么复杂的,不一一展开,浪费大家时间。
经过上面两个初始化方法,大概拿到了一些准备数据,现在再回到请求流程的核心方法 DispatcherServlet#doDispatch,代码如下所示,其实就是三步骤:找HandleMapping、找HandlerAdapter、执行HandlerAdapter的handle方法,这个过程中,用到的都是各种初始化后得到的东西。

java">// 找出处理这个请求的执行链对象 HandlerExecutionChain,包含方法和拦截器
mappedHandler = getHandler(processedRequest, false);
if (mappedHandler == null || mappedHandler.getHandler() == null) {noHandlerFound(processedRequest, response);return;
}// 根据 HandlerMethod 获取处理器 HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());// 执行实际的处理器处理请求,这步骤是最核心的
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

总结陈词

此篇文章介绍了SpringMVC 初始化相关的分析,仅供学习参考。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

CSDN_END.gif


http://www.ppmy.cn/devtools/91206.html

相关文章

JavaEE 从入门到精通(二) ~SpringMVC 接收请求和设置响应

晚上好&#xff0c;愿这深深的夜色给你带来安宁&#xff0c;让温馨的夜晚抚平你一天的疲惫&#xff0c;美好的梦想在这个寂静的夜晚悄悄成长。 目录 前言 一、获取请求数据 1. 简单参数 1.1 请求行获取参数 a. 与查询参数的名称相同&#xff0c;底层会自动映射到形参中。 …

python打包成能够在mac里面运行的程序

要将你的PyQt5应用程序打包成可以在macOS上运行的独立应用程序&#xff0c;可以使用工具如PyInstaller或py2app。下面是使用py2app的详细步骤&#xff0c;因为它是macOS上专用的打包工具&#xff0c;并且更好地支持PyQt5。 1. 安装py2app 首先&#xff0c;确保你的macOS系统上…

html+css网页设计公司网站模版3个页面 无js 静态页面

htmlcss网页设计公司网站模版3个页面 无js 静态页面 网页作品代码简单&#xff0c;可使用任意HTML编辑软件&#xff08;如&#xff1a;Dreamweaver、HBuilder、Vscode 、Sublime 、Webstorm、Text 、Notepad 等任意html编辑软件进行运行及修改编辑等操作&#xff09;。 获取源…

鸿蒙(API 12 Beta2版)媒体开发【处理音频焦点事件】

音频打断策略 多音频并发&#xff0c;即多个音频流同时播放。此场景下&#xff0c;如果系统不加管控&#xff0c;会造成多个音频流混音播放&#xff0c;容易让用户感到嘈杂&#xff0c;造成不好的用户体验。为了解决这个问题&#xff0c;系统预设了音频打断策略&#xff0c;对…

零基础入门转录组数据分析——机器学习算法之boruta(训练模型)

零基础入门转录组数据分析——机器学习算法之boruta&#xff08;训练模型&#xff09; 目录 零基础入门转录组数据分析——机器学习算法之boruta&#xff08;训练模型&#xff09;1. boruta基础知识2. boruta&#xff08;Rstudio&#xff09;——代码实操2. 1 数据处理2. 2 构建…

【Web开发手礼】探索Web开发的秘密(十四)-Vue2(1)Node.js的安装、Vue入门

主要介绍了Node.js的安装教程、Vue2常用的一些指令、声明周期&#xff01;&#xff01;&#xff01; 文章目录 前言 Node.js安装 选择安装目录 验证NodeJS环境变量 配置npm的全局安装路径 切换npm的淘宝镜像 安装Vue-cli ​编辑 Vue2入门 引入vue.js文件 入门代码 常用指令 生…

Bash Shell 脚本中的循环语句

文章目录 Bash Shell 脚本中的循环语句一、for 循环1.1 列表循环1.2 不带列表循环&#xff08;C 风格的 for 循环&#xff09; 二、案例示例2.1 打印 1-5 的数字2.2 打印 5 次 "hello world"2.3 打印 abcde2.4 输出 0-50 之间的偶数 三、应用技巧3.1 使用花括号和 se…

5G 网络切片

5G 业务分类 增强型移动宽带(eMBB) 传统数据业务&#xff0c;特点是高带宽超高可靠性低时延业务&#xff08;URLLC&#xff09;无人驾驶、工业自动化等业务, 特点是高可靠、低时延海量机器类通信(mMTC) 物联网&#xff0c;特点是大量连接&#xff0c;时延不敏感&#xff0c;数…