我们知道Spring有两大类事件,一类是Application事件,超类是SpringApplicationEvent,这类事件是在Spring程序启动时,过程中分为几个阶段,每进行一个阶段,发出一个事件,依次对应ApplicationStartingEvent到ApplicationReadyEvent。标志着Application从启动开始到启动完成,各个阶段的分割点。参考Spring启动过程中Application事件的监听与处理。
另一类是ApplicationContext事件,超类是ApplicationContextEvent,表示ApplicationContext生命周期内的各个阶段。参考Spring的容器事件——ApplicationContextEvent的监听与处理
我们在监听上述两大类事件时,可能会遇到同一个事件之间两次的情况,为什么会出现这样的情况呢,下面具体来看看
1.现象复现
我们接着上篇文章Spring启动过程中Application事件的监听与处理,对于ApplicationEvent的监听:
@Component
public class ApplicationEventListener implements ApplicationListener<SpringApplicationEvent >{
private Logger log = Logger.getLogger(this.getClass());
@Override
public void onApplicationEvent(SpringApplicationEvent event) {
if(event instanceof ApplicationStartingEvent) {//启动之前
log.info("处理ApplicationStartingEvent");
}else if(event instanceof ApplicationReadyEvent ){//启动成功之后
}
}
运行之后,输出了两次"处理ApplicationStartingEvent"。说明来了两个ApplicationStartingEvent。
开启Debug模式,执行onApplicationEvent方法时,观察当前的ApplicationEventListener 对象,事件到来的顺序,对应的ApplicationEventListener地址:
1.ApplicationStartingEvent com.proudsmart.iot.listener.ApplicationEventListener@60dcc9fe
2.ApplicationStartingEvent com.proudsmart.iot.listener.ApplicationEventListener@443118b0
3.ApplicationEnvironmentPreparedEvent com.proudsmart.iot.listener.ApplicationEventListener@443118b0
4.ApplicationPreparedEvent com.proudsmart.iot.listener.ApplicationEventListener@443118b0
5.ApplicationStartedEvent com.proudsmart.iot.listener.ApplicationEventListener@443118b0
6.ApplicationReadyEvent com.proudsmart.iot.listener.ApplicationEventListener@443118b0
7.ApplicationEnvironmentPreparedEvent com.proudsmart.iot.listener.ApplicationEventListener@60dcc9fe
…
ApplicationReadyEvent 60dcc9fe com.proudsmart.iot.listener.ApplicationEventListener@60dcc9fe
观察下,ApplicationEventListener运行过程中生成了两个对象,一个是ApplicationEventListener@60dcc9fe,另一个是ApplicationEventListener@443118b0。
其中ApplicationEventListener@60dcc9fe先执行了ApplicationStartingEvent 事件,然后ApplicationEventListener@443118b0执行了Application整个生命周期的5个事件,接着ApplicationEventListener@60dcc9fe执行了剩下的4个事件。请记住这个执行顺序,第2节会分析。
一次是有启动参数的
第二次是没有启动参数的
2. 执行两次的原因
在没有答案之前,我从ApplicationEvent.getArgs()获得参数(启动时,我配置了–spring.profiles.active=discovery参数)。发现ApplicationEventListener@60dcc9fe也就是先创建的ApplicationEventListener对象,执行时能够获取到这个参数。后一个ApplicationEventListener获取参数为空。
基于参数和事件的两次执行顺序,我大胆猜想了下,第一个ApplicationEventListener是对应的root容器,第二个对应的可能是子容器。
然后上网查询了下,得到答案:
在web项目中如果同时集成了spring和springMVC的话,上下文中会存在两个容器,即spring的applicationContext.xml的父容器和springMVC的applicationContext-mvc.xml的子容器。这两个容器有相同的生命周期,所以同一个事件,在不同容器启动过程中都会发送一次。
如此,明白了多出的一次事件是引入了SpringMVC造成的。基于第一节中的时间执行顺序,我们可以得出这样的结论:
root容器启动开始–>创建子容器mvc并启动–>子容器mvc启动完成–>root容器继续启动–>root容器启动完成
3.解决方法
解决这个问题主要逻辑是,我们只关注root application context 的事件,处理它,而忽略mvc application context 的事件。那么问题变成了,如何区分是root Application context。
事件分为两类,每一类有不同的处理方法。
3.1 ApplicationEvent处理
ApplicationEvent提供的方法并不多,可能用到是getSpringApplication()
我们看一下SpringApplication的方法,测试了几次发现并没有能够区分是否为root的标识。。。如果有哪位同学知道请告诉我一声,感激不尽!!
所以我们只能走“旁门左道”,启动时加上启动参数,执行onApplicationEvent方法时,如果获取到的参数有值,则说明是root:
public void onApplicationEvent(SpringApplicationEvent event) {
String[] args = event.getArgs();
if(args == null || args.length ==0 ) {//MVC容器发出的事件不关注
return ;
}
if(event instanceof ApplicationStartingEvent) {//启动之前
log.info("处理ApplicationStartingEvent");
}else if(event instanceof ApplicationReadyEvent ){//启动成功之后
}
}
3.1 ApplicationContextEvent处理
ApplicationContextEvent 有getApplicationContext()可以获取到上下文(就是容器),如果容器的父容器为空,那么他就是Root容器。
@Override
public void onApplicationEvent(ApplicationContextEvent event) {
if(event.getApplicationContext().getParent() == null){ //root application context
//TODO
}
}