你好呀,我是歪歪。
前段时刻看到同事在项目里边运用了一个叫做 @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("歪歪"));
}
最后把服务发动起来,调用一次:
输出正常,完事儿,这个 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 这个小口子,把源码越撕越大了?
本文正在参与「金石方案」