以前开发一个项目,要花费不少时间在搭建项目,配置文件上,到现在Spring Boot开箱即用,需要技术栈导入pom就可以了,技术变更带来效率提示是巨大的。有时候我会疑惑,这一切如何得来的,Spring Boot怎么抛弃war部署,抛弃繁琐xml配置。
阅读本文章需要一定的Spring框架知识储备,最后能了解Spring如何进行Bean初始化的,至少知道BeanDefinition之类的知识点,才能更好阅读文章。下面代码基于Spring Boot 2.7.2 、 Spring Cloud 2021.0.3。
先从项目启动放入入口,每一个Spring Boot 项目都需要main入口都要调用SpringApplication.run
开始
1 | public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) { |
webApplicationType是一个枚举类,描述当前项目web类型
NONE: 当前项目不是一个web项目
SERVLET: 基于servlet api的传统web项目
REACTIVE: Spring webFlux 响应式web框架
deduceFromClasspath : 根据项目jar判断当前项目属于上面哪个一个类型,后面创建Spring 上下文对象需要用到
getSpringFactoriesInstances:从给定接口从文件META-INF/spring.factories
使用类名去加载全类名,并且返回接口所有实现类, 配置文件格式如下
1 | org.springframework.context.ApplicationContextInitializer=\ |
这个类似JVM的SPI机制,对于Spring为什么没有使用SPI来 引入扩展实例,我猜SPI不满足多构造参数的实现类初始化,这里暂时将这种机制称作:SpringFactoriesLoader加载。
BootstrapRegistryInitializer:用于初始化BootstrapRegistry的回调接口,在 使用BootstrapRegistry之前调用它。
ApplicationContextInitializer:在执行Spring工厂类调用AbstractApplicationContext.refresh(Spring 工厂核心方法bean初始化)之前初始化ConfigurableApplicationContext的回调接口。主要是做一个配置文件设置、属性设置。
ConfigurableApplicationContext 是一个SPI接口用于通过 配置方式初始化ApplicationContext 。Spring Boot作为Spring框架的集大成者上下文对象ApplicationContext往往根据不同环境有所区别的,这时很需要ApplicationContextInitializer这种接口,由不同组件根据自身情况去实现接口初始化上下文对象。
ApplicationContextInitializer接口
DelegatingApplicationContextInitializer
: 通过环境变量 context.initializer.classes
类名,加载所有ConfigurdiableApplicationContext子类,实例化,排序执行ApplicationContextInitializer接口(接口参数)。SharedMetadataReaderFactoryContextInitializer
: 注册CachingMetadataReaderFactoryPostProcessor 用于向容器注册SharedMetadataReaderFactoryBean,用于缓存Spring加载资源ContextIdApplicationContextInitializer
: 初始化ContextIdConfigurationWarningsApplicationContextInitializer
:报告@ComponentScan配置错误信息输入告警日志RSocketPortInfoApplicationContextInitializer
: 创建一个监听事件,将server.ports赋值到 local.rsocket.server.portServerPortInfoApplicationContextInitializer
: 创建web事件监听: 发布server namespace网络端口ConditionEvaluationReportLoggingListener
: 创建一个事件监听,spring初始化成功或失败,打印相关信息。
ApplicationListener列表
EnvironmentPostProcessorApplicationListener
: 监听ApplicationEnvironmentPreparedEvent事件,执行EnvironmentPostProcessor 配置文件前置处理器,加载配置文件到ConfigurableEnvironmentAnsiOutputApplicationListener
: 监听Spring刚启动事件,从配置文件加载ansi配置。LoggingApplicationListener
: 加载日志相关配置进行初始化设置。BackgroundPreinitializer
: 通过多线程方式初始化Formatter、Validation、HttpMessageConverter、jackson、UTF-8设置。DelegatingApplicationListener
:从配置文件 key:context.listener.classes加载监听器类名并实例化注册到容器中ParentContextCloserApplicationListener
: 监听父级容器关闭事件,并且将事件传递到子级逐级传递下取。ClearCachesApplicationListener
: 清除类加器缓存FileEncodingApplicationListener
: 检测当前系统环境的file.encoding和spring.mandatory-file-encoding设置的值是否一样,如果不一样的话,就会抛出一个IllegalStateException异常,程序启动立马停止
run方法
1 | public ConfigurableApplicationContext run(String... args) { |
run()
Spring Boot框架启动流程
- 获取Java 命令行启动参数,从中提取Spring 配置参数,转换从对应变量
- 创建配置文件对象ConfigurableEnvironment ,命令行中会有profile设置,所以要根据profile加载配置文件,在执行配置文件事件
- 已经加载好文件了,从环境变量中检测是否存在配置spring.beaninfo.ignore,如果设置,写入到ConfigurableEnvironment中
- 开始打印banner,平常看到各种banner就是在这里执行
- 开始创建ConfigurableApplicationContext ,Spring 容器工厂上下文对象
- 对刚刚创建ConfigurableApplicationContext 调用ApplicationContextInitializer 进行属性设置
- 启动Spring 容器IOC、AOP
- 发布Spring启动完成事件
- 从容器中所有ApplicationRunner CommandLineRunner在调用方法
在run方法里面就完成完成整个Spring容器启动流程了,包括Spring Cloud加载也是这里完成的。下面详细分析prepareEnvironment()
,配置文件上下文如何初始化的
prepareEnvironment
1 | private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, |
getOrCreateEnvironment() 如果当前environment如何为空,则会根据根据webApplicationType 类型选择对应类进行初始化。大家可能好奇environment怎么可能有值呢,接着玩下看,当我分析Spring Cloud时你就会返回environment不需要创建了。
ps: Environment 内部使用PropertySource区分不同配置文件,每一个源配置都有自己的名字,比如系统变量systemProperties、环境变量systemEnvironment等等。使用一个propertySourceList一个list将所有PropertySource保存起来,在队列前面永远最优先加载。
在上面写过一个监听器EnvironmentPostProcessorApplicationListener
,它处理environmentPrepared事件,使用SpringFactoriesLoader加载所有EnvironmentPostProcessor 前置处理器,其中之一ConfigDataEnvironmentPostProcessor
就是去做读取配置文件,里面还有很多逻辑处理,这里就不展开了,有兴趣的同学自行去分析代码。读取文件本身也是根据环境变量来的,这里有几个Spring内置配置
spring.config.location 设定加载文件路径,没有则是使用类路径./、config/
spring.config.additional-location: 加载外部文件路径,这个可以spring.config.location 共存,优先级最大
spring.config.name 设定文件名前置,默认 application
上面这些变量都是从环境变量、系统变量中获取的,当然不会从配置文件读取到。通过设定文件路径、文件名这样方式确定加载文件,加载文件规则如下${spring.config.location}/ ${spring.config.name}-${profile}.${extension}
extension:文件名后缀 内置支持4种,分别是: properties、yml、xml、yaml
看下ConfigurableApplicationContext 如何被初始化的
prepareContext
1 | private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context, |
在这里完成了Spring 容器初始化,下一步就是启动了。
bean初始化
其实我一直很好奇@Configuration这个注入如何实现配置类,还有还么多Class要被Spring进行初始化,如何变成BeanDefinition最后变成bean。我确定从AbstractApplicationContext.refresh()
debug,终于被我发现Spring魔法,在invokeBeanFactoryPostProcessors()
在执行invokeBeanFactoryPostProcessors方法中回去获取BeanDefinitionRegistryPostProcessor
类型内置对象,并且执行所有实现类。
BeanDefinitionRegistryPostProcessor
: 你可以理解成BeanDefinition注册前置处理器,主要就是生成BeanDefinition,再还给容器。在Spring还没有初始化bean时,这个接口运行实现类去初始化BeanDefinition再交还给Spring工厂对象,简白点就是这个对象会创建BeanDefinition,交给Spring,后续进行初始化bean。下面要讲解其中一个实现类ConfigurationClassPostProcessor
postProcessBeanFactory创建postProcessBeanFactory
1 | public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { |
看完上面的代码,ConfigurationClassPostProcessor就是Spring将带有@Configuration 标记Class经过一系列处理生成BeanDefinition的机制。在@SpringBootApplication 中有个一个@EnableAutoConfiguration带有@Import(AutoConfigurationImportSelector.class),这个会被ConfigurationClassPostProcessor解析加载。其中AutoConfigurationImportSelector使用SpringFactoriesLoader加载,会将所有@EnableAutoConfiguration的配置类全部都加载ClassName,可以让Spring Boot 加载ScanPackage 基础包路径之外的配置类,再通过@ConditionalOnBean、@ConditionalOnProperty这类注解,根据Class、配置判断是否进行解析。
也就是说Spring Boot一开始就已经获取到所有配置类,只有当符合条件时才会进入解析、加载、实例化。
Spring Cloud
上面说了Spring Boot自动化配置接下来就是Spring Cloud方面,看了上面源码,发现没有代码有关Spring Cloud,现在还不知道配置中心的配置如何作用到已经开始运行Spring 容器中。在开始分析代码之前,先简单看一个例子
可以看到applicatioinContext 有一个父级上下文,而这个就是Spring Cloud 上下文对象。看到这个是不是很惊奇,这个父级上下文在哪里初始化的呢,从代码角度去看了。
上面分析过ApplicationListener监听器中,在Spring Cloud lib jar中有一个实现类BootstrapApplicationListener,通过它来启动Spring Cloud。
1 | public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { |
这个监听器主要根据配置文件信息来启动Spring Cloud组件,如果没有相应的配置根据项目环境来,看下Spring Cloud上下文如何被初始化出来的。
1 | private ConfigurableApplicationContext bootstrapServiceContext(ConfigurableEnvironment environment, |
现在所有代码都看完了,我们来理一理整一个流程就会清晰明了。
在BootstrapApplicationListener中会根据配置文件或者是项目环境jar来是否启动加载bootstrap配置文件。先从生成加载Spring Cloud配置信息,
使用SpringApplicationBuilder来构建SpringApplication对象,然后执行SpringApplication.run 方法,这个代码我们已经分析过了,初始化Spring容器上下文对象,然后进入核心refresh方法执行IOC。SpringApplicationBuilder构造SpringApplication 中没有像我们写启动类main方法,会设置启动类Class。所以被ConfigurationClassPostProcessor
解析BeanDefinition,并没有@SpringApplication 这个注解,所以这个Spring Cloud 工厂没有获取到basepackae、@EnableAutoConfiguration这些东西。根据上面代码知道Spring Cloud将BootstrapImportSelectorConfiguration
作为BeanDefinition交给ConfigurationClassPostProcessor
,这样父级容器只有加载BootstrapConfiguration
标记类,父级bean和子级bean相互隔离。这样父级容器就可以去启动与Spring Cloud有关的bean。当Spring Cloud容器已经完成bean初始化后,再来执行SpringApplicaton.run 启动Spring 容器创建。这样在子级启动之前已经将配置中心的配置对应的对象已经创建出来了。再通过ApplicationContextInitializer接口将配置对象加载ConfigurableEnvironment中。
这里使用较短的篇幅来分析Spring Boot这个框架如何工作,站在自己的思维上,使用3个知识点来展示Spring Boot技术细节实现。第一个从SpringApplication.run
了解Spring两大工厂对象ConfigurableApplicationContext
、ConfigurableEnvironment
如何初始化处理出来的,配置文件如何被加载的,加载规则,知识点SpringFactoriesLoader机制,如果要做Spring Boot组件必须要这个。了解了Spring Boot ApplicationContextInitializer、ApplicationListener这些接口,还有SpringApplicationRunListener
为整个Spring Boot事件监听器,对应整个框架的不同阶段处理。第二简单分析了
Spring容器启动时如何生成BeanDefinition的机制实现类:BeanDefinitionRegistryPostProcessor
,了解了Spring Boot组件如何被加载、实例化,这个依赖启动类的注解。最后Spring Cloud组件如何加载实例化,这个依赖于前面两个。