零、本文说明
本文从“期望在Spring Boot启动时运行自定义任务”这一需求出发,简单说明了探究Application启动过程的方式和其中的重要节点。在介绍几种实现方式的同时,附带说明执行时机和原理参考。
值得注意:虽然本文基于Spring Boot,但是探究启动过程时,不可避免要结合Spring的内容。
本文探究基于:IDEA 2022.2.5社区版 + Spring Boot 2.6.0 (对应的Spring 5.3.13)
一、Spring Boot的启动流程
了解Spring Boot的启动流程,主要目的便是可以知道自定义的启动任务应该放在哪个环节去执行,从而更好地满足各种各样奇奇怪怪的需求。
(如果你已经很熟悉这个流程了,则可以跳过,直接看自定义启动运行任务的几种方式)
而若是要想知道Spring Boot启动流程的细节,可以通过以下途径:
1、探索run方法
进入项目启动类,再进入到SpringApplication.run()方法的实现
启动类
不断地找run()方法的实际实现方法,可以找到下文这个Run方法。这便是应用启动时,会依次运行的内容。
SpringApplication.class
值得注意的是,这短短的几十行代码,里面涉及的内容实在是太庞大了,无论是从业者还是为了学习,都应该去深入探究一番,不仅仅是可以学到启动流程,还能学到很多Spring相关内部原理性的很多东西。
2、应用程序启动追踪
Spring允许使用ApplicationStartup来追踪应用启动过程(所以Spring Boot能追踪)
如下图所示,自定义SpringApplication方式,设置BufferingApplicationStartup缓存启动过程
启动类
Pom.xml引入actuator依赖包
actuator文档:https://docs.spring.io/spring-boot/docs/3.2.0-SNAPSHOT/actuator-api/htmlsingle/#overview
<!-- Spring Boot Actuator 模块提供了生产级别的功能,比如健康检查,审计,指标收集,HTTP 跟踪等,帮助我们监控和管理Spring Boot 应用。 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
application.yml中新增actuator配置(目的就是提供获取启动追踪的缓存的路径)
# actuator 配置
management:
server:
port: 12313 # 使用独立端口,不设置的话和web端口就一样
endpoints:
web:
exposure:
include: startup # 暴露 startup 端点
启动项目后,用get请求地址(不需要附带入参):localhost:12313/actuator/startup
Postman中请求示例
将会获得一个很长的JSON格式的启动追踪记录,可以保存成文本文件慢慢阅读。
UP主稍微阅读了一下,这个记录内容显示的启动流程大概如下(详细的自己找个项目去阅读完整版):
spring.boot.application.starting
spring.boot.application.environment-prepared
spring.boot.application.context-prepared
spring.boot.application.context-loaded
spring.beans.instantiate
spring.context.config-classes.parse(用ConfigurationClassPostProcessor 进行配置类的解析阶段)
spring.context.beandef-registry.post-process(调用BeanDefinitionRegistryPostProcessor bean)
spring.context.bean-factory.post-process
spring.beans.instantiate
spring.context.config-classes.enhance
spring.beans.instantiate(interface javax.servlet.ServletContextListener )
spring.boot.webserver.create(TomcatServletWebServerFactory)
spring.beans.instantiate(Application)
spring.beans.instantiate(按照依赖关系,先后实例化各种自实现的bean)(此处涉及Spring框架对反转控制(IoC)原则的实现。IoC也被称为依赖注入(DI))
spring.beans.smart-initialize(SmartInitializingSingleton Bean的初始化)
spring.context.refresh
spring.boot.application.started
spring.boot.application.read
3、官方文档参考
Spring官方文档(中文网):https://springdoc.cn/spring/core.html#core.appendix.application-startup-steps
SpringBoot官方文档描述Application event发布顺序:
SpringBoot官方文档描述Application event发布顺序(中文网):https://springdoc.cn/spring-boot/features.html#features.spring-application.application-events-and-listeners
二、Application启动运行自定义任务的方式列举
下面两种方式在Spring Boot Application中的执行时机:Application已启动(applicationStartedEvent),应用程序的Liveness为CORRECT状态。
注意,下面两种方式的方法都执行完以后,应用程序的ReadinessState才会变成ACCEPTING_TRAFFIC状态。此时应用才算准备好了,可以开始处理外部来的请求。
特别说明:因为下面两种方式执行的时候,Application都启动好了,所以可以任意使用@Autowired等方式注入bean依赖项来用,无需顾虑。
1、实现ApplicationRunner接口
@Component
public class StartRunApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
System.out.println("--------------------开始运行StartRunApplicationRunner-------------------");
for (String optionName : args.getOptionNames()) {
System.out.println("optionName: " + optionName + ", optionValue: " + args.getOptionValues(optionName));
}
System.out.println("--------------------结束运行StartRunApplicationRunner-------------------");
}
}
启动后,若有传入启动参数则可以读取到。
注意,启动参数需要这样写(多个参数用空格隔开,每个参数的参数名需要以两个减号开头,以等号的形式设置参数值):--a=testArg1 --b=Arg2
示例
2、实现CommandLineRunner接口
@Component
public class StartRunCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("--------------------开始运行StartRunCommandLineRunner-------------------");
for (String arg : args) {
System.out.println(arg);
}
System.out.println("--------------------结束运行StartRunCommandLineRunner-------------------");
}
}
启动后,若有传入启动参数则可以读取到。
注意,启动参数需要这样写(多个参数用空格隔开):testArg1 Arg2
示例
三、特殊需求情况下,利用监听器、回调来执行自定义任务
预计在启动期间运行的任务应该交由
CommandLineRunner
和ApplicationRunner
组件执行,而不是使用Spring组件的生命周期回调,如@PostConstruct
。事件监听器不应该运行潜在耗时的任务,因为它们默认是在同一个线程中执行。 考虑使用 ApplicationRunner 和 CommandLineRunner 代替。
——官方文档说明
常规情况下,使用ApplicationRunner和CommandLineRunner就已经足够满足日常需求。
若是真有某个需求,一定要在Application启动的过程中执行某些任务,则可以考虑以下的几种方式。
特别说明:如果监听器要监听的事件在bean可以开始实例化前(比如BeanDefined都还没开始),请不要使用@Autowired等方式注入bean依赖项,因为你会获得null。
1、Spring bean生命周期回调(Spring Boot中自然也适用)
JSR-250的 @PostConstruct 和 @PreDestroy 注解通常被认为是在现代Spring应用程序中接收生命周期回调的最佳实践。使用这些注解意味着你的Bean不会被耦合到Spring特定的接口。
——官方说明
官网文档参考:https://docs.spring.io/spring-framework/reference/core/beans/factory-nature.html
注意:这两个注解,是应用于所处的Bean上的,目标就是为这个Bean做一些额外的事情。
@Component
public class MyPostConstruct {
/**
* 让Bean在容器对Bean设置了所有必要的属性后执行初始化工作(此时例如可以用来执行预填充之类的行为)
*/
@PostConstruct
public void init() {
System.out.println("--------------------MyPostConstruct开始运行PostConstruct-------------------");
System.out.println("--------------------MyPostConstruct结束运行PostConstruct-------------------");
}
/**
* 让Bean在包含它的容器被销毁时获得一个回调(其实一般就是关闭应用的时候)(此时例如可以用来执行清空缓存之类的行为)
*/
@PreDestroy
public void destroy() {
System.out.println("--------------------MyPostConstruct开始运行PreDestroy-------------------");
System.out.println("--------------------MyPostConstruct结束运行PreDestroy-------------------");
}
}
2、Application发布的事件的监听器(@EventListener)
关于@EventListener:
需要Spring版本>=4.2。
只能应用于方法上。
用于监听被发布到ApplicationContext中的ApplicationEvent(本质就是观察者模式)。
官网介绍:https://docs.spring.io/spring-framework/reference/core/beans/context-introduction.html
注:下面代码示例的事件监听,是按照Spring Boot发布的顺序从早到晚放置的。(实际能监听的事件还有很多,只要是继承自ApplicationEvent都行,请自行探索)
注:如果你好奇代码里为啥标了无效二字的监听器,实际真的监听不到事件,那么,请拉到文章后面看扩展部分内容(涉及监听器注册原理)。
@Configuration
public class MyApplicationListener{
/**
* 监听ApplicationStarting事件(无效): 一个 ApplicationStartingEvent 在运行开始时被发布,但在任何处理之前,除了注册监听器和初始化器之外。
*/
@EventListener
public void applicationStartingListener(ApplicationStartingEvent applicationStartingEvent) {
System.out.println("-----------------ApplicationStarting----------------");
}
/**
* 监听ApplicationEnvironmentPrepared事件(无效): 当在上下文中使用的 Environment 已知,但在创建上下文之前,将发布 ApplicationEnvironmentPreparedEvent。
*/
@EventListener
public void applicationEnvironmentPreparedListener(ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) {
System.out.println("-----------------ApplicationEnvironmentPrepared----------------");
}
/**
* 监听ApplicationContextInitialized事件(无效): 当 ApplicationContext 已准备好并且 ApplicationContextInitializers 被调用,但在任何Bean定义被加载之前,ApplicationContextInitializedEvent 被发布。
*/
@EventListener
public void applicationContextInitializedListener(ApplicationContextInitializedEvent applicationContextInitializedEvent) {
System.out.println("-----------------ApplicationContextInitialized----------------");
}
/**
* 监听ApplicationPrepared事件(无效): 一个 ApplicationPreparedEvent 将在刷新开始前但在Bean定义加载后被发布。
*/
@EventListener
public void applicationPreparedListener(ApplicationPreparedEvent applicationPreparedEvent) {
System.out.println("-----------------ApplicationPrepared----------------");
}
/**
* 监听ApplicationContextRefreshedEvent事件
*/
@EventListener(classes = {ContextRefreshedEvent.class}) // 允许的写法2,这里可以写多个要监听的类
public void contextRefreshedEventListener() {
System.out.println("-----------------ContextRefreshedEvent----------------");
}
/**
* 监听ApplicationStarted事件: 在上下文被刷新之后,但在任何应用程序和命令行运行程序被调用之前,将发布一个 ApplicationStartedEvent。
*/
@EventListener
public void applicationStartedListener(ApplicationStartedEvent applicationStartedEvent) {
System.out.println("-----------------ApplicationStarted----------------");
}
/**
* 官方文档:应用组件可以在任何时候通过注入 ApplicationAvailability 接口并调用其上的方法来检索当前的可用性状态。
* 一个应用程序的 “Liveness” 状态告诉我们它的内部状态是否允许它正常工作,或者在当前失败的情况下自行恢复。
* 一个broken状态的 “Liveness” 状态意味着应用程序处于一个无法恢复的状态,基础设施应该重新启动应用程序。
* Spring Boot应用程序的内部状态大多由Spring ApplicationContext 表示。
* 如果 application context 已成功启动,Spring Boot就认为应用程序处于有效状态。一旦context被刷新,应用程序就被认为是活的 <br />
*/
@EventListener
public void onLivenessStateChange(AvailabilityChangeEvent<LivenessState> event) {
switch (event.getState()) {
case CORRECT:
System.out.println("LivenessState is correct");
break;
case BROKEN:
System.out.println("LivenessState is broken");
break;
}
}
/**
* 监听ApplicationReady事件: 在任何ApplicationRunner 和 CommandLineRunner被调用后,将发布一个 ApplicationReadyEvent。
*/
@EventListener
public void applicationReadyListener(ApplicationReadyEvent applicationReadyEvent) {
System.out.println("-----------------ApplicationReady----------------");
}
/**
* 官方文档:应用组件可以在任何时候通过注入 ApplicationAvailability 接口并调用其上的方法来检索当前的可用性状态。
* 应用程序的 “Readiness” 状态告诉平台,该应用程序是否准备好处理流量。failing状态的 “Readiness” 告诉平台,它暂时不应该将流量发送到该应用程序。
* 这通常发生在启动期间,当 CommandLineRunner 和 ApplicationRunner 组件还在被处理的时候,或者是应用程序觉得目前负载已经到了极限,不能再处理额外的请求的时候。
* 一旦 ApplicationRunner 和 CommandLineRunner 被调用完成,就认为应用程序已经准备好了
*/
@EventListener
public void onReadinessStateChange(AvailabilityChangeEvent<ReadinessState> event) {
switch (event.getState()) {
case ACCEPTING_TRAFFIC:
System.out.println("ReadinessState is accepting traffic");
break;
case REFUSING_TRAFFIC:
System.out.println("ReadinessState is refusing traffic");
break;
}
}
/**
* 监听ApplicationFailed事件: 如果启动时出现异常,将发布一个 ApplicationFailedEvent
*/
@EventListener
public void applicationFailedListener(ApplicationFailedEvent applicationFailedEvent) {
System.out.println("-----------------ApplicationFailed----------------");
}
}
3、Application发布的事件的监听器(实现ApplicationListener接口)
这个方式其实本质上和使用@EventListener注解是一样的,也是为了能够产生监听器ApplicationListener实例去监听ApplicationEvent。
存在的问题也类似,在ContextRefreshedEvent事件前的那些事件,也都是监听不到的。
原因请拉到文章后面看扩展部分内容(涉及监听器注册原理)。
但是,和@EventListener注解不一样。这个方式可以通过配置,让Application启动时提前加载自定义的类并实例化,使得可以监听到很完整的各种ApplicationEvent。
当然,如果你要监听的事件,是在ContextRefreshedEvent事件之后发布的,那么就不需要担心监听不到的事情,也不需要额外配置,直接照常实现ApplicationListener接口即可。
先看ApplicationListener接口源码:
可以看到,只要事件继承自ApplicationEvent,就都可以通过实现ApplicationListener接口来实现监听。
实现事件监听器示例(这里监听的是ApplicationContextInitializedEvent事件):
@Component
public class MyApplicationContextInitializedListener implements ApplicationListener<ApplicationContextInitializedEvent> {
@Override
public void onApplicationEvent(ApplicationContextInitializedEvent event) {
System.out.println("---------------ApplicationContextInitializedEvent------------------");
}
}
由上文可知,仅仅是靠这个类,是监听不到ApplicationContextInitializedEvent的,因为事件的发布,早于这个监听器bean实例化后真正开始监听的时间。
这时候,就需要使用配置文件spring.factories。
在项目的resources文件夹下,新建一个META-INF文件夹,然后新建一个spring.factories文件。
在spring.factories里面写入:
org.springframework.context.ApplicationListener=com.basic.happytest.modules.startRun.MyApplicationContextInitializedListener,\
com.basic.happytest.modules.startRun.MyApplicationStartingListener
(org.springframework.context.ApplicationListener 是接口路径)
(com.basic.happytest.modules.startRun.MyApplicationContextInitializedListener是实现类的路径,也就是希望提前加载的监听器)
(多个实现类可以用英文逗号隔开,反斜线是为了可以换行书写更美观一些)
至于为什么弄一个spring.factories就可以了,请拉到文章后面看扩展部分内容(涉及原理)。
4、自实现SpringApplicationRunListeners以监听Application发布的事件
不推荐,原理可以看扩展部分的内容中,关于spring.factories配置的原因的内容。
看完其实就知道可以怎么去做了。
5、自实现Java Servlet监听器
javax.servlet.ServletContextListener:监听Java Web的ServletContext生命周期变化。
注意:这里的是ServletContext,和本文其他地方的Context是不一样的。
注意:此处实测利用@Autowired注入依赖项是OK的。(但是我网上有看到有人说不行,不是很清楚为什么)
ServeletContext和事件监听生效的时机,实际也是在Application的ContextRefresh期间,详情请拉到文章后面看扩展部分内容。
@Component
public class MyServletContextListener implements ServletContextListener {
/**
* ServletContext初始化后执行
*/
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("--------------------ServletContext Initialized -------------------");
}
/**
* ServletContext销毁触发,一般就是结束应用的时候
*/
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("--------------------ServletContext Destroyed -------------------");
}
}
javax.servlet.ServletContextAttributeListener:监听Java Web的ServletContext对象共享数据变化时刻。
更多事件监听的内容,请自行探索。
四、扩展
1、@EventListener注解的监听器生效时机及原理说明
@EventListener注解的处理是通过内部 EventListenerMethodProcessor bean 执行的。
EventListener.java
此外,EventListenerMethodProcessor继承自BeanFactoryPostProcessor接口,实现的postProcessBeanFactory方法,在容器将所有bean解析成beanDefinition后,实例化之前执行(方法内容是:先获取所有实现EventListenerFactory接口的单例对象,排序后保存在容器中)。
当所有单例 bean 都初始化完成以后,Spring的IOC容器会回调实现SmartInitializingSingleton接口的EventListenerMethodProcessor 的 afterSingletonsInstantiated()方法。afterSingletonsInstantiated方法本身在各种判断后,会调用processBean方法。processBean方法会注册@EventListener注解的方法作为独立的ApplicationListener实例,并关联到ApplicationContext的initialMulticaster(事件广播器)上,这之后,@EventListener注解的监听器才会开始监听事件。
所以,不妨找找源码里,什么时候执行afterSingletonsInstantiated()方法。
也就知道具体什么时候@EventListener注解的监听器才会开始生效。
让我们回到SpringApplication.run方法,这个启动开始的地方。
SpringApplication.java
进入到refreshContext(context);方法。
SpringApplication.java
继续深入。
SpringApplication.java
一路找到ConfigurableApplicationContext接口实现类AbstractApplicationContext里的refresh方法。
AbstractApplicationContext.java
我们可以看到refresh方法里有一个finishBeanFactoryInitialization(beanFactory);的环节,注释写有singletons,所以进去看看实现。
AbstractApplicationContext.java
继续进入到beanFactory.preInstantiateSingletons()里面,可以找到ConfigurableListableBeanFactory接口的实现类DefaultListableBeanFactory。
DefaultListableBeanFactory.java
这里面,我们可以看到,先是在那里实例化bean,然后是执行了afterSingletonsInstantiated方法。
前文我们提到,EventListenerMethodProcessor 的 afterSingletonsInstantiated方法本身在各种判断后,会调用processBean方法。processBean方法会注册@EventListener注解的方法作为独立的ApplicationListener实例,并关联到ApplicationContext的initialMulticaster(事件广播器)上。
这时候,我们再回过头,就会发现,实际上afterSingletonsInstantiated方法如果执行完了,立马就是发布ContextRefreshedEvent事件。
那么,可以知道,在ContextRefreshedEvent事件前的那些事件,都是监听不到的。
2、配置spring.factories后自定义ApplicatonListener实现类就能提前注册的原因
让我们回到SpringApplication.java中,先看看SpringApplication的构造方法。
SpringApplication.java
可以看到,在Spring(Spring Boot)启动流程中,在初始化SpringApplication对象时,会先通过SPI方式获取所有的ApplicationListener的实现类。
SpringApplication.java
实际就是读取classPath路径所有META-INF/spring.factories文件,来找ApplicationListener的实现类(在这里说的spring.factories,因为是Spring Boot项目,所以实际上是spring-boot的jar包里的对应文件)。
我们自己建一个META-INF/spring.factories文件,为的就是在这个时候,让自定义的ApplicationListener实现类也被加载进去。
那么,为什么在这里被加载进来,就会生效呢?
继续来看看Application启动时就执行的的run方法。
SpringApplication.java
可以看到箭头指向的内容——获取SpringApplicationRunListeners的实例对象。
进去查看【获取SpringApplicationRunListeners】的对象方法的实现。
SpringApplication.java
可以发现,【SpringApplicationRunListeners的实例对象】的来源,也是通过SPI方式获取所有的SpringApplicationRunListener的实现类。
查看这个loadFactoryNames方法实现。其中注释说明是重点。
SpringFactoriesLoader
我们可以在外部库里找到spring.factories。
可以看到Spring Boot的spring.factories
Spring Boot包
Spring Boot的spring.factories里面可以找到SpringApplicationRunListener
Spring Boot的spring.factories
这里可以发现,加载的【SpringApplicationRunListeners的实例对象】实际就是EventPublishingRunListener。
EventPublishingRunListener的构造函数,也就是实例化时,是会依据入参,去获取SpringAppliction所有监听器(实际就是前文提及的SpringApplication初始化时去加载的ApplicationListener实现类)关联到initialMulticaster(事件广播器)上。
EventPublishingRunListener
EventPublishingRunListener其实就是进行发布各种SpringApplicationEvents的,Spring Boot启动过程中的Application事件就会从这里用initialMulticaster(事件广播器)发出,并由与其相关联的监听器接收。
比如run方法里:
SpringAppliction.java
至此,一切都清晰明了了。
我们去整一个spring.factories,为的就是让SpringApplication初始化时把我们【自定义的ApplicationListener实现类】给加载了。让其在实例化【SpringApplicationRunListeners的实例对象】时,能够把【自定义的ApplicationListener实现类】给关联到initialMulticaster(事件广播器)上面。使得可以监听到启动过程中完整的事件。
(其实这里实际也可以看出来,还有一种偏门的监听方式,就是去自实现SpringApplicationRunListeners,并配置到spring.factories里,这样也行)
3、自实现ApplictionListener监听器的注册时机(生效时机)
让我们再次回到SpringApplication.run方法,这个启动开始的地方。
SpringApplication.java
进入到refreshContext(context);方法。
SpringApplication.java
继续深入。
SpringApplication.java
一路找到ConfigurableApplicationContext接口实现类AbstractApplicationContext里的refresh方法。
AbstractApplicationContext.java
可以看到有一个注册监听器的环节:registerListeners();
进入方法内部看看。
AbstractApplicationContext.java
方法注释已经说明了一切,我们自定义的ApplicationListener实现类(可能还有其他的不是我们自定义的监听器),如果没有去配置spring.factories,就会在此时被找到并实例化,然后关联到ApplicationContext的initialMulticaster(事件广播器)上,开始监听事件。
(你可能会注意到源码底下的这个Publish early application events,但是实际在refresh方法的最开始,prepareRefresh()是将earlyApplicationEvents设置为空的。所以即便真有事件,也是refresh里产生的。更早的外部的,是没有的。)
4、Spring Boot关于ServletContext的初始化和ServletListener相关事件监听器生效
让我们又又又一次回到SpringApplication.run方法,这个启动开始的地方。
SpringApplication.java
进入到refreshContext(context);方法。
SpringApplication.java
继续深入。
SpringApplication.java
一路找到ConfigurableApplicationContext接口实现类AbstractApplicationContext里的refresh方法。
AbstractApplicationContext.java
这次,我们要看的是onRefresh(),而且是实现类ServletWebServerApplicationContext的具体实现
ServletWebServerApplicationContext.java
进入createWebServer方法。
ServletWebServerApplicationContext.java
可以看到,这里就是在创建/初始化ServletContext。
ServletWebServerApplicationContext.java
让我们先进入这个获取自我初始化方法getSelfInitializer()中
ServletWebServerApplicationContext.java
可以看到,getServletContextInitializerBeans() 在获取ServletContxt初始化相关的bean。
进去瞧瞧。
ServletWebServerApplicationContext.java
这里我们就找到了,原来不仅仅是EventListener,还有其他的东西,都是这个时候实例化后收集的。
再进去看看细节。
ServletContextInitializerBeans.java
addAdaptableBeans()值得留意
ServletContextInitializerBeans.java
终于,我们找到了EventListener,但是这里还没完,因为这个类只是其收集作用,真正注册监听器还没到时候。
接下来,进入getSelfInitializer().onStartup(servletContext)的onStartup
选择ServletContextInitializer接口实现类RegistrationBean
RegistrationBean.java
选择子类ServletListenerRegistrationBean(Filter啥的就是其他的RegistrationBean子类)
ServletListenerRegistrationBean.java
此时,可以看到ServletListenerRegistrationBean就是专门注册ServletContext的相关监听器的。
ServletListenerRegistrationBean.java
至此,ServletContext的初始化位置和ServletListener相关事件监听器注册,都已经找到。
我们也就能知道,ServletContext初始化是在ApplicationContext refresh的时候,ServletContext相关的监听器也是初始化的时候一并注册。
五、后话
实际上,关于@EventListener、ApplicationListener。其实我们实际开发过程中,更多时候并不是用来在Spring Boot(或Spring)的Application启动过程执行自定义任务。而是去自实现ApplicationEvent,将事件发布到上下文中,然后用监听器去监听发布的事件,做相应的处理。(即观察者模式)