Spring Boot 项目启动时在 prepareContext 阶段做了哪些事?

概览

如果你对Spring Boot 启动流程还不甚了解,可阅读《Spring Boot 启动流程详解》这篇文章。如果你已了解,那就让我们直接看看prepareContext() 源码。

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {//将环境对象放到上下文中context.setEnvironment(environment);//后置处理应用上下文postProcessApplicationContext(context);//应用初始化上下文applyInitializers(context);//触发监听器‘上下文准备完成’时间listeners.contextPrepared(context);//打印启动信息和激活的profileif (this.logStartupInfo) {logStartupInfo(context.getParent() == null);logStartupProfileInfo(context);}// Add boot specific singleton beans//将 applicationArguments 和 printedBanner 实例注册到bean工厂ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();beanFactory.registerSingleton("springApplicationArguments", applicationArguments);if (printedBanner != null) {beanFactory.registerSingleton("springBootBanner", printedBanner);}if (beanFactory instanceof DefaultListableBeanFactory) {((DefaultListableBeanFactory) beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);}//是否设置bean懒加载if (this.lazyInitialization) {//是则beanFactory后置处理添加LazyInitializationBeanFactoryPostProcessor实例context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());}// Load the sourcesSet<Object> sources = getAllSources();Assert.notEmpty(sources, "Sources must not be empty");//加载sourcesload(context, sources.toArray(new Object[0]));//触发监听器‘上下文加载完毕’事件listeners.contextLoaded(context);
}

通过上面的源码我们可以看出,在准备上下文阶段,spring boot 做了很多事情,这其中主要包括以下内容:

  1. postProcessApplicationContext(),后置处理应用上下文。
  2. applyInitializers(),‘应用’初始化上下文。
  3. listeners.contextPrepared(),触发监听器的‘应用上下文准备完成’事件。
  4. 懒加载模式为 beanFactory 添加 LazyInitializationBeanFactoryPostProcessor() 后置处理器。
  5. 加载sources,包含 primarySources 和自定义的 sources。
  6. listeners.contextLoaded(),触发监听器的‘上下文加载完成’事件。

接下来让我们详细的看看每个步骤具体做了什么。

后置处理应用上下文

postProcessApplicationContext() 上下文的后置处理,这里由于 beanNameGenerator 和 resourceLoader 都为null,addConversionService 为true,故仅对bean工厂设置了 conversionService 转换服务。这里的转换服务为 ApplicationConversionService 类的单例实例。bean工厂为 GenericApplicationContext 无参构造函数创建的 DefaultListableBeanFactory()。

protected void postProcessApplicationContext(ConfigurableApplicationContext context) {if (this.beanNameGenerator != null) {context.getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR,this.beanNameGenerator);}if (this.resourceLoader != null) {if (context instanceof GenericApplicationContext) {((GenericApplicationContext) context).setResourceLoader(this.resourceLoader);}if (context instanceof DefaultResourceLoader) {((DefaultResourceLoader) context).setClassLoader(this.resourceLoader.getClassLoader());}}if (this.addConversionService) {context.getBeanFactory().setConversionService(ApplicationConversionService.getSharedInstance());}
}

‘应用’初始化器

@SuppressWarnings({ "rawtypes", "unchecked" })
protected void applyInitializers(ConfigurableApplicationContext context) {for (ApplicationContextInitializer initializer : getInitializers()) {Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),ApplicationContextInitializer.class);Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");initializer.initialize(context);}
}

applyInitializers()主要是执行SpringApplication对象实例化阶段从spring.factories配置文件中读取并加载的ApplicationContextInitializer接口实现类的initialize()方法。从方法的注释上看主要是在上下文刷新之前做一些操作。spring-boot-x.x.x.jar 默认的spring.factories中包含以下这些实现类:

DelegatingApplicationContextInitializer执行一些其他的ApplicationContextInitializer,这些ApplicationContextInitializer来源于context.initializer.classes属性配置;相当一些自定义的初始化器委托给DelegatingApplicationContextInitializer执行
ContextIdApplicationContextInitializer构建一个上下文ContextId对象并注册到bean工厂中,这里的id一般为从environment环境对象中读取的spring.application.name属性配置,因此一般就是你设置的项目名
ConfigurationWarningsApplicationContextInitializer添加一个ConfigurationWarningsPostProcessor后置处理器到bean工厂后置处理器中;该后置处理器主要校验@ComponentScan扫描的包或类所在包是否包含org.springframework或org,包含则进行warning级别的提示
RSocketPortInfoApplicationContextInitializer将上下文对象包装成一个listener监听器添加到当前应用上下文的applicationListeners监听器列表中;包装的listener主要监听了RSocketServerInitializedEvent事件,将获取的端口设置到类型名为server.ports的属性map中,key为local.rsocket.server.port
ServerPortInfoApplicationContextInitializer将自身添加到当前应用上下文的applicationListeners监听器列表中;ServerPortInfoApplicationContextInitializer本身也实现的ApplicationListener接口,主要监听了WebServerInitializedEvent事件

以上表格的ApplicationContextInitializer都是按优先级排过序的,程序执行时就是从上到下执行。另外实际断点的过程中,我们可以看到除了以上的initializer之外,还有一些其他jar包中的initializer,如下:

  • org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer
    • 将CachingMetadataReaderFactoryPostProcessor添加到上下文对象的beanFactoryPostProcessors列表中
  • io.dubbo.springboot.DubboConfigurationApplicationContextInitializer
    • environment环境中是否设置spring.dubbo.scan属性,如果设置了则实例化一个AnnotationBean,并注册到bean工厂中,同时添加到上下文对象的bean工厂后置处理器列表中和beanFactory对象的bean后置处理器列表中。
  • org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
    • 构建一个ConditionEvaluationReportListener监听器并添加到当前应用上下文的applicationListeners监听器列表中;同时从bean工厂中获取ConditionEvaluationReport。ConditionEvaluationReportListener监听所有的ApplicationEvent事件并触发ConditionEvaluationReportLoggingListener的onApplicationEvent()方法。主要目的打印条件评估报告,以帮助开发者了解Spring Boot 启动过程中自动配置的细节。

实际执行顺序如下图所示:

这里解释下GenericTypeResolver.resolveTypeArgumen() 方法作用,其主要是解析initializer类实现泛型接口的泛型类型,从而判断当前context是否是initializer类实现接口的泛型类型的子类。这里有点绕口,大家可以理解为 ApplicationContextInitializer 接口的泛型类型必须和context接口类型一致,代码上看ApplicationContextInitializer接口的泛型类型就是ConfigurableApplicationContext 类型,所以感觉这里的判断有点多此一举。

触发监听器‘上下文准备完成’事件

listeners.contextPrepared() 表示在上下文准备完成阶段要触发的操作。该事件触发的流程与listeners.starting() 执行流程一致,这里不再讲解,如不清楚可先看看《Spring Boot 启动流程详解》这篇文章。通过打断点可以看到,这里监听了 ApplicationContextInitializedEvent 事件的监听器有:BackgroundPreinitializer、DelegatingApplicationListener、DubboHolderListener;实际这三个listener 都未有 ApplicationContextInitializedEvent 事件的处理逻辑,这里之所有会被扫描出来,是因为其实现的接口泛型类型为 ApplicationContextInitializedEvent 的父类或者未指定,其中 DubboHolderListener 属于未指定。

打印启动信息和激活的profile

上面表格之所以没有列这一步,是因为这里只是打印启动信息及激活profile,没有这一步骤对框架执行并不影响,所以也不重要。

注册 ApplicationArguments 和 Banner bean,设置bean是否可以被覆盖

这里通过 DefaultListableBeanFactory 将 ApplicationArguments 和 Banner 实例对象以单例的方式进行注入,同时设置不允许 bean 被覆盖,让我们看看 registerSingleton() 方法源码:

@Override
public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {//调用父类DefaultSingletonBeanRegistry的registerSingleton() 方法super.registerSingleton(beanName, singletonObject);//更新工厂内部维护的单例bean名称updateManualSingletonNames(set -> set.add(beanName), set -> !this.beanDefinitionMap.containsKey(beanName));//清除映射clearByTypeCache();
}

源码很简单,调用父类的 DefaultSingletonBeanRegistry 的 registerSingleton() 方法,让我再看看 DefaultSingletonBeanRegistry 类的 registerSingleton() 方法。

@Override
public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {Assert.notNull(beanName, "Bean name must not be null");Assert.notNull(singletonObject, "Singleton object must not be null");synchronized (this.singletonObjects) {Object oldObject = this.singletonObjects.get(beanName);if (oldObject != null) {throw new IllegalStateException("Could not register object [" + singletonObject +"] under bean name '" + beanName + "': there is already object [" + oldObject + "] bound");}addSingleton(beanName, singletonObject);}
}protected void addSingleton(String beanName, Object singletonObject) {synchronized (this.singletonObjects) {this.singletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName);this.earlySingletonObjects.remove(beanName);this.registeredSingletons.add(beanName);}
}

其逻辑也很简单,就是先加锁,再判断内部维度单例bean的map是否包含当前bean,包含则抛出异常,单例bean不能被重复创建。如果不存在,则加到缓存map中。

加载sources

先看看源码~

protected void load(ApplicationContext context, Object[] sources) {if (logger.isDebugEnabled()) {logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources));}//创建一个BeanDefinitionLoader用于加载beanBeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources);if (this.beanNameGenerator != null) {loader.setBeanNameGenerator(this.beanNameGenerator);}if (this.resourceLoader != null) {loader.setResourceLoader(this.resourceLoader);}if (this.environment != null) {loader.setEnvironment(this.environment);}//进行bean的装载loader.load();
}

getBeanDefinitionRegistry() 方法用于从上下文中获取bean工厂,断点可以看到返回的是context自身,因为其实现了BeanDefinitionRegistry接口。createBeanDefinitionLoader() 方法就是调用 BeanDefinitionLoader() 的构造函数,函数入参包含当前上下文context 和 sources,这里sources主要就是 primarySources,而 primarySources 就是Spring Boot 启动类。让我们再看看 BeanDefinitionLoader() 构造函数。

BeanDefinitionLoader(BeanDefinitionRegistry registry, Object... sources) {Assert.notNull(registry, "Registry must not be null");Assert.notEmpty(sources, "Sources must not be empty");this.sources = sources;//创建注解bean定义readerthis.annotatedReader = new AnnotatedBeanDefinitionReader(registry);//创建xml bean定义readerthis.xmlReader = new XmlBeanDefinitionReader(registry);if (isGroovyPresent()) {//如果有加载groovy.lang.MetaClass,在创建groovy定义bean的readerthis.groovyReader = new GroovyBeanDefinitionReader(registry);}//创建类路径bean定义扫描器this.scanner = new ClassPathBeanDefinitionScanner(registry);//添加类排除过滤器this.scanner.addExcludeFilter(new ClassExcludeFilter(sources));
}

BeanDefinitionLoader 构造函数主要就是创建各种bean定义解析器用于bean的定义加载。接下来就是调用创建好的 BeanDefinitionLoader 的 load() 方法,其主要作用就是加载定义的bean,包括注解定义、xml定义、groovy脚本定义以及classPath定义。由于这部分实现比较复杂,这里不做详细说明,后面会单独写一篇文章进行详细介绍。

触发监听器‘上下文加载完成’事件

一样的事件处理流程就不再多说了,这里主要看看执行了哪些listener的相应事件。同样断点可以看到有如下监听器监听了 ApplicationPreparedEvent 事件,这里也通过表格展示下每个监听器具体干了什么。

CloudFoundryVcapEnvironmentPostProcessor从内部定义的延迟日志DeferredLog切换到实时日志并进行打印
ConfigFileApplicationListener添加一个属性源后置处理 PropertySourceOrderingPostProcessor 到bean工厂后置处理器列表中
LoggingApplicationListener注册springBootLoggingSystem、springBootLogFile、springBootLoggerGroups等相关bean
BackgroundPreinitializer无响应的事件处理
DelegatingApplicationListener无响应的事件处理
DubboHolderListenerspring-boot-dubbo-starter-1.0.0.jar中的监听器,内部啥也没干。。。

至此,spring boot 在启动时的上下文准备阶段主要就干了这些事。

注:spring boot 版本为2.3.10

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/27114.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mmap引起的内存泄漏分析

最近遇到一个内存泄漏问题&#xff0c;由于问题出现在客户端&#xff0c;只能通过客户提供的Log来分析。 根据客户提供的/proc/meminfo数据发现&#xff0c;MemAvailable 由294072kB减小至18128kB&#xff0c;减小约269MB&#xff0c;引起该变化的最直接原因是PageTables由614…

CTFshow-web sql注入

Web171 1 在题目中可以看到查询语句为 "select username,password from user where username !flag and id ".$_GET[id]." limit 1;"; 直接使用万能密码 查到了所有用户 获得flag Web172 0 可以看到返回逻辑显示 如果返回的查询数据中username不等于fl…

Linux基础IO【II】真的很详细

目录 一.文件描述符 1.重新理解文件 1.推论 2.证明 2.理解文件描述符 1.文件描述符的分配规则 3.如何理解文件操作的本质&#xff1f; 4.输入重定向和输出重定向 1.原理 2.代码实现重定向 3.dup函数 ​编辑 4.命令行中实现重定向 二.关于缓冲区 1.现象 …

Web应用安全测试-业务功能滥用(一)

Web应用安全测试-业务功能滥用&#xff08;一&#xff09; 1、短信定向转发 漏洞描述&#xff1a;短信接收人可任意指定 测试方法&#xff1a;拦截发送短信的请求&#xff0c;将手机号改为测试人员的手机号&#xff0c;测试是否可接收短信验证码。 风险分析&#xff1a;攻击…

echarts学习:使用dataset管理数据

前言 在我们公司的组件库中有许多echarts图表相关的组件&#xff0c;这些组件在使用时&#xff0c;只需将图表数据以特定的格式传入组件中&#xff0c;十分方便。因此当我得知echarts 可以使用dataset集中管理数据时&#xff0c;我就决定自己一定要搞懂它&#xff0c;于是在最…

oracle 删除当前用户下所有表

荆轲刺秦王 通常呢 我们将正式环境的 oracle 数据库 导出成 dmp 文件&#xff0c;然后导入到测试环境或者本地环境&#xff0c;期间可能会出现各种问题。那么如何使错误的导入数据全部删除呢。可以这样做&#xff1a; 1. 本地虚拟机启动 oracle 服务 2. sqldeveloper 连接 o…

vue 安装依赖报错

解决方法&#xff1a; npm install --legacy-peer-deps 然后再运行项目即可。

LabVIEW利用旋转编码器脉冲触发数据采集

利用旋转编码器发出的脉冲控制数据采集&#xff0c;可以采用硬件触发方式&#xff0c;以确保每个脉冲都能触发一次数据采集。本文提供了详细的解决方案&#xff0c;包括硬件连接、LabVIEW编程和触发设置&#xff0c;确保数据采集的准确性和实时性。 一、硬件连接 1. 旋转编码…

北斗应急通信手持终端如何在户外使用

北斗应急通信手持终端在户外的使用&#xff0c;需要遵循一定的步骤和注意事项以确保其高效、安全地运作。以下是一个清晰的使用指南&#xff1a; 一、准备阶段 检查电量&#xff1a;确保北斗应急通信手持终端的电量充足&#xff0c;并携带备用电源以应对长时间使用的情况。 熟…

Ubuntu系统设置中文输入法

重新设置超级用户权限(root)密码(非必要) sudo passwd root 需要注意的是Ubuntu的root密码不能少于8个字符 设置成功后输入命令和新的密码即可无需输入sudo启用root命令 su - 更新软件包列表 sudo apt update sudo apt upgrade 安装fcitx5输入法框架 个别情况需要卸载旧的…

红黑树【C++实现】

文章目录 红黑树的概念红黑树的性质红黑树的操作红黑树结点的定义红黑树的插入情况一&#xff1a;插入结点的叔叔存在&#xff0c;且叔叔的颜色是红色情况二: 插入结点的叔叔存在&#xff0c;且叔叔的颜色是黑色情况三: 插入结点的叔叔不存在 红黑树的验证红黑树的查找 红黑树的…

实现直流高电压(100Vdc~1000Vdc)检测的采样电路(隔离方案)

目前&#xff0c;在电力系统自动化领域、新能源电动汽车领域以及高压储能领域&#xff0c;经常需要采样、检测高压直流母线电压&#xff0c;一般直流高压可能达到100Vdc&#xff5e;1000Vdc&#xff0c;结合电路成本和采样精度&#xff0c;我们设计人员就需要选择合适的采样电路…

【elementui源码解析】如何实现自动渲染md文档-第一篇

文章目录 目录 背景 获取源码 代码分析 背景 之前基于vant3的源码开发过二次开发过组件&#xff0c;其中vant实现了将md文档渲染到界面上&#xff0c;有天突发奇想想知道这是如何实现的将md文档渲染到界面上的&#xff0c;因为平时开发中使用elementui占多数&#xff0c;所…

java线程池讲解!核心参数

创建方式 | 构造方法 Executor构造方法 存放线程的容器&#xff1a; private final HashSet<Worker> workers new HashSet<Worker>(); 构造方法&#xff1a; public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit uni…

利用Morph Studio平台免费生成AI视频教程和效果体验

今天体验一下生成AI视频平台&#xff0c;目前是免费的&#xff0c;但生成效果还是不错的,可以根据输入文字&#xff0c;或者上传图片&#xff0c;或者上传视频来自动生成视频。 访问官网&#xff0c;登录之后点击“create Library” &#xff0c;比如我建了一个“AI视频”的Li…

4S店试驾线上预约小程序源码系统 前后端分离 带完整的源代码包+安装部署教程

系统概述 这款 4S 店试驾线上预约小程序源码系统旨在为 4S 店和消费者提供便捷、高效的试驾预约服务。通过小程序&#xff0c;消费者可以轻松预约试驾&#xff0c;4S 店可以方便地管理预约信息&#xff0c;提高工作效率和服务质量。 代码示例 系统特色功能一览 1.便捷的预约流…

面向对象编程重载

系列文章目录 文章目录 系列文章目录前言一、重载&#xff08;overload&#xff09; 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff0c;这篇文章男女通用&#xff0c;看懂了…

floating_point的IP核使用

参考文章&#xff1a;Vivado IP核之定点数转为浮点数Floating-point_vivado 浮点数-CSDN博客 IP核的配置 后边还要做FFT&#xff0c;所以理论上最好的输出方式是单精度浮点。 输入精度&#xff1a;为了满足要求&#xff0c;输出数据的24位&#xff0c;其中 1位符号位&#xff…

2024年【制冷与空调设备运行操作】考试内容及制冷与空调设备运行操作考试报名

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 制冷与空调设备运行操作考试内容考前必练&#xff01;安全生产模拟考试一点通每个月更新制冷与空调设备运行操作考试报名题目及答案&#xff01;多做几遍&#xff0c;其实通过制冷与空调设备运行操作新版试题很简单。…

pytest配置文件配置并通过allure生成报告

之前已经学习了使用pytestrequests实现各种方式的调用和一些脚本的执行&#xff0c;今天来学习下如何使用pytest.ini配置文件来管理用例的执行以及如何使用allure生成测试报告。 1.pytest.ini文件配置 在项目目录下新建pytest.ini文件&#xff0c;然后进行配置&#xff0c;pyt…