SpringApplicationRunListener是什么?
SpringApplicationRunListener 接口的作用主要就是在Spring Boot 启动初始化的过程中可以通过SpringApplicationRunListener接口回调来让用户在启动的各个流程中可以加入自己的逻辑。比如以下的方法
Galois通过注入各种Listener到SpringBoot的启动监听器列表中,实现了SpringBoot启动后初始化各种AgentService的功能
default void starting(ConfigurableBootstrapContext bootstrapContext) {
starting();
}
@Deprecated
default void starting() {
}
default void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
ConfigurableEnvironment environment) {
environmentPrepared(environment);
}
@Deprecated
default void environmentPrepared(ConfigurableEnvironment environment) {
}
default void contextPrepared(ConfigurableApplicationContext context) {
}
default void contextLoaded(ConfigurableApplicationContext context) {
}
default void started(ConfigurableApplicationContext context) {
}
default void running(ConfigurableApplicationContext context) {
}
default void failed(ConfigurableApplicationContext context, Throwable exception) {
}
问题所在
正常来说,所有listener的started
方法只会执行一次,就是在SpringBoot项目启动完成之后。但是目前遇到了执行两次的情况,在通过查看SpringBoot的源码之后,发现了 当SpringBoot作为SpringCloud的一员启动时,SpringBoot的run方法被执行了两次
。下面通过源码来一步步看看。
- 首先是进入
SpringBootApplication.run
方法来看看,里面注册了许多ApplicationListener
,其中有一个名为BootstrapApplicationListener
的ApplicationListener [org.springframework.cloud.bootstrap.BootstrapApplicationListener
,在SpringCloud
包下]
注意,SpringApplicationRunListener跟ApplicationListener不是同一个级别的
- 追踪代码,发现了这个
BootstrapApplicationListener
的实现方法,如下。最关键的地方,就是画线的这个方法,在这个方法里面,重新调用了SpringBootApplication.run
方法,但不是完全调用,是变更了一些SpringBoot的配置【不启动Web容器,不打印Banner
等等】,通过run
方法,来构造出context上下文
,并用来初始化SpringCloud的相关组件。
- 下图为
BootstrapApplicationListener
调用SpringBootApplication.run
方法的关键代码,也就是这个builder.run
的地方,会使得SpringBoot配置的SpringApplicationRunListener
重复执行一遍。【注意给context进行setId的这个操作
】
问题解决
如何处理SpringApplicationRunListener
被执行两遍的问题?首先,肯定是避免不了SpringBootApplication.run
方法被执行两遍的命运的。虽然图中代码写明,SpringCloud的自定义执行run
方法,给生成的context上下文
set了id,为bootstrap
,但是吧,给它设置这个id的时候,run
方法已经执行完了,所以实际上开头列出的SpringApplicationRunListener
接口,started
方法访问到的这个context上下文
,名字还是application
【第二个context名为application-1
】。
但是请注意,在SpringBoot的启动顺序中,如下图所示,第二次run
方法执行,是在prepareEnvironment
方法里面的,也就是说,两次run
方法执行的时候,先后执行了两次starting
方法,再分开来执行后续的其它hook方法。那这样的话,我们就可以根据这个特性,来构造一个解决方案。
- 方案如下
- 声明一个属性,类似
protected int invokeCount
- 在
starting
方法中,进行如下操作invokeCount++
- 在其它需要实现的hook方法中,如
started
,新增如下判断,当结果为true
时,表示可以正式执行hook实现,否则就跳过本次hook代码执行if (--invokeCount == 0) { return true; } return false;
- 总体思路就是,通过
starting
方法来判断当前共将执行几次run
,之后的hook方法就根据判断结果来决定是否执行
- 声明一个属性,类似
题外话
- 高版本的SpringBoot解决了这个问题,升级可忽略
- 通过实现接口
ApplicationListener
,同样可以监听SpringBoot启动事件,且不会发生重复调用,使用如下
/**
* @author liuguangsheng
* @since 1.0.0
**/
@Component
public class DemoListener implements ApplicationListener<ApplicationStartedEvent> {
@Override
public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
System.out.println("##############onApplicationEvent");
}
}