接着上篇继续分析 SpringBoot 的启动过程。
SpringBoot的版本为:2.1.0 release,最新版本。
一.时序图
一样的,我们先把时序图贴上来,方便理解:
二.源码分析
回顾一下,前面我们分析到了下面这步:
public static ConfigurableApplicationContext run(Class<?>[] primarySources,
String[] args) {
// 前面分析了前一部分,SpringApplication的构造方法,本文分析后一部分的run()方法
return new SpringApplication(primarySources).run(args);
}
SpringApplication#run方法的内容较多,准备刷屏了:
public ConfigurableApplicationContext run(String... args) {
// StopWatch是Spring中一个任务执行时间控制的类,记录了任务的执行时间
StopWatch stopWatch = new StopWatch();
stopWatch.start();// 开始时间
ConfigurableApplicationContext context = null;
// 创建一个SpringBootExceptionReporter的List,记录启动过程中的异常信息
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 设置系统属性 "java.awt.headless" 为 true
configureHeadlessProperty();
// 获取所有启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
// 调用所有监听器的starting()
listeners.starting();
try {
// 根据入参创建 ApplicationArguments 对象
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
// 准备应用上下文 environment 对象
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
// 设置系统属性 "spring.beaninfo.ignore",是否忽略bean信息
configureIgnoreBeanInfo(environment);
// 获取打印 Banner 对象,也就是应用启动时候看到控制台输出的那个很大的 "SpringBoot"
Banner printedBanner = printBanner(environment);
// 创建IOC容器
context = createApplicationContext();
// 获取异常报告,在异常时报告异常信息
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 准备上下文
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
// 刷新上下文
refreshContext(context);
// 刷新完后
afterRefresh(context, applicationArguments);
stopWatch.stop();// 启动完成,记录启动结束时间
if (this.logStartupInfo) {// 打印详细启动时间信息
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
// 调用所有监听器的started(ctx)
listeners.started(context);
// 调用实现了 ApplicationRunner、CommandLineRunner 的对象的 run 方法
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);// 调用所有监听器的running()
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;// 返回上下文
}
上面从大体上介绍了SpringApplication在run()中做的事情,下面详细分析每步具体的操作。
SpringApplication#getRunListeners,获取所有启动监听器,一样的套路,通过SPI机制,通过工厂创建SpringApplicationRunListener的实例:
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
// 注意这个地方,我们第二次看到了getSpringFactoriesInstances这个方法,作用就是从类路径下 META-INF/spring.factories 下加载 SpringFactory 实例,最后传入 SpringApplicationRunListener.class,创建实例
return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
SpringApplicationRunListener.class, types, this, args));
}
从 spring.factories 里面找到如下内容:也就是最后拿到SpringApplicationRunListener的实例是EventPublishingRunListener的对象。
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
SpringApplication#configureHeadlessProperty,设置系统属性 "java.awt.headless" 。headless是系统的一种配置模式,在系统可能缺少显示设备、键盘或鼠标这些外设的情况下可以使用该模式,也就是告诉服务器,没有这些硬件设施,当需要这是设备信息的时候,别慌,我可以使用awt组件通过计算模拟出这些外设的特性。
private void configureHeadlessProperty() {
// this.headless在初始化的时候值为 "true"
System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, System.getProperty(
SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
}
SpringApplication#prepareEnvironment,准备应用上下文 environment 对象,像设置当前使用的配置文件profile,就在该方法内完成:
private ConfigurableEnvironment prepareEnvironment(
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 添加 ConversionService 转换器,保存启动参数
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 调用所有监听器的environmentPrepared(env)
listeners.environmentPrepared(environment);
// 将 environment 绑定到当前Spring上下文
bindToSpringApplication(environment);
// 判断是否用户自定义Environment
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader())
.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
SpringApplication#configureEnvironment,添加 ConversionService 转换器,保存启动参数,即最外面main的入参args数组:
protected void configureEnvironment(ConfigurableEnvironment environment,
String[] args) {
if (this.addConversionService) {
ConversionService conversionService = ApplicationConversionService
.getSharedInstance();
environment.setConversionService(
(ConfigurableConversionService) conversionService);
}
// 合并命令行启动参数到当前 enviroment,这个args就是最外层App启动传入的main方法的参数
configurePropertySources(environment, args);
// 设置当前运行环境的配置文件(dev/uat/prod)
configureProfiles(environment, args);
}
SpringApplication#configureProfiles ,设置当前运行环境的配置文件,会去查看"spring.profiles.active"中指定的是什么:
protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
// 去找当前激活的Profile是哪个 "spring.profiles.active"
environment.getActiveProfiles(); // ensure they are initialized
// But these ones should go first (last wins in a property key clash)
Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
environment.setActiveProfiles(StringUtils.toStringArray(profiles));
}
不小心有陷入进去了,再次回到run()。
接着看后面是获取Banner。根据环境找到Banner,默认找classpath下面的 banner.txt,Gitee上面很多管理系统打印出各种自定义名称。实现很简单,只要你提前制作好banner.txt,放到resources下面,启动的时候,就会加载你的,覆盖原始的Banner信息。这部分代码比较简单,只要跟进去看下就能看明白,考虑篇幅问题,省略过了。分享一个在线设计Banner.txt的网站。
如果需要关闭Banner输出,在App里面调用:
SpringApplication.setBannerMode(Banner.Mode.OFF);// 关掉Banner
接下来重点看一下 SpringApplication#prepareContext,分析SpringBoot如何准备上下文的:
private void prepareContext(ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
// 给当前 context 设置 environment
context.setEnvironment(environment);
// 设置 beanNameGenerator、resourceLoader、ConversionService
postProcessApplicationContext(context);
// 获取所有 ApplicationContextInitializer,调用其 initialize(ctx) 方法
applyInitializers(context);
// 调用所有监听器的contextPrepared(ctx)
listeners.contextPrepared(context);
if (this.logStartupInfo) {
// 打印启动进程信息
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);// 打印 profile 信息
}
// Add boot specific singleton beans 注册 SpringBoot 特有的单实例
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory) {
// 设置是否允许重写 BeanDefinition
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// 创建 BeanDefinitionLoader,用于加载 BeanDefinition,此处加载应用启动类有@SpringBootApplication 注解的主类
load(context, sources.toArray(new Object[0]));
// 调用所有监听器的contextLoaded(ctx)
listeners.contextLoaded(context);
}
稳住,快启动完了。接着看SpringApplication#refreshContext,完成刷新上下文的操作:
private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);// 刷新上下文,最后使用了AbstractApplicationContext#refresh方法,进入了Spring的启动流程
if (this.registerShutdownHook) {
try {
// 注册停止钩子,为了在应用程序shutdown的时候销毁所有 bean 实例
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
然后是到了SpringApplication#afterRefresh,这是一个模板方法,父类不提供实现,留给子类发挥想象实现,在context刷新好之后需要做的事情可以在此方法实现中完成。
最后就是 stopWatch.stop() 停止计时,打印启动耗时信息,回调监听器的started(ctx)方法,返回上下文context。
至此,SpringBoot启动过程分析完成。