你好呀,我是歪歪。

前段时刻看到同事在项目里边运用了一个叫做 @EventListener 的注解。

在这之前,我知道这个注解的用法和想要达到的目的,可是也仅限于此,其内部作业原理对我来说是一个黑盒,我完彻底全不知道它怎样就实现了“监听”的效果。

现在已然现已呈现在项目里边了,投入上生产上去运用了,所以我打算盘一下它,避免今后碰到问题的时分错失一个装逼的…

哦,不。

错失一个体现自己的机会。

扯下@EventListener这个注解的神秘面纱。

Demo

首要,按照歪歪歪师傅的老规矩,第一步啥也别说,先搞一个 Demo 出来,没有 Demo 的源码解读,就像是吃面的时分没有大蒜,差点意思。

先衬托一个布景吧。

假定现在的需求是用户注册成功之后给他发个短信,告诉他一下。

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

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

这代码能用,彻底没有任何问题。可是,你仔细想,发短信告诉这个动作按理来说,不应该和用户注册的行为“耦合”在一起,难道你短信发送的时分失利了,用户就不算注册成功吗?

上面的代码便是一个耦合性很强的代码。

怎样解耦呢?

应该是在用户注册成功之后,发布一个“有用户注册成功了”的作业:

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

然后有当地去监听这个作业,在监听作业的当地触发“短信发送”的动作。

这样的优点是后续假定不发短信了,要求发邮件,或许短信、邮件都要发送,诸如此类的需求改变,咱们的用户注册流程的代码不需求进行任何改变,仅仅是在作业监听的当地搞作业就完事了。

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

怎样做呢?

咱们能够根据 Spring 供给的 ApplicationListener 去做这个作业。

我的 Demo 里边用的 Spring 版本是 5.2.10。

这次的 Demo 也十分的简略,咱们首要需求一个方针来封装作业相关的信息,比方我这儿用户注册成功,肯定要关怀的是 userName:

@Data
publicclassRegisterSuccessEvent{
privateStringuserName;
publicRegisterSuccessEvent(StringuserName){
this.userName=userName;
}
}

我这儿仅仅为了做 Demo,方针很简略,实际运用进程中,你需求什么字段就放进去就行。

然后需求一个作业的监听逻辑:

@Slf4j
@Component
publicclassRegisterEventListener{
@EventListener
publicvoidhandleNotifyEvent(RegisterSuccessEventevent){
log.info("监听到用户注册成功作业:"+
"{},你注册成功了哦。记得来玩儿~",event.getUserName());
}
}

接着,经过 Http 接口来进行作业发布:

@Resource
privateApplicationContextapplicationContext;
@GetMapping("/publishEvent")
publicvoidpublishEvent(){
applicationContext.publishEvent(newRegisterSuccessEvent("歪歪"));
}

最后把服务发动起来,调用一次:

扯下@EventListener这个注解的神秘面纱。

输出正常,完事儿,这个 Demo 就算是搞定了,就只要十多行代码。

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

扯下@EventListener这个注解的神秘面纱。

Debug

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

这没啥好犹疑的,肯定是挑选打作业监听的这个当地:

扯下@EventListener这个注解的神秘面纱。

然后直接便是一个建议调用,拿到调用栈再说:

扯下@EventListener这个注解的神秘面纱。

经过调查调用栈发现,满是 Spring 的 event 包下的办法。

此刻,我还是一头雾水的,彻底不知道应该怎样去看,所以我只要先看第一个涉及到 Spring 源码的当地,也便是这个反射调用的当地:

org.springframework.context.event.ApplicationListenerMethodAdapter#doInvoke

扯下@EventListener这个注解的神秘面纱。

经过调查这三个要害的参数,咱们能够判定此刻的确是经过反射在调用咱们 Demo 里边的 RegisterEventListener 类的 handleNotifyEvent 办法,入参是 RegisterSuccessEvent 方针,其 userName 字段的值是“歪歪”:

扯下@EventListener这个注解的神秘面纱。

此刻,我的第一个问题就来了:Spring 是怎样知道要去触发我的这个办法的呢?

或许换个问法:handleNotifyEvent 这个我自己写的办法名称怎样就呈现在这儿了呢?

然后顺着这个 method 找过去一看:

扯下@EventListener这个注解的神秘面纱。

哦,原来是当时类的一个字段,随便还看到了 beanName,也是其一个字段,对应着 Demo 的 RegisterEventListener。

到这儿,第二个问题就随之而来了:已然要害字段都在当时类里边了,那么这个当时类,也便是 ApplicationListenerMethodAdapter 是什么时分冒出来的呢?

带着这个问题,持续往下检查调用栈,会看到这儿的这个 listener 便是咱们要找的这个“当时类”:

扯下@EventListener这个注解的神秘面纱。

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

然后你就会来到这个当地,把目光停在这个当地:

org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent

扯下@EventListener这个注解的神秘面纱。

为什么会在这个当地停下来呢?

因为在这个办法里边,便是整个调用链中 listener 第一次呈现的当地。

所以,第二个断点的位置,咱们也找到了,便是这个当地:

org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent

扯下@EventListener这个注解的神秘面纱。

可是,朋友们留意,我要可是了。

可是,当然把断点打在这个当地,重启服务准备调试的时分,你会发现重启的进程中就会停在断点处,而停下来的时分,你去调试会发现底子就不是你所关怀的逻辑。

满是 Spring 发动进程中触发的一些结构的监听逻辑。比方运用发动作业,就会在断点处停下:

扯下@EventListener这个注解的神秘面纱。

怎样办呢?

扯下@EventListener这个注解的神秘面纱。

针对这种情况,有两个办法。

第一个是服务发动进程中,把断点停用,发动完结之后再次翻开断点,然后触发调用。

idea 也供给了这样的功用,这个图标便是大局的断点启用和停用的图标:

扯下@EventListener这个注解的神秘面纱。

这个办法在咱们本次调试的进程中是卓有成效的,可是假定假如今后你想要调试的代码,便是要在结构发动进程中调试的代码呢?

所以,我更想教你第二种方案:运用条件断点。

经过调查入参,咱们能够看到 event 方针里边有个 payload 字段,里边放的便是咱们 Demo 中的 RegisterSuccessEvent 方针:

扯下@EventListener这个注解的神秘面纱。

那么,咱们可不能够打上断点,然后让 idea 识别到是上述情况的时分,即有 RegisterSuccessEvent 方针的时分,才在断点处停下来呢?

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

在断点处右键,然后弹出框里边有个 Condition 输入框:

扯下@EventListener这个注解的神秘面纱。

Condition,都知道吧,高考词汇,四级词汇了,抓紧时刻背一背:

扯下@EventListener这个注解的神秘面纱。

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

别的,关于 Condition 还有一个短语,叫做 in good condition。

反应过来大概是“情况良好”的意思。

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

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

便是说你头发情况不太好,需求留意一下。

扯下@EventListener这个注解的神秘面纱。

扯远了,说回条件断点。

在这儿,咱们的条件是:event 方针里边的 payload 字段放的是咱们 Demo 中的 RegisterSuccessEvent 方针时就停下来。

所以应该是这样的:

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

扯下@EventListener这个注解的神秘面纱。

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

然后,咱们再次建议调用,在断点处停下来了:

扯下@EventListener这个注解的神秘面纱。

主要重视 134 行的 listener 是怎样来的。

当咱们调查 getApplicationListeners 办法的时分,会发现这个办法它主要是在对 retrieverCache 这个缓存在搞作业。

扯下@EventListener这个注解的神秘面纱。

这个缓存里边放的便是在项目发动进程中现已触发过的结构自带的 listener 方针:

扯下@EventListener这个注解的神秘面纱。

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

因而要害逻辑就在 retrieveApplicationListeners 办法里边:

org.springframework.context.event.AbstractApplicationEventMulticaster#retrieveApplicationListeners

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

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

扯下@EventListener这个注解的神秘面纱。

这个 for 循环在干啥事呢?

便是循环当时所有的 listener,过滤出能处理当时这个作业的 listener。

能够看到当时一共有 20 个 listener,最后一个 listener 便是咱们自定义的 registerEventListener:

扯下@EventListener这个注解的神秘面纱。

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

supportsEvent(listener, eventType, sourceType)

这个办法,便是判断 listener 是否支持给定的作业:

扯下@EventListener这个注解的神秘面纱。

因为咱们知道当时的作业是咱们发布的 RegisterSuccessEvent 方针。

对应到源码中,这儿给定的作业,也便是 eventType 字段,对应的便是咱们的 RegisterSuccessEvent 方针。

扯下@EventListener这个注解的神秘面纱。

所以当循环到咱们的 registerEventListener 的时分,在 supportsEventType 办法中,用 eventType 和 declaredEventTypes 做了一个对比,假如比上了,就阐明当时的 listener 能处理这个 eventType。

前面说了 eventType 是 RegisterSuccessEvent 方针。

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

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

扯下@EventListener这个注解的神秘面纱。

而这个 declaredEventTypes,便是 RegisterSuccessEvent 方针:

扯下@EventListener这个注解的神秘面纱。

这不就照应上了吗?

所以,这个 for 循环完毕之后,里边一定是有 registerEventListener的,因为它能处理当时的 RegisterSuccessEvent 这个作业。

扯下@EventListener这个注解的神秘面纱。

可是你会发现循环完毕之后 list 里边有两个元素,突然冒出来个 DelegatingApplicationListener 是什么鬼?

这个时分怎样办?

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

经过前面的一顿分析,咱们现在又能够回到这儿了。

经过 debug 咱们知道这个时分咱们拿到的便是咱们自定义的 listener 了:

扯下@EventListener这个注解的神秘面纱。

从这个 listener 里边能拿到类名、办法名,从 event 中能拿到恳求参数。

后续反射调用的进程,条件齐全,水到渠成的就完结了作业的发布。

看到这儿,你细细回想一下,整个的调试进程,是不是一环扣一环。只需思路不乱,抓住骨干,问题不大。

进一步思考

到这儿,你是不是认为现已调试的差不多了?

自己现已知道了 Spring 自定义 listener 的大致作业原理了?

闭着眼睛想一想也就知道大概是一个什么流程了?

那么我问你一个问题:你回想一下我最最开端定位到反射这个当地的时分是怎样说的?

扯下@EventListener这个注解的神秘面纱。

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

请问:这些属性是什么时分设置到这个类里边的呢?

扯下@EventListener这个注解的神秘面纱。

这个…

如同…

是不是的确没讲?

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

可是假如我不主动提,你是不是也想不起来呢,所以我也彻底能够就写到这儿就完毕了。

我把这部分独自写一个小节便是提一下这个问题:假如你仅仅跟着网上的文章看,特别是源码解读或许方案设计类文章,仅仅看而不带着自己的思路,不自己亲身下手,其实很多问题你思考不全的,要害是看完今后你还会误以为你学全了。

扯下@EventListener这个注解的神秘面纱。

现在咱们看一下 ApplicationListenerMethodAdapter 这个类是咋来的。

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

扯下@EventListener这个注解的神秘面纱。

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

扯下@EventListener这个注解的神秘面纱。

能够看到,在这个结构办法里边,便是在构建咱们要寻找的 beanName、method、declaredEventTypes 这类字段。

而之所以会触发这个结构办法,是因为 Spring 容器在发动的进程中调用了下面这个办法:

org.springframework.context.event.EventListenerMethodProcessor#afterSingletonsInstantiated

扯下@EventListener这个注解的神秘面纱。

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

扯下@EventListener这个注解的神秘面纱。

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

在标号为 ② 的当地去触发 ApplicationListenerMethodAdapter 类的结构办法,此刻就能够把 beanName,署理方针类,署理办法经过参数传递过去。

在标号为 ③ 的当地,将这个 listener 加入到 Spring 的上下文中,后续触发的时分直接从这儿获取即可。

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

还是看调用栈:

扯下@EventListener这个注解的神秘面纱。

你即便再不了解 Spring,你至少也听说过容器发动进程中有一个 refresh 的动作吧?

便是这个当地:

扯下@EventListener这个注解的神秘面纱。

这儿,refreshContext,便是整个 SpringBoot 结构发动进程的中心办法中的一步。

便是在这个办法里边中,在服务发动的进程中,ApplicationListenerMethodAdapter 这个类和一个 beanName 为 registerEventListener 的类扯上了关系,为后续的作业发布的动作,埋好了伏笔。

细节

前面了解了关于 Spring 的作业发布机制骨干代码的流程之后,相信你现已能从“容器发动时”和“恳求建议时”这两个阶段进行了一个粗犷的阐明了。

可是,留意,我又要“可是”了。

里边其实还有很多细节需求留意的,比方作业发布是一个串行化的进程。假定某个作业监听逻辑处理时刻很长,那么势必会导致其他的作业监听呈现等候的情况。

比方我搞两个作业监听逻辑,在其中一个的处理逻辑中睡眠 3s,模拟业务处理时刻。建议调用之后,从日志输出时刻上能够看出来,的确是串行化,的确是呈现了等候的情况:

扯下@EventListener这个注解的神秘面纱。

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

扯下@EventListener这个注解的神秘面纱。

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

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

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

扯下@EventListener这个注解的神秘面纱。

那么怎样开启呢?

骨干流程都给你说了个大概了,这些分支细节,就自己去研讨吧。

再比方,@EventListener 注解里边还有这两个参数,咱们是没有运用到的:

扯下@EventListener这个注解的神秘面纱。

它应该怎样运用而且其到的效果是什么呢?

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

这也是归于分支细节的部分,自己去研讨吧

再再比方,前面讲到 ApplicationListenerMethodAdapter 这个类的时分:

扯下@EventListener这个注解的神秘面纱。

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

扯下@EventListener这个注解的神秘面纱。

这个儿子的名字里边带着个 “Transactional”,你就知道这是和业务相关的东西了。

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

扯下@EventListener这个注解的神秘面纱。

想都不用想,肯定是能够针对业务不同阶段进行作业监听。

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

再再再比方,前面说到了 Spring 容器在发动的进程中调用了下面这个办法:

org.springframework.context.event.EventListenerMethodProcessor#afterSingletonsInstantiated

扯下@EventListener这个注解的神秘面纱。

这个办法归于哪个类?

它归于 EventListenerMethodProcessor 这个类。

那么请问这个类是什么时分呈现在 Spring 容器里边的呢?

扯下@EventListener这个注解的神秘面纱。

这个…

如同…

是不是的确没讲?

是的,可是这个类在整个结构里边只要一次调用:

扯下@EventListener这个注解的神秘面纱。

调试起来那不是手拿把掐的作业?

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

扯下@EventListener这个注解的神秘面纱。

本文正在参与「金石方案」