Spring Boot 启动时做了什么?
SpringBoot现在已经成为Java后端开发的事实标准,现在世面上绝大多数应用都是基于此。此外还有许多框架就是基于SpringBoot开发的,例如蚂蚁金服的SofaBoot。
今天就来聊一聊SpringBoot的底层是怎么运作的。
启动入口
一个典型的SpringBoot应用,入口一般长这样:
@SpringBootApplication
public class BaoziApplication {
public static void main(String[] args) {
SpringApplication.run(BaoziApplication.class, args);
}
}看起来有一种简单到极致的美。但就是这么一行简单的代码,撑起了背后的庞然大物。
SpringBoot启动程序也是从这里开始,SpringApplication.run最终会返回一个应用上下文ConfigurableApplicationContext,一般情况下直接忽略即可,context还有很多方式可以获取。
通常MainClass,即应用的主类会带@SpringBootApplication注解,这是SpringBoot的关键。
初始化SpringApplication对象
SpringApplication包含了应用的主要信息,启动的第一步就是要创建这个对象。代码不多就几行,都在构造方法中。
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}我们来逐步解析
resourceLoader
在ResourceLoader中可以指定应用加载的资源和类加载器。没有特殊的需求可以不需要设置。
primarySources
主资源primarySource,也就是用@SpringBootApplication注解标记的类。
提示
虽说代码里的主资源是一个list,但Spring官方建议只传1个,一个应用中只能有一个@SpringBootApplication or @EnableAutoConfiguration注解
webApplicationType
应用类型分为:SERVLET应用、REACTIVE应用、非WEB应用。
这是根据classpath中是否存在相应的类来推断的,如果引入spring-webmvc就会设置为SERVLET应用,开启默认的8080端口,提供HTTP服务。
如果未引入WEB组件,则是非WEB引用,这种应用在执行完程序后会自动退出进程(除非通过其他方式手动设置了常驻运行线程),很适合跑一些小型命令行任务。
提示
关于REACTIVE应用,后面专门写一篇文章来说,这类应用用在需要实时交互的场景,比如打车软件,还有交互式的AI agent。
initializer和listener
初始化ApplicationContextInitializer和ApplicationListener
其中initializer和listener都会从META-INF/spring.factories中加载,大部分都定义在spring-boot-x.x.x.RELEASE.jar和spring-boot-autoconfigure-x.x.x.RELEASE.jar文件中。
提示
spring.factories文件加载的缓存保存在SpringFactoriesLoader静态字段cache中,cache是一个map,每一个类加载器对应cache中的一个key。
mainApplicationClass
查找应用的主类(main方法所在的类,默认情况下应该和primarySource是同一个类)。
以上初始化完成后就开始调用SpringApplication对象的run方法(不是静态的run方法),run方法完成了整个应用的初始化。SpringApplication提供了一些set接口,可以在调用run方法之前对其进行定制。
SpringBoot的应用生命周期
正式进入run方法。SpringBoot通过SpringApplicationRunListener接口定义了应用全生命周期的回调函数,应用到了一个新的状态就会回调RunListener的对应方法。借着这个我们就可以把SpringBoot生命周期分为几个阶段。
下面就根据这些状态完整介绍一下spring应用生命周期,在每个状态下都描述了进入该状态需要完成的操作,进入相应状态后会调用SpringApplicationRunListener接口的对应方法。
starting
进入starting状态前,最主要的操作是实例化SpringApplicationRunListener。SpringApplicationRunListener也是从META-INF/spring.factories中加载实例。
默认应该至少会有一个EventPublishingRunListener,定义在spring-boot-x.x.x.RELEASE.jar中。EventPublishingRunListener用于发布Spring事件SpringApplicationEvent,应用准备启动时就会发布一个ApplicationStartingEvent事件。
ApplicationListener和SpringApplicationRunListener接口的区别。
- ApplicationListener接口只会监听自己所关心的spring事件并进行处理,一般情况下一个ApplicationListener只监听一个事件,可以自由新增事件或新增监听器,另ApplicationListener可用于spring和springboot;
- SpringApplicationRunListener更多是作为springboot生命周期的钩子,在springboot生命周期的特定位置被调用,其中EventPublishingRunListener就是在各个生命周期节点发布事件,而ApplicationListener实现类只要关注了事件就会被触发。
注意
SpringApplicationRunListener抛异常会导致程序运行终止
environment prepared
这一阶段要解析命令行参数,存到ApplicationArguments接口的实现类DefaultApplicationArguments中,以"--"开头的参数会被解析为option参数,否则解析为非option参数。
初始化Environment,Environment中的getPropertySources()保存了所有的属性源,命令行参数解析后也被加入到属性源中,这样应用的命令行参数就可以做为Spring的properties来使用,也可以在Environment中读取到。
属性源是有优先级的,CommandLine属性源被设为最高优先级,因此在命令行中设置properties可以覆盖其他properties。关于SpringBoot的配置文件,可以看这篇:一篇文讲清楚SpringBoot配置文件的弯弯绕绕
从属性源中查找spring.profiles.active属性,设置为Environment的激活profile,另外,如果有在SpringApplication对象上添加额外的profile(通过调用setAdditionalProfiles),这些profile会合并到Environment中。
profile的作用大致可以理解为应用标签,例如在不同的环境设置不同的profile,用于区分控制应用的行为。默认一定有一个active profile
提示
这里涉及的configureEnvironment/configurePropertySources/configureProfiles这几个方法都可以被重写,以便定制Environment。
把Environment绑定到SpringApplication,以便后续使用。
环境准备完成后,由EventPublishingRunListener广播了一个ApplicationEnvironmentPreparedEvent事件,订阅了此事件的监听器有:
- ConfigFileApplicationListener,在这里会加载application.properties文件
- LoggingApplicationListener,这里会初始化应用的日志系统,后面单独说明
context prepared
打印banner,属性banner.location=classpath:banner.txt定义了banner的内容,可以添加banner.txt文件来修改banner。
创建应用上下文实例,根据应用类型来决定上下文的类型,创建实例。可以通过SpringApplication的setApplicationContextClass来自定义应用上下文类型,如果这么做应用类型就会根据上下文类型来推断。
相关信息
三种应用类型对应的上下文类型分别是:
- SERVLET -> AnnotationConfigServletWebServerApplicationContext
- REACTIVE -> AnnotationConfigReactiveWebServerApplicationContext
- 非WEB类型 -> AnnotationConfigApplicationContext
提示
如果引入了SpringBoot Actuator特性,Spring上下文和上下文中的bean可以通过beans端点查看
从META-INF/spring.factories中实例化SpringBootExceptionReporter,用于报告应用启动时产生的异常。
后处理应用上下文postProcessApplicationContext(可重写)。这里很重要的是把SpringApplication中的resourceLoader和classloader设置到上下文中,如果构造SpringApplication对象时指定了resourceLoader则需要关注。
调用所有ApplicationContextInitializer实例的initialize方法进行初始化操作。
context loaded
打印应用的启动日志和激活的profile日志。
使用context.getBeanFactory().registerSingleton()方法注册了几个特殊的单例bean: springApplicationArguments、springBootBanner。
从primarySources和sources中加载BeanDefinition,一般至少有一个primarySource(springboot的主类),这里的source可以是配置类、package或xml、groovy。
加载是通过BeanDefinitionLoader这个类的实例来完成的,BeanDefinitionLoader提供了几个重载的load方法,可分别从各种source中加载BeanDefinition。
- 如果资源是带注解的Class将委托给AnnotatedBeanDefinitionReader来加载
- 如果资源是xml文件将委托给XmlBeanDefinitionReader来加载
- 如果资源是groovy(SpringBoot支持)将委托给GroovyBeanDefinitionReader来加载
- 如果资源是Package将委托给ClassPathBeanDefinitionScanner进行包路径扫描
加载完成后在beanFactory中就可以使用BeanDefinition。
started
调用AbstractApplicationContext的refresh()进行上下文刷新。
上下文刷新最主要的操作是把BeanDefinition转化为Bean对象实例。
自动化配置也在这一步完成,在refresh()中会调用invokeBeanFactoryPostProcessors(beanFactory),进而通过ConfigurationClassPostProcessor完成自动化配置,详见自动化配置
提示
面试中经常问的spring三级缓存,就在refresh()方法中,可以重点看一下。这部分内容后面也会再单独开一篇来讲。
如果设置了registerShutdownHook=true,这一步还会注册应用程序关闭钩子,调用AbstractApplicationContext的doClose()方法,这里会发布ContextClosedEvent事件,当然还预留了一个onClose()方法交给子类去实现。
afterRefresh(),这个方法的默认实现是空的,应该是spring在这留的一个扩展点
running / ready
最早这个状态叫running,不知道在哪个版本改成了ready。
运行所有的CommandLineRunner或ApplicationRunner bean
只要Spring上下文中包含CommandLineRunner或ApplicationRunner类型的bean,都会在应用启动时被运行,其区别是:
- CommandLineRunner只能获取到原始的字符串命令行参数
- ApplicationRunner则可以获取到解析后的选项(类似于--option1=a --option2=b)
注意
Runner抛出异常会导致程序运行终止
failed
以上除了进入starting状态之前做的一些初始化操作之外,其他任何一处发生异常都会使应用进入failed状态,并进行应用退出的处理:处理退出码、报告异常。
关于日志系统加载
SpringBoot提供了对日志系统的抽象,顶级抽象类是org.springframework.boot.logging.LoggingSystem,下面有几个实现类是LogbackLoggingSystem、Log4J2LoggingSystem分别表示logback日志系统和log4j2日志系统。
日志系统的加载步骤:
- 监听到ApplicationStartingEvent事件,从当前classpath中查找特定的类,如果找到ch.qos.logback.core.Appender则创建LogbackLoggingSystem,如果找到org.apache.logging.log4j.core.impl.Log4jContextFactory则创建Log4J2LoggingSystem
- 调用日志系统的beforeInitialize()进行预初始化
- 监听到ApplicationEnvironmentPreparedEvent事件,对日志系统进行初始化
- 创建日志文件对象,日志文件的位置在
${logging.file.path}+${logging.file.path} - 把日志相关的属性设置到SystemProperty中
- 调用日志系统的initialize()进行初始化
提示
在日志系统加载完成后,打印出来的日志才是标准格式
配置文件加载
SpringBoot把配置文件都抽象成了属性源 PropertySource,详细可以查看这篇文章:一篇文讲清楚SpringBoot配置文件的弯弯绕绕
两张大图
最后用两张图来总结一下,一张图是完整启动过程,另一张是SpringBoot核心类图。

