Spring Boot 启动时做了什么?
启动从main方法调用静态方法SpringApplication.run(MainClass.class, args)开始,最终会返回一个应用上下文ConfigurableApplicationContext。
通常MainClass,即应用的主类会带@SpringBootApplication注解
TIP
Spring官方建议一个应用中只能有一个@SpringBootApplication or @EnableAutoConfiguration注解
初始化SpringApplication对象
构造方法中设置了资源加载器ResourceLoader,主资源primarySource,web应用类型,初始化ApplicationContextInitializer和ApplicationListener,应用的主类(main方法所在的类,默认情况下应该和primarySource是同一个类)。
应用类型分为:SERVLET应用、REACTIVE应用、非WEB应用。
在ResourceLoader中可以指定应用加载的资源和类加载器。
其中initializer和listener都会从META-INF/spring.factories中加载实例,大部分都定义在spring-boot-x.x.x.RELEASE.jar和spring-boot-autoconfigure-x.x.x.RELEASE.jar文件中。
TIP
spring.factories文件加载的缓存保存在SpringFactoriesLoader静态字段cache中SpringBoot2,cache是一个map,每一个类加载器对应cache中的一个key。
初始化完成后则调用SpringApplication对象的run方法(不是静态的run方法),run方法完成了整个应用的初始化。SpringApplication提供了一些set接口,可以在调用run方法之前对其进行定制。
spring应用生命周期
下面就正式进入run方法。SpringApplicationRunListener接口定义了spring应用生命周期中的几个状态,下面就根据这些状态完整介绍一下spring应用生命周期,在每个状态下都描述了进入该状态需要完成的操作,进入相应状态后会调用SpringApplicationRunListener接口的对应方法。
starting
进入starting状态前,最主要的操作是实例化SpringApplicationRunListener。SpringApplicationRunListener也是从META-INF/spring.factories中加载实例。
SpringApplicationRunListener接口是一个spring全生命周期的钩子,会在spring应用的各个生命周期被调用。
默认应该至少会有一个EventPublishingRunListener,定义在spring-boot-x.x.x.RELEASE.jar中。EventPublishingRunListener用于发布Spring事件SpringApplicationEvent,应用准备启动时就会发布一个ApplicationStartingEvent事件。
TIP
ApplicationListener和SpringApplicationRunListener接口的区别。
ApplicationListener接口只会监听自己所关心的spring事件并进行处理,一般情况下一个ApplicationListener只监听一个事件,可以自由新增事件或新增监听器,另ApplicationListener可用于spring和springboot;
SpringApplicationRunListener更多是作为springboot生命周期的钩子,在springboot生命周期的特定位置被调用,不太容易扩展(需要改底层源码),其中EventPublishingRunListener就是在各个生命周期节点发布事件,而ApplicationListener实现类只要关注了事件就会被触发。
WARNING
SpringApplicationRunListener抛异常会导致程序运行终止
environment prepared
解析命令行参数,存到ApplicationArguments接口的实现类DefaultApplicationArguments中,以"--"开头的参数会被解析为option参数,否则解析为非option参数。
初始化Environment,Environment中的getPropertySources()保存了所有的属性源,命令行参数解析后也被加入到属性源中,这样应用的命令行参数就可以做为Spring的properties来使用,也可以在Environment中读取到。
TIP
属性源是有优先级的,命令行属性源被设为最高优先级,因此在命令行中设置properties可以覆盖其他properties。查看完整的属性源优先级
从属性源中查找spring.profiles.active属性,设置为Environment的激活profile,另外,如果有在SpringApplication对象上添加额外的profile(通过调用setAdditionalProfiles),这些profile会合并到Environment中。
TIP
这里涉及的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来自定义应用上下文类型,如果这么做应用类型就会根据上下文类型来推断。
TIP
三种应用类型对应的上下文类型分别是:
- SERVLET -> AnnotationConfigServletWebServerApplicationContext
- REACTIVE -> AnnotationConfigReactiveWebServerApplicationContext
- 非WEB类型 -> AnnotationConfigApplicationContext
TIP
如果引入了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完成自动化配置,详见自动化配置
如果设置了registerShutdownHook=true,这一步还会注册应用程序关闭钩子,调用AbstractApplicationContext的doClose()方法,这里会发布ContextClosedEvent事件,当然还预留了一个onClose()方法交给子类去实现。
afterRefresh(),这个方法的默认实现是空的,应该是spring在这留的一个扩展点
running
运行所有的CommandLineRunner或ApplicationRunner bean
TIP
只要Spring上下文中包含CommandLineRunner或ApplicationRunner类型的bean,都会在应用启动时被运行,其区别是:CommandLineRunner只能获取到原始的字符串命令行参数,ApplicationRunner则可以获取到解析后的选项(类似于--option1=a --option2=b)
WARNING
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()进行初始化
属性源优先级
所有属性源中的属性都可以在代码中用@Value来直接使用,或直接注入Environment对象,使用Environment对象来动态获取,或可以绑定到@ConfigurationProperties上。
TIP
关于@ConfigurationProperties的使用,可以参照这篇文章
在不同的属性源中如果有相同的属性,将会按优先级顺序加载,优先级如下:
- 家目录下的
~/.spring-boot-devtools.properties文件(如果开启了devtools) - 用
@TestPropertySource标记的类 - 测试目录下的properties
- 命令行参数
- SPRING_APPLICATION_JSON属性,可以用
--spring.application.json='{"name":"test"}'方式在命令行设置一串json属性 - ServletConfig初始化参数
- ServletContext初始化参数
- 来自于
java:comp/env的JNDI属性 - Java系统属性,即
System.getProperties() - 操作系统环境变量
- 随机值属性源RandomValuePropertySource,会提供一些random.*属性
- jar包外带profile的属性文件,即application-{profile}.properties/yml
- jar包内带profile的属性文件
- jar包外不带profile的属性文件,即application.properties/yml
- jar包内不带profile的属性文件
- 用@PropertySource和@Configuration标记的类
- 用SpringApplication.setDefaultProperties设置的默认属性
TIP
第12和14条jar包外的属性文件是指在jar包当前目录下的./config目录或./目录