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");
}
}

SpringApplicationRunListener在SpringBoot启动过程中被调用两次的情况通常发生在SpringBoot作为SpringCloud的一部分运行时。原因在于BootstrapApplicationListener会再次调用SpringApplication.run,导致listener的started方法执行两次。解决方案是通过在starting方法中记录调用次数,在后续hook方法中判断是否已执行过,以避免重复执行。高版本的SpringBoot已经修复此问题,可通过升级或使用ApplicationListener接口避免重复调用。
8744

被折叠的 条评论
为什么被折叠?



