扯下@EventListener这个注解的神秘面纱。
你好呀,我是歪歪。
前段时间看到同事在项目里面使用了一个叫做 @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 这个小口子,把源码越撕越大了?
扯下@EventListener这个注解的神秘面纱。的更多相关文章
- 揭开GrowingIO无埋点的神秘面纱
揭开GrowingIO无埋点的神秘面纱 早在研究用户行为分析的时候,就发现国内的GrowingIO在宣传无埋点技术,最近正好抽出时间来研究一下所谓的无埋点到底是什么样的. 我分六部分来分析一下无埋 ...
- 揭开Sass和Compass的神秘面纱
揭开Sass和Compass的神秘面纱 可能之前你像我一样,对Sass和Compass毫无所知,好一点儿的可能知道它们是用来作为CSS预处理的.那么,今天请跟我一起学习下Sass和Compass的一些 ...
- ASP.NET 运行时详解 揭开请求过程神秘面纱
对于ASP.NET开发,排在前五的话题离不开请求生命周期.像什么Cache.身份认证.Role管理.Routing映射,微软到底在请求过程中干了哪些隐秘的事,现在是时候揭晓了.抛开乌云见晴天,接下来就 ...
- 带你揭开ATM的神秘面纱
相信大家都用过ATM取过money吧,但是有多少人真正是了解ATM的呢?相信除了ATM从业者外了解的人寥寥无几吧,鄙人作为一个从事ATM软件开发的伪专业人士就站在我的角度为大家揭开ATM的神秘面纱吧. ...
- 揭开.NET消息循环的神秘面纱(GetMessage()无法取得任何消息,就会进入Idle(空闲)状态,进入睡眠状态(而不是Busy Waiting)。当消息队列不再为空的时候,程序会自动醒过来)
揭开.NET消息循环的神秘面纱(-) http://hi.baidu.com/sakiwer/item/f17dc33274a04df2a9842866 曾经在Win32平台下奋战的程序员们想必记得, ...
- 揭开Docker的神秘面纱
Docker 相信在飞速发展的今天已经越来越火,它已成为如今各大企业都争相使用的技术.那么Docker 是什么呢?为什么这么多人开始使用Docker? 本节课我们将一起解开Docker的神秘面纱. 本 ...
- 揭开Redis的神秘面纱
本篇博文将为你解开Redis的神秘面纱,通过阅读本篇博文你将了解到以下内容: 什么是Redis? 为什么选择 Redis? 什么场景下用Redis? Redis 支持哪些语言? Redis下载 Red ...
- java高级精讲之高并发抢红包~揭开Redis分布式集群与Lua神秘面纱
java高级精讲之高并发抢红包~揭开Redis分布式集群与Lua神秘面纱 redis数据库 Redis企业集群高级应用精品教程[图灵学院] Redis权威指南 利用redis + lua解决抢红包高并 ...
- 解开lambda最强作用的神秘面纱
我们期待了很久lambda为java带来闭包的概念,但是如果我们不在集合中使用它的话,就损失了很大价值.现有接口迁移成为lambda风格的问题已经通过default methods解决了,在这篇文章将 ...
- 【转】再讲IQueryable<T>,揭开表达式树的神秘面纱
[转]再讲IQueryable<T>,揭开表达式树的神秘面纱 接上篇<先说IEnumerable,我们每天用的foreach你真的懂它吗?> 最近园子里定制自己的orm那是一个 ...
随机推荐
- 关闭Google自动更新
一.禁用任务计划 二.禁用更新服务 三.重命名更新程序 首先找到谷歌浏览器的安装位置
- 01. JavaScript基础知识
一.JavaScript简介 JavaScript 是一门解释型编程语言,解释型编程语言指代码不需要手动编译,而是通过解释器边解释边执行.所以,要运行 JS,我们需要在计算机中安装 JS 的解释器 ...
- JAVA 、Http协议:
JAVA如何配置服务器: Http协议: 1.什么是Http协议 HTTP,超文本传输协议(HyperText Transfer Protocol)是互联网上应用最为广泛的 一种网络协议.所有的W ...
- 盒模型属性-width height-padding-border-margin
宽度 width: 作用:设置可以添加元素内容的区域的宽度. 属性值: 特殊应用: • 如果一个元素不添加width 属性,默认属性值为auto,不同的元素浏览器会根据其 特点自动计算出实际宽度,例 ...
- linux sync命令
Linux sync命令用于数据同步,sync命令是在关闭Linux系统时使用的. Linux 系统中欲写入硬盘的资料有的时候为了效率起见,会写到 filesystem buffer 中,这个 buf ...
- 【Win11】Win11家庭版升级专业版
1.将已激活的Win11家庭版升级成未激活的Win11专业版:输入密钥[BCQNW-3VWYB-4V7QD-M6R2B-7MH26] 2.将未激活的Win11专业版升级成已激活的专业版 :输入密钥[6 ...
- 过滤器函数 filtes 的使用总结
// import parseTime, formatTime and set to filter /** * Show plural label if time is plural number * ...
- 用requests库的get( )函数访问搜狗网站20次
代码: import requests from bs4 import BeautifulSoup def getHTMLText(url): try: r=requests.get(url,time ...
- Surge 开启脚本功能后,京东自动签到总结
本人小白,花费半天时间才弄好,写下这个给自己和后来人指路. Surge 开启脚本功能后,京东自动签到获取京豆总结: 1.注意 去 Raw 真实地址下载 js https://github.com/No ...
- Jmeter读取Csv文件,字段中有逗号分隔,读取不成功
Jmeter读取Csv文件,字段中有逗号分隔,读取不成功