Spring事件之注解@EventListener讲解

文章目录

  • 1 注解@EventListener
    • 1.1 示例Demo
      • 1.1.1 简单例子
      • 1.1.2 解耦
      • 1.1.3 Spring事件
    • 1.2 深入@EventListener
      • 1.2.1 debug调试
      • 1.2.2 问题一: Spring是怎么知道要去触发这个方法
      • 1.2.3 问题二:ApplicationListenerMethodAdapter
      • 1.2.4 问题三:SimpleApplicationEventMulticaster
      • 1.2.5 问题四:调试问题
      • 1.2.6 问题六:如何获取到自定义监听
      • 1.2.7 问题七:如何获取自定义事件
    • 1.3 进一步深究
      • 1.3.1 引入
      • 1.3.2 ApplicationListenerMethodAdapter
      • 1.3.3 与SpringBoot结合
    • 1.4 细节
      • 1.4.1 单线程执行事件
      • 1.4.2 线程池执行事件
      • 1.4.3 @EventListener注解参数

1 注解@EventListener

点击了解 Spring中的事件讲解(Application Event)

1.1 示例Demo

1.1.1 简单例子

假设现在的需求是用户注册成功之后给他发个短信,通知他一下。
正常来说,伪代码很简单:

boolean success = userRegister(user);
if(success){sendMsg("...........test.............");
}

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

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

1.1.2 解耦

应该是在用户注册成功之后,发布一个有用户注册成功了的事件:

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

然后有地方去监听这个事件,在监听事件的地方触发短信发送的动作。

这样的好处是后续假设不发短信了,要求发邮件,或者短信、邮件都要发送,诸如此类的需求变化,我们的用户注册流程的代码不需要进行任何变化,仅仅是在事件监听的地方搞事情就完事了。

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

1.1.3 Spring事件

我们可以基于 Spring 提供的 ApplicationListener 去做这个时间。
这次的 Demo 也非常的简单,我们首先需要一个对象来封装事件相关的信息,比如我这里用户注册成功,肯定要关心的是 userName:

@Data
public class RegisterSuccessEvent {private String userName;public RegisterSuccessEvent(String userName) {this.userName = userName;}
}

我这里只是为了做 Demo,对象很简单,实际使用过程中,你需要什么字段就放进去就行。

然后需要一个事件的监听逻辑:

@Slf4j
@Component
public class RegisterEventListener {@EventListenerpublic void handleNotifyEvent(RegisterSuccessEvent event) {log.info("监听到用户注册成功事件:" +"{},测试成功", event.getUserName());}}

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

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

1.2 深入@EventListener

1.2.1 debug调试

打断点首先选择打事件监听的这个地方:
图片
然后直接就是一个发起调用,拿到调用栈再说:
在这里插入图片描述

通过观察调用栈发现,全是 Springevent 包下的方法。
完全不知道应该怎么去看,所以我只有先看第一个涉及到 Spring 源码的地方,也就是这个反射调用的地方:

org.springframework.context.event.ApplicationListenerMethodAdapter#doInvoke

在这里插入图片描述
通过观察这三个关键的参数,我们可以断定此时确实是通过反射在调用我们 Demo 里面的 RegisterEventListener 类的 handleNotifyEvent 方法,入参是 RegisterSuccessEvent 对象,其 userName 字段的值是“歪歪”:
在这里插入图片描述

1.2.2 问题一: Spring是怎么知道要去触发这个方法

或者换个问法:handleNotifyEvent 这个自己写的方法名称怎么就出现在这里了呢?
然后顺着这个 method 找过去一看:
在这里插入图片描述
原来是当前类的一个字段,随便还看到了 beanName,也是其一个字段,对应着 DemoRegisterEventListener

1.2.3 问题二:ApplicationListenerMethodAdapter

既然关键字段都在当前类里面了,那么这个当前类,也就是 ApplicationListenerMethodAdapter 是什么时候冒出来的呢?
带着这个问题,继续往下查看调用栈,会看到这里的这个 listener 就是我们要找的这个“当前类”:
在这里插入图片描述

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

1.2.4 问题三:SimpleApplicationEventMulticaster

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

org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent

图片

为什么会在这个地方停下来呢?
因为在这个方法里面,就是整个调用链中 listener 第一次出现的地方。
所以,第二个断点的位置,我们也找到了,就是这个地方:

org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent

在这里插入图片描述

1.2.5 问题四:调试问题

点击了解 idea 断点调试技巧
但是,当然把断点打在这个地方,重启服务准备调试的时候,你会发现重启的过程中就会停在断点处,而停下来的时候,你去调试会发现根本就不是你所关心的逻辑。
全是 Spring 启动过程中触发的一些框架的监听逻辑。比如应用启动事件,就会在断点处停下:
在这里插入图片描述

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

  • 第一个是服务启动过程中,把断点停用,启动完成之后再次打开断点,然后触发调用。
    idea 也提供了这样的功能,这个图标就是全局的断点启用和停用的图标:
    这个方法在我们本次调试的过程中是行之有效的,但是假设如果以后你想要调试的代码,就是要在框架启动过程中调试的代码呢?
  • 使用条件断点。
    通过观察入参,我们可以看到 event 对象里面有个 payload 字段,里面放的就是我们 Demo 中的 RegisterSuccessEvent 对象:
    在这里插入图片描述

那么,我们可不可以打上断点,然后让 idea 识别到是上述情况的时候,即有 RegisterSuccessEvent 对象的时候,才在断点处停下来呢,当然是可以的,打条件断点就行。

在断点处右键,然后弹出框里面有个 Condition 输入框:
在这里插入图片描述
在这里,我们的条件是:event 对象里面的 payload 字段放的是我们 Demo 中的 RegisterSuccessEvent 对象时就停下来。
所以应该是这样的:event instanceof PayloadApplicationEvent && (((PayloadApplicationEvent) event).payload instanceof RegisterSuccessEvent)

1.2.6 问题六:如何获取到自定义监听

当我们观察 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 方法中,用 eventTypedeclaredEventTypes 做了一个对比,如果比上了,就说明当前的 listener 能处理这个 eventType

1.2.7 问题七:如何获取自定义事件

前面说了 eventTypeRegisterSuccessEvent 对象。那么这个 declaredEventTypes 是个啥玩意呢?
declaredEventTypes 字段也在之前就出现过的 ApplicationListenerMethodAdapter 类里面。supportsEventType 方法也是这个类的方法:
图片
而这个 declaredEventTypes,就是 RegisterSuccessEvent 对象:
图片
这不就呼应上了吗?
所以,这个 for 循环结束之后,里面一定是有 registerEventListener 的,因为它能处理当前的 RegisterSuccessEvent 这个事件。
图片
但是你会发现循环结束之后 list 里面有两个元素,突然冒出来个 DelegatingApplicationListener 是什么?

这个时候怎么办?别去研究它,它不会影响我们的程序运行,所以可以先做个简单的记录,不要分心,要抓住主要线路。

经过前面的一顿分析,我们现在又可以回到这里了。
通过 debug 我们知道这个时候我们拿到的就是我们自定义的 listener 了:
图片
从这个 listener 里面能拿到类名、方法名,从 event 中能拿到请求参数。
后续反射调用的过程,条件齐全,顺理成章的就完成了事件的发布。

1.3 进一步深究

1.3.1 引入

到这里,是不是认为已经调试的差不多了?已经知道了 Spring 自定义 listener 的大致工作原理了?
闭着眼睛想一想也就知道大概是一个什么流程了?
那么问一个问题:回想一下我最最开始定位到反射这个地方的时候是怎么说的?
图片
是不是给了你这一张图,说 beanName、method、declaredEventTypes 啥的都在 ApplicationListenerMethodAdapter 这个类里面,这些属性是什么时候设置到这个类里面的呢?

1.3.2 ApplicationListenerMethodAdapter

现在我们看一下 ApplicationListenerMethodAdapter 这个类是咋来的。
就是想看看 beanName 是啥时候和这个类扯上关系的嘛,很简单,刚刚才提到的条件断点又可以用起来了:
图片
重启之后,在启动的过程中就会在构造方法中停下,于是我们又有一个调用栈了:
图片
可以看到,在这个构造方法里面,就是在构建我们要寻找的 beanName、method、declaredEventTypes 这类字段。
而之所以会触发这个构造方法,是因为 Spring 容器在启动的过程中调用了下面这个方法:

org.springframework.context.event.EventListenerMethodProcessor#afterSingletonsInstantiated

图片

在这个方法里面,会去遍历 beanNames,然后在 processBean 方法里面找到带有 @EventListener 注解的 bean
图片
解释说明:

  • 在标号为 ① 地方找到这个 bean 具体是哪些方法标注了 @EventListener
  • 在标号为 ② 的地方去触发 ApplicationListenerMethodAdapter 类的构造方法,此时就可以把 beanName,代理目标类,代理方法通过参数传递过去。
  • 在标号为 ③ 的地方,将这个 listener 加入到 Spring 的上下文中,后续触发的时候直接从这里获取即可。

1.3.3 与SpringBoot结合

那么 afterSingletonsInstantiated 这个方法是什么时候触发的呢?还是看调用栈:
图片
即使再不熟悉 SpringBoot,至少也听说过容器启动过程中有一个 refresh 的动作吧?
就是这个地方:
图片
这里,refreshContext,就是整个 SpringBoot 框架启动过程的核心方法中的一步。
就是在这个方法里面中,在服务启动的过程中,ApplicationListenerMethodAdapter 这个类和一个 beanNameregisterEventListener 的类扯上了关系,为后续的事件发布的动作,埋好了伏笔。

1.4 细节

1.4.1 单线程执行事件

前面了解了关于 Spring 的事件发布机制主干代码的流程之后,相信已经能从容器启动时请求发起时这两个阶段进行了一个粗犷的说明了。
但是,里面其实还有很多细节需要注意的,比如事件发布是一个串行化的过程。假设某个事件监听逻辑处理时间很长,那么势必会导致其他的事件监听出现等待的情况。

比如有两个事件监听逻辑,在其中一个的处理逻辑中睡眠 3s,模拟业务处理时间。发起调用之后,从日志输出时间上可以看出来,确实是串行化,确实是出现了等待的情况:
在这里插入图片描述
针对这个问题,我们前面讲源码关于获取到 listener 之后,其实有这样的一个逻辑:
在这里插入图片描述
这不就是线程池异步的逻辑吗?只不过默认情况下是没有开启线程池的。

开始之后,日志就变成了这样:
在这里插入图片描述

1.4.2 线程池执行事件

@EventListener 注解默认是在发布事件的线程上同步执行监听器方法,即串行化执行。如果想在事件监听器方法中使用线程池来实现并发执行,可以通过以下方式进行配置:

创建一个线程池 Bean:

@Configuration
public class ThreadPoolConfig {@Beanpublic Executor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(10); // 设置核心线程数executor.setMaxPoolSize(20); // 设置最大线程数executor.setQueueCapacity(100); // 设置队列容量executor.setThreadNamePrefix("event-listener-"); // 设置线程名称前缀executor.initialize();return executor;}
}

@EventListener 注解中指定使用的线程池:

@EventListener()
@Async("taskExecutor") // 指定使用的线程池 Bean 名称
public void handleEvent(Event event) {// 处理事件逻辑,会在指定的线程池中并发执行
}

上述示例中,通过 @Async 注解指定了使用名为 taskExecutor 的线程池来执行监听器方法。

1.4.3 @EventListener注解参数

@EventListener 注解里面还有这两个参数,我们是没有使用到的:
在这里插入图片描述
@EventListener 注解有两个可选参数:classescondition

  • classes 参数:用于指定要监听的事件类型。可以指定一个或多个事件类型,以数组形式传递。例如:
@EventListener(classes = {EventA.class, EventB.class})
public void handleEvent(Event event) {// 处理事件逻辑
}

上述示例中,方法 handleEvent() 会监听 EventA 和 EventB 类型的事件。

  • condition 参数:用于指定一个 SpEL 表达式作为条件,只有当条件满足时,才会执行监听器方法。例如:
@EventListener(condition = "#event.source == 'source'")
public void handleEvent(Event event) {// 处理事件逻辑
}

上述示例中,方法 handleEvent() 只有当事件源为 source 时才会执行。
通过使用这两个参数,可以更加灵活地控制监听器的行为。可以根据具体需求选择要监听的事件类型,并根据条件来过滤需要处理的事件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/667309.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(12)喝汽水

文章目录 每日一言题目解题思路一代码 解题思路二代码 结语 每日一言 长风沛雨,艳阳明月。田野被喜悦铺满,天地间充满着生的豪情。 题目 已知1瓶汽水1元,2个空瓶可以换一瓶汽水,输入整数n(n>0)&#x…

Maven工程的配置及使用

一、Maven章节 Maven 是 Apache 软件基金会组织维护的一款专门为 Java 项目提供构建和依赖管理支持的工具 1.1、maven的作用 1)依赖管理: 方便快捷的管理项目依赖的资源包(jar包)避免版本冲突 2)统一项目结构&…

Android studio打开md无法显示md渲染问题

Where is Android Studio Markdown support plugin preview preference? - Stack Overflow android studio开发无法选择markdown渲染功能的问题 原因是java runtime出了问题 搜索下面功能 Choose Boot Java Runtime for the IDE 选择带JCEF的 可以选最新的java版本 重启之…

Jvm FullGC 如何排查?

使用场景 我们在使用系统时,有时请求和响应会变得特别慢,系统也变得很卡。 有可能是FullGC的问题,可以逐步地进行排查。 使用jps和top确定进程号pid jps可以列出正在运行的jvm进程,并显示jvm执行主类名称( main()函数所在的类…

浙政钉(专有钉钉)

专有钉钉是浙政钉的测试版本,可在正式发布之前进行业务开发。 专有钉钉 原名政务钉钉 是高安全、强管控、灵活开放的面向大型组织专有独享的协同办公平台。支持专有云、混合云等多种方式灵活部署,以满足客户特定场景所需为目标,最大化以“平…

window 镜像---负载篇

前提:需要修改window的powershell执行脚本的策略 步骤:以管理员身份打开powershell,执行 Get-ExecutionPolicy查看当前执行策略,若返回值是Restricted,需执行Set-ExecutionPolicy RemoteSigned powershell 版本信息&am…

gif格式图片是怎么做的?教你一招在线转换

常见的图片有jpg、png以及gif格式,其中,jpg和png格式的图片我们一般手机拍摄或是接受的都是这两种格式,但是gif格式的图片我们却需要从网上下载。那么,当我们想要自己制作gif动画的时候要怎么操作呢?只需要使用在线制作…

MAE实战:使用MAE提高主干网络的精度(一)

摘要 MAE已经出来有几年了,很多人还不知道怎么去使用,本文通过两个例子说明一下。分两部分,一部分介绍一个简单的例子,让大家了解MAE训练的流程。一部分是一个新的模型,让大家了解如何将自己的模型加入MAE。 论文标…

steam搬砖项目赚钱吗?操作流程看这一篇就够了

很多人应该听说过steam,它是国外一款知名的游戏社交平台,也是目前世界上最大的游戏平台之一。而steam搬砖项目,关键就是靠信息差。我们要做的就是在steam以低价买入道具装备,然后上架到网易buff卖出,赚取差价。 什么人…

【PLC一体机】PLC一体机中如何实现触摸屏和PC电脑的通讯

博主今天准备把之前买的PLC一体机拿出来玩一下,翻看以前的博文,发现没有记录分享PLC一体机中如何实现触摸屏程序下载的内容。 如之前博文介绍的那样,PLC一体机由PLC和触摸屏两部分集成的设备,因此设备内部已经做好了PLC和触摸屏之…

C++拷贝构造函数、赋值运算符重载

1.拷贝构造函数 拷贝构造函数的写法如图所示 调用方式如下 接下来我来说说它的特征 1.1特征 拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。 拷贝构造函…

XGB-2: Boosted Trees(提升树)简介

XGBoost代表“Extreme Gradient Boosting”,其中术语“Gradient Boosting”来源于Friedman的论文《Greedy Function Approximation: A Gradient Boosting Machine》。 梯度提升树已经存在一段时间,关于这个主题有很多资料。本部分将使用监督学习的元素&…

【Chrono Engine学习总结】1-安装配置与程序运行

本文仅用于个人安装记录。 官方安装教程 https://api.projectchrono.org/8.0.0/tutorial_install_chrono.html Windows下安装 windows下安装就按照教程好了。采用cmake-gui进行配置,建议首次安装只安装核心模块。然后依此configure下irrlicht,sensor…

6款超好用的IDEA插件,开发必备!

今天给大家介绍几款开发必备的IDEA插件: JRebel 热部署插件,让你在修改完代码后,不用再重新启动,很实用!但是,不是免费的,需要大家继续发挥下自己的聪明才智才能happy的使用 Json Parser 厌倦…

Abp 从空白的WebApplication中添加EntityFrameworkCore生成数据库

书接上文:Abp 从空白WebApplication开始 开发环境:.NET6、Volo.Abp 数据库:Sqlite 说明:纯属个人强行入门。我个人觉得按照官网的操作不舒服,所以自己研究着来,请读者根据自己的需要进行参考。我能保证的…

Ubuntu+GPU搭建Stable-Diffusion教程

【前序】已经安装anaconda 1.git拉取项目到本地 执行git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git 进入项目目录下 cd stable-diffusion-webui/ 2. 安装对应Python依赖包 首先安装pytorch和torchvision,若是GPU环境的用户需要安装与cu…

2024年最新幻兽帕鲁服务器搭建教程

玩转幻兽帕鲁服务器,阿里云推出新手0基础一键部署幻兽帕鲁服务器教程,傻瓜式一键部署,3分钟即可成功创建一台Palworld专属服务器,成本仅需26元,阿里云服务器网aliyunfuwuqi.com分享2024年新版基于阿里云搭建幻兽帕鲁服…

深度解析源码,Spring 如何使用三级缓存解决循环依赖

目录 一. 前言 二. 基础知识 2.1. 什么是循环依赖? 2.2. 三级缓存 2.3. 原理执行流程 三. 源码解读 3.1. 代码入口 3.2. 第一层 3.3. 第二层 3.4. 第三层 3.5. 返回第二层 3.6. 返回第一层 四. 原理深度解读 4.1. 什么要有三级缓存? 4.2.…

【HarmonyOS 4.0 应用开发实战】ArkTS 快速入门

个人名片: 🐼作者简介:一名大三在校生,喜欢AI编程🎋 🐻‍❄️个人主页🥇:落798. 🐼个人WeChat:hmmwx53 🕊️系列专栏:🖼️…

python中[[]] * (n)和[[] for _ in range(n)]的区别

1、现象 刷leetcode207的时候碰到一个坑,用[[]] * (n)初始化二维数组,逻辑是正确的,但是结果始终不对。 2、原因 最后定位是初始化语句使用错误导致的,我使用的是[[]] * (n),应该使用[[] for _ in range(n)] 3、解…