以前开发一个项目,要花费不少时间在搭建项目,配置文件上,到现在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
开始
public static ConfigurableApplicationContext run(Class primarySource, String... args) {
return run(new Class[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
public SpringApplication(Class... primarySources) {
this(null, primarySources);
}
public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//web 项目类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
webApplicationType是一个枚举类,描述当前项目web类型
NONE: 当前项目不是一个web项目
SERVLET: 基于servlet api的传统web项目
REACTIVE: Spring webFlux 响应式web框架
deduceFromClasspath : 根据项目jar判断当前项目属于上面哪个一个类型,后面创建Spring 上下文对象需要用到
getSpringFactoriesInstances:从给定接口从文件META-INF/spring.factories
使用类名去加载全类名,并且返回接口所有实现类, 配置文件格式如下
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
这个类似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
: 初始化ContextId
ConfigurationWarningsApplicationContextInitializer
:报告@ComponentScan配置错误信息输入告警日志
RSocketPortInfoApplicationContextInitializer
: 创建一个监听事件,将server.ports赋值到 local.rsocket.server.port
ServerPortInfoApplicationContextInitializer
: 创建web事件监听: 发布server namespace网络端口
ConditionEvaluationReportLoggingListener
: 创建一个事件监听,spring初始化成功或失败,打印相关信息。
ApplicationListener列表
EnvironmentPostProcessorApplicationListener
: 监听ApplicationEnvironmentPreparedEvent事件,执行EnvironmentPostProcessor 配置文件前置处理器,加载配置文件到ConfigurableEnvironment
AnsiOutputApplicationListener
: 监听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方法
public ConfigurableApplicationContext run(String... args) {
long startTime = System.nanoTime();
//调用BootstrapRegistryInitializer接口对上下文进行初始化
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
// 设置 java.awt.headless 缺失显示设备需要CPU介入显示
configureHeadlessProperty();
//获取事件发布器实例,这里会将上面监听器实例装进发布器,监听器类似事件消费者
SpringApplicationRunListeners listeners = getRunListeners(args);
//发布starting 事件
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
//获取所有启动参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//创建配置文件对象
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
//从配置文件中忽视bean
configureIgnoreBeanInfo(environment);
//Banner 配置 打印
Banner printedBanner = printBanner(environment);
//使用ApplicationContextFactory 初始化ApplicationContentx Spring 工厂
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
//配置文件对象配置
//开始对applicationContext context 进行初始化
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context); // 调用refresh
//空方法
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
listeners.started(context, timeTakenToStartup);
//调用所有 ApplicationRunner CommandLineRunner
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
listeners.ready(context, timeTakenToReady);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
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
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
// 根据webApplicationType 创建
// SERVLET => ApplicationServletEnvironment
//REACTIVE=> ApplicationReactiveWebEnvironment
// NONE => ApplicationEnvironment
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 命令行可能会有profile,可以选择那个profile,也会将命令行参数生成一个PropertySources
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 添加 configurationProperties PropertySource到propertySourceList 队列最前面
ConfigurationPropertySources.attach(environment);
// 执行所有SpringApplicationRunListener
listeners.environmentPrepared(bootstrapContext, environment);
// 将defaultProperties sources 移致队尾
DefaultPropertiesPropertySource.moveToEnd(environment);
Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
"Environment prefix cannot be set via properties.");
// 从配置文件对应spring.main.* 属性注入
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
//将类型转换器设置到environment
environment = convertEnvironment(environment);
}
// 因为EnvironmentPostProcessor 可能加载到配置文件里,这时需要configurationProperties 放入第一
ConfigurationPropertySources.attach(environment);
return environment;
}
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.name}-{extension}
- extension:文件名后缀 内置支持4种,分别是: properties、yml、xml、yaml
看下ConfigurableApplicationContext 如何被初始化的
prepareContext
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
// 初始化 ConfigurableEnvironment
context.setEnvironment(environment);
//初始化resourceLoader ConversionService
postProcessApplicationContext(context);
//执行上面从SpringFactoriesLoader加载 ApplicationContextInitializer 对ConfigurableApplicationContext 属性设置
applyInitializers(context);
// 调用SpringApplicationRunListener.contextPrepared 事件
listeners.contextPrepared(context);
// 执行BootstrapContextClosedEvent 事件
bootstrapContext.close(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
//下面添加特定单例对象,为Spring初始化bean IOC 处理必要的bean
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
// 这里已经从配置文件加载 设置到自身属性上了,这时设置给上下文对象
// allowCircularReferences 允许同名bean覆盖 lazyInitialization 对所有bean使用懒加载
if (beanFactory instanceof AbstractAutowireCapableBeanFactory) {
((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(this.allowCircularReferences);
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// 这个前置处理器主要作用就是将配置defaultProperties 移到队尾
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
// Load the sources 这里有启动类
Set