线上陈年老代码,Spring Event 一个事件每次都执行 2 次,为啥?

1、现象

@Component
public class RecomputeBaojieAmountListener implements ApplicationListener<RecomputeBaojieAmountEvent> {
    @EventListener
    @Override
    public void onApplicationEvent(RecomputeBaojieAmountEvent event) {
        System.out.println("事件执行了......");
    }
}
SpringBeanFactory.getContext().publishEvent(new RecomputeBaojieAmountEvent("TestApplication"));

Spring 发布的事件每次都会执行 2 次。

2、排查过程

2.1、是一次发布了 2 个事件?还是 1 个事件有 2 个监听者?

由于发布事件采用的是 getContext().publishEvent(),从源码看确实存在发布 2 次的逻辑。

org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType)
    
    protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
Assert.notNull(event, "Event must not be null");

// Decorate event as an ApplicationEvent if necessary
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent) event;
}
else {
applicationEvent = new PayloadApplicationEvent<>(this, event);
if (eventType == null) {
eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
}
}

// Multicast right now if possible - or lazily once the multicaster is initialized
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
}
else {
            // 当前 context 发布事件
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}

// Publish event via parent context as well...
    // 父 context 发布事件
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext) {
((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
}
else {
this.parent.publishEvent(event);
}
}
}

根据代码逻辑,只需查看当前 context 有没有 parent 就可以确认是否发布了 2 个事件;

ce1112312abbe5ab29b08d5cb5046999.png

debug 发现 parent 为 null,可以排除发布 2 个事件的可能了。

网上搜索到的文章,大多都是这种场景的解决方案。

2.2、2 个监听者都是谁?怎么来的?

既然上面排除了发布 2 个事件,那只能是有 2 个监听者了。

继续 debug 看监听者的相关信息,直接获取监听者,发现只能获取到同一个,和前面的猜测不同?

08c84d1f9248b78557c6c6ae68c9edac.png

ec2777d630a56b971bda2e5390a02574.png

097a082e7fa9eb3959752d9df3063c1c.png

继续往更深层次查看监听者,发现 context 中的 listener 集合中有 2 个和 RecomputeBaojieAmountEvent 相关的。

0553ff678d40bbba926b2a9d7f1a1fa1.png

相信对 Spring 比较熟悉的同学,看到 ApplicationListenerMethodAdapter 之后就知道问题的原因了。

由于下图中对 Listener 的用法错误,导致产生了 2 个 listener,事件就会执行 2 次。

02e19287fe0b513fb723121f386828a5.png

3、事件的原理

事件的原理其实就是搞明白事件的发布和处理的过程。

简单概括,一个事件发布后,会根据 eventType、sourceType 从所有 Listener 中找到处理这个事件的 Listener,执行事件逻辑。

3.1 怎么找到 listener 的?

private Collection<ApplicationListener<?>> retrieveApplicationListeners(
ResolvableType eventType, @Nullable Class<?> sourceType, @Nullable ListenerRetriever retriever) {

List<ApplicationListener<?>> allListeners = new ArrayList<>();
Set<ApplicationListener<?>> listeners;
Set<String> listenerBeans;
synchronized (this.retrievalMutex) {
listeners = new LinkedHashSet<>(this.defaultRetriever.applicationListeners);
listenerBeans = new LinkedHashSet<>(this.defaultRetriever.applicationListenerBeans);
}
    // 遍历 listeners,找到该事件的 listener
for (ApplicationListener<?> listener : listeners) {
if (supportsEvent(listener, eventType, sourceType)) {
if (retriever != null) {
retriever.applicationListeners.add(listener);
}
allListeners.add(listener);
}
}
    // 遍历 listenerBeans,找到该事件的 listener bean 对象
if (!listenerBeans.isEmpty()) {
BeanFactory beanFactory = getBeanFactory();
for (String listenerBeanName : listenerBeans) {
try {
Class<?> listenerType = beanFactory.getType(listenerBeanName);
if (listenerType == null || supportsEvent(listenerType, eventType)) {
ApplicationListener<?> listener =
beanFactory.getBean(listenerBeanName, ApplicationListener.class);
if (!allListeners.contains(listener) && supportsEvent(listener, eventType, sourceType)) {
if (retriever != null) {
if (beanFactory.isSingleton(listenerBeanName)) {
retriever.applicationListeners.add(listener);
}
else {
retriever.applicationListenerBeans.add(listenerBeanName);
}
}
allListeners.add(listener);
}
}
}
catch (NoSuchBeanDefinitionException ex) {
// Singleton listener instance (without backing bean definition) disappeared -
// probably in the middle of the destruction phase
}
}
}
AnnotationAwareOrderComparator.sort(allListeners);
if (retriever != null && retriever.applicationListenerBeans.isEmpty()) {
retriever.applicationListeners.clear();
retriever.applicationListeners.addAll(allListeners);
}
return allListeners;
}

从代码可以看出,Listener 被存放在 2 个地方,listeners 和 listenerBeans。

而 Listener 是在 Spring 启动完成后就已经初始化完成(创建好了bean,就等着事件发布后执行),所以这次问题的根本是搞清楚是在什么节点把 RecomputeBaojieAmountListener  放进 listeners 和 listenerBeans 的。

3.2 什么时候创建的 Listener ?

3.2.1、registerListeners

在 bean 创建的过程中,会走到这个方法

org.springframework.context.support.AbstractApplicationContext#registerListeners

870126978bfdca3234d5ba424b825952.png

这里会找到属于 ApplicationListener 的 listener,并把它们添加到 listeners 和 listenerBeans 中。

也就是通过 @Component 注解 和  实现 ApplicationListener 接口的方式创建的 RecomputeBaojieAmountListener。

3.2.2、EventListenerMethodProcessor

这个 Processor 是处理使用 @EventListener 注解定义的 Listener。

org.springframework.context.event.EventListenerMethodProcessor#processBean

6a9fa5a95fa48efa0bc0f2f845c95843.png

这里会创建一个新的 Listener,并添加到 listeners 中。

5ea5218c3ec1cd6f2c4fdb1ec0727c7f.png

但和 1 里面不同的是,创建的对象是 ApplicationListenerMethodAdapter,而不是我们业务定义的 Listener 的名字。这也是我们前面直接 getBean 只找到一个 Listener 的原因。

到此,前面的疑问就都解开了。

扯两句

炫技有风险,使用需谨慎。

原创不易,多多关注,一键三连,感谢支持!

  • 15
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值