关于后端:扯下EventListener这个注解的神秘面纱

6次阅读

共计 7146 个字符,预计需要花费 18 分钟才能阅读完成。

你好呀,我是歪歪。

前段时间看到共事在我的项目外面应用了一个叫做 @EventListener 的注解。

在这之前,我晓得这个注解的用法和想要达到的目标,然而也仅限于此,其外部工作原理对我来说是一个黑盒,我完完全全不晓得它怎么就实现了“监听”的成果。

当初既然曾经呈现在我的项目外面了,投入上生产下来应用了,所以我打算盘一下它,免得当前碰到问题的时候错过一个装逼的 …

哦,不。

错过一个体现本人的机会。

Demo

首先,依照歪歪歪徒弟的老规矩,第一步啥也别说,先搞一个 Demo 进去,没有 Demo 的源码解读,就像是吃面的时候没有大蒜,差点意思。

先铺垫一个背景吧。

假如当初的需要是用户注册胜利之后给他发个短信,告诉他一下。

失常来说,伪代码很简略:

boolean success = userRegister(user);
if(success){sendMsg("客官,你注册胜利了哦。记得来玩儿~");
}

这代码能用,齐全没有任何问题。然而,你认真想,发短信告诉这个动作按理来说,不应该和用户注册的行为“耦合”在一起,难道你短信发送的时候失败了,用户就不算注册胜利吗?

下面的代码就是一个耦合性很强的代码。

怎么解耦呢?

应该是在用户注册胜利之后,公布一个“有用户注册胜利了”的事件:

boolean success = userRegister(user);
if(success){publicRegisterSuccessEvent(user);
}

而后有中央去监听这个事件,在监听事件的中央触发“短信发送”的动作。

这样的益处是后续假如不发短信了,要求发邮件,或者短信、邮件都要发送,诸如此类的需要变动,咱们的用户注册流程的代码不须要进行任何变动,仅仅是在事件监听的中央搞事件就完事了。

这样就算是实现了两个动作的“解耦”。

怎么做呢?

咱们能够基于 Spring 提供的 ApplicationListener 去做这个事件。

我的 Demo 外面用的 Spring 版本是 5.2.10。

这次的 Demo 也十分的简略,咱们首先须要一个对象来封装事件相干的信息,比方我这里用户注册胜利,必定要关怀的是 userName:

@Data
public class RegisterSuccessEvent {

    private String userName;

    public RegisterSuccessEvent(String userName) {this.userName = userName;}
}

我这里只是为了做 Demo,对象很简略,理论应用过程中,你须要什么字段就放进去就行。

而后须要一个事件的监听逻辑:

@Slf4j
@Component
public class RegisterEventListener {

    @EventListener
    public void handleNotifyEvent(RegisterSuccessEvent event) {
        log.info("监听到用户注册胜利事件:" +
                "{},你注册胜利了哦。记得来玩儿~", event.getUserName());
    }

}

接着,通过 Http 接口来进行事件公布:

@Resource
private ApplicationContext applicationContext;
@GetMapping("/publishEvent")
public void publishEvent() {applicationContext.publishEvent(new RegisterSuccessEvent("歪歪"));
}

最初把服务启动起来,调用一次:

输入失常,完事儿,这个 Demo 就算是搞定了,就只有十多行代码。

这么简略的 Demo 你都不想亲自动手去搭一个的话,想要靠肉眼学习的话,那么我只能说:

Debug

来,我问你,如果是你的话,就这几行代码,第一个断点你会打在哪里?

这没啥好犹豫的,必定是抉择打事件监听的这个中央:

而后间接就是一个发动调用,拿到调用栈再说:

通过观察调用栈发现,全是 Spring 的 event 包下的办法。

此时,我还是一头雾水的,齐全不晓得应该怎么去看,所以我只有先看第一个波及到 Spring 源码的中央,也就是这个反射调用的中央:

org.springframework.context.event.ApplicationListenerMethodAdapter#doInvoke

通过观察这三个要害的参数,咱们能够判定此时的确是通过反射在调用咱们 Demo 外面的 RegisterEventListener 类的 handleNotifyEvent 办法,入参是 RegisterSuccessEvent 对象,其 userName 字段的值是“歪歪”:

此时,我的第一个问题就来了:Spring 是怎么晓得要去触发我的这个办法的呢?

或者换个问法:handleNotifyEvent 这个我本人写的办法名称怎么就呈现在这里了呢?

而后顺着这个 method 找过来一看:

哦,原来是以后类的一个字段,轻易还看到了 beanName,也是其一个字段,对应着 Demo 的 RegisterEventListener。

到这里,第二个问题就随之而来了:既然关键字段都在以后类外面了,那么这个以后类,也就是 ApplicationListenerMethodAdapter 是什么时候冒出来的呢?

带着这个问题,持续往下查看调用栈,会看到这里的这个 listener 就是咱们要找的这个“以后类”:

所以,咱们的问题就变成了,这个 listener 是怎么来的?

而后你就会来到这个中央,把眼光停在这个中央:

org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent

为什么会在这个中央停下来呢?

因为在这个办法外面,就是整个调用链中 listener 第一次呈现的中央。

所以,第二个断点的地位,咱们也找到了,就是这个中央:

org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent

然而,敌人们留神,我要然而了。

然而,当然把断点打在这个中央,重启服务筹备调试的时候,你会发现重启的过程中就会停在断点处,而停下来的时候,你去调试会发现基本就不是你所关怀的逻辑。

全是 Spring 启动过程中触发的一些框架的监听逻辑。比方利用启动事件,就会在断点处停下:

怎么办呢?

针对这种状况,有两个方法。

第一个是服务启动过程中,把断点停用,启动实现之后再次关上断点,而后触发调用。

idea 也提供了这样的性能,这个图标就是全局的断点启用和停用的图标:

这个办法在咱们本次调试的过程中是卓有成效的,然而假如如果当前你想要调试的代码,就是要在框架启动过程中调试的代码呢?

所以,我更想教你第二种计划:应用条件断点。

通过观察入参,咱们能够看到 event 对象外面有个 payload 字段,外面放的就是咱们 Demo 中的 RegisterSuccessEvent 对象:

那么,咱们可不可以打上断点,而后让 idea 辨认到是上述情况的时候,即有 RegisterSuccessEvent 对象的时候,才在断点处停下来呢?

当然是能够的,打条件断点就行。

在断点处右键,而后弹出框外面有个 Condition 输入框:

Condition,都意识吧,高考词汇,四级词汇了,抓紧时间背一背:

在 idea 的断点这里,它是“条件”的意思,带着个输入框,代表让你输出条件的意思。

另外,对于 Condition 还有一个短语,叫做 in good condition。

反馈过去大略是“状况良好”的意思。

比方:我已出仓,in good condition。

再比方:Your hair is not in good condition。

就是说你头发情况不太好,须要留神一下。

扯远了,说回条件断点。

在这里,咱们的条件是:event 对象外面的 payload 字段放的是咱们 Demo 中的 RegisterSuccessEvent 对象时就停下来。

所以应该是这样的:

event instanceof PayloadApplicationEvent && (((PayloadApplicationEvent) event).payload instanceof RegisterSuccessEvent)

当咱们这样设置实现之后,重启我的项目,你会发现重启过程十分丝滑,并没有在断点处停下来,阐明咱们的条件断点起作用了。

而后,咱们再次发动调用,在断点处停下来了:

次要关注 134 行的 listener 是怎么来的。

当咱们察看 getApplicationListeners 办法的时候,会发现这个办法它次要是在对 retrieverCache 这个缓存在搞事件。

这个缓存外面放的就是在我的项目启动过程中曾经触发过的框架自带的 listener 对象:

调用的时候,如果能从缓存中拿到对应的 listener,则间接返回。而咱们 Demo 中的自定义 listener 是第一次触发,所以必定是没有的。

因而要害逻辑就在 retrieveApplicationListeners 办法外面:

org.springframework.context.event.AbstractApplicationEventMulticaster#retrieveApplicationListeners

这个办法外面的逻辑较多,我不会逐行解析。

只说一下这个要害的 for 循环:

这个 for 循环在干啥事呢?

就是循环以后所有的 listener,过滤出能解决以后这个事件的 listener。

能够看到以后一共有 20 个 listener,最初一个 listener 就是咱们自定义的 registerEventListener:

每一个 listener 都通过一次 supportsEvent 办法判断:

supportsEvent(listener, eventType, sourceType)

这个办法,就是判断 listener 是否反对给定的事件:

因为咱们晓得以后的事件是咱们公布的 RegisterSuccessEvent 对象。

对应到源码中,这里给定的事件,也就是 eventType 字段,对应的就是咱们的 RegisterSuccessEvent 对象。

所以当循环到咱们的 registerEventListener 的时候,在 supportsEventType 办法中,用 eventType 和 declaredEventTypes 做了一个比照,如果比上了,就阐明以后的 listener 能解决这个 eventType。

后面说了 eventType 是 RegisterSuccessEvent 对象。

那么这个 declaredEventTypes 是个啥玩意呢?

declaredEventTypes 字段也在之前就呈现过的 ApplicationListenerMethodAdapter 类外面。supportsEventType 办法也是这个类的办法:

而这个 declaredEventTypes,就是 RegisterSuccessEvent 对象:

这不就响应上了吗?

所以,这个 for 循环完结之后,外面肯定是有 registerEventListener 的,因为它能解决以后的 RegisterSuccessEvent 这个事件。

然而你会发现循环完结之后 list 外面有两个元素,忽然冒出来个 DelegatingApplicationListener 是什么鬼?

这个时候怎么办?

别去钻研它,它不会影响咱们的程序运行,所以能够先做个简略的记录,不要分心,要抓住主要矛盾。

通过后面的一顿剖析,咱们当初又能够回到这里了。

通过 debug 咱们晓得这个时候咱们拿到的就是咱们自定义的 listener 了:

从这个 listener 外面能拿到类名、办法名,从 event 中能拿到申请参数。

后续反射调用的过程,条件齐全,牵强附会的就实现了事件的公布。

看到这里,你细细回忆一下,整个的调试过程,是不是一环扣一环。只有思路不乱,抓住骨干,问题不大。

进一步思考

到这里,你是不是认为曾经调试的差不多了?

本人曾经晓得了 Spring 自定义 listener 的大抵工作原理了?

闭着眼睛想一想也就晓得大略是一个什么流程了?

那么我问你一个问题:你回忆一下我最最开始定位到反射这个中央的时候是怎么说的?

是不是给了你这一张图,说 beanName、method、declaredEventTypes 啥的都在 ApplicationListenerMethodAdapter 这个类外面?

请问:这些属性是什么时候设置到这个类外面的呢?

这个 …

如同 …

是不是的确没讲?

是的,所以说这部分我也得给你补上。

然而如果我不被动提,你是不是也想不起来呢,所以我也齐全能够就写到这里就完结了。

我把这部分独自写一个大节就是提一下这个问题:如果你只是跟着网上的文章看,特地是源码解读或者方案设计类文章,只是看而不带着本人的思路,不本人亲自下手,其实很多问题你思考不全的,要害是看完当前你还会误以为你学全了。

当初咱们看一下 ApplicationListenerMethodAdapter 这个类是咋来的。

咱们不就是想看看 beanName 是啥时候和这个类扯上关系的嘛,很简略,刚刚才提到的条件断点又能够用起来了:

重启之后,在启动的过程中就会在构造方法中停下,于是咱们又有一个调用栈了:

能够看到,在这个构造方法外面,就是在构建咱们要寻找的 beanName、method、declaredEventTypes 这类字段。

而之所以会触发这个构造方法,是因为 Spring 容器在启动的过程中调用了上面这个办法:

org.springframework.context.event.EventListenerMethodProcessor#afterSingletonsInstantiated

在这个办法外面,会去遍历 beanNames,而后在 processBean 办法外面找到带有 @EventListener 注解的 bean:

在标号为 ① 中央找到这个 bean 具体是哪些办法标注了 @EventListener。

在标号为 ② 的中央去触发 ApplicationListenerMethodAdapter 类的构造方法,此时就能够把 beanName,代理指标类,代理办法通过参数传递过来。

在标号为 ③ 的中央,将这个 listener 退出到 Spring 的上下文中,后续触发的时候间接从这里获取即可。

那么 afterSingletonsInstantiated 这个办法是什么时候触发的呢?

还是看调用栈:

你即便再不相熟 Spring,你至多也据说过容器启动过程中有一个 refresh 的动作吧?

就是这个中央:

这里,refreshContext,就是整个 SpringBoot 框架启动过程的外围办法中的一步。

就是在这个办法外面中,在服务启动的过程中,ApplicationListenerMethodAdapter 这个类和一个 beanName 为 registerEventListener 的类扯上了关系,为后续的事件公布的动作,埋好了伏笔。

细节

后面理解了对于 Spring 的事件公布机制骨干代码的流程之后,置信你曾经能从“容器启动时”和“申请发动时”这两个阶段进行了一个粗暴的阐明了。

然而,留神,我又要“然而”了。

外面其实还有很多细节须要留神的,比方事件公布是一个串行化的过程。假如某个事件监听逻辑解决工夫很长,那么势必会导致其余的事件监听呈现期待的状况。

比方我搞两个事件监听逻辑,在其中一个的解决逻辑中睡眠 3s,模仿业务解决工夫。发动调用之后,从日志输入工夫上能够看进去,的确是串行化,的确是呈现了期待的状况:

针对这个问题,咱们后面讲源码对于获取到 listener 之后,其实有这样的一个逻辑:

这不就是线程池异步的逻辑吗?

只不过默认状况下是没有开启线程池的。

开始之后,日志就变成了这样:

那么怎么开启呢?

骨干流程都给你说了个大略了,这些分支细节,就本人去钻研吧。

再比方,@EventListener 注解外面还有这两个参数,咱们是没有应用到的:

它应该怎么应用并且其到的作用是什么呢?

对应的源码是哪个局部呢?

这也是属于分支细节的局部,本人去钻研吧

再再比方,后面讲到 ApplicationListenerMethodAdapter 这个类的时候:

你会发现它还有一个子类,点过来一看,它有一个叫做 ApplicationListenerMethodTransactionalAdapter 的儿子:

这个儿子的名字外面带着个“Transactional”,你就晓得这是和事务相干的货色了。

它外面有个叫做 TransactionalEventListener 的字段,它也是一个注解,外面对应着事务的多个不同阶段:

想都不必想,必定是能够针对事务不同阶段进行事件监听。

这部分“儿子”的逻辑,是不是也能够去钻研钻研。

再再再比方,后面提到了 Spring 容器在启动的过程中调用了上面这个办法:

org.springframework.context.event.EventListenerMethodProcessor#afterSingletonsInstantiated

这个办法属于哪个类?

它属于 EventListenerMethodProcessor 这个类。

那么请问这个类是什么时候呈现在 Spring 容器外面的呢?

这个 …

如同 …

是不是的确没讲?

是的,然而这个类在整个框架外面只有一次调用:

调试起来那不是手拿把掐的事件?

也能够去钻研钻研嘛,看着看着,不就缓缓的从 @EventLintener 这个小口子,把源码越撕越大了?

正文完
 0