Spring事件之注解@EventListener讲解

news2024/9/22 11:26:51

文章目录

  • 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 {

    @EventListener
    public 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 {
    @Bean
    public 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.coloradmin.cn/o/1431186.html

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

相关文章

(Python)列表字典数据本地存储工具

一个简单的实现简便 "列表字典" 数据存储本地。 适合不会SQL但又想实现数据存储本地的同学。 操作使用都非常简单。 文件只做了简单的加密处理,如果需要复杂加密的同学可以修改加密函数。 感兴趣并且动手能力强的同学,可以把它封装成工具类…

Linux Centos stream9 mdadm

RAID(Redundant Array of Independent Disk独立冗余磁盘阵列)技术是加州大学伯克利分校1987年提出,最初是为了组合小的廉价磁盘来代替大的昂贵磁盘,同时希望磁盘失效时不会使对数据的访问受损失而开发出一定水平的数据保护技术。RAID就是一种由多块廉价磁…

Fink CDC数据同步(二)MySQL数据同步

1 开启binlog日志 2 数据准备 use bigdata; drop table if exists user;CREATE TABLE user(id INTEGER NOT NULL AUTO_INCREMENT,name VARCHAR(20) NOT NULL DEFAULT ,birth VARCHAR(20) NOT NULL DEFAULT ,gender VARCHAR(10) NOT NULL DEFAULT ,PRIMARY KEY(id) ); ALTER TA…

vite打包原理

vite 工程化开发:打包工具 启动速度很快 核心原理还是webpack 把webpack封装了,把webpack对象封装了 和vue2整体结构几乎一致 webpack两种模式:开发&生产 代码打包编译,本地起一个web服务器实时预览编译后的结果 build 命令模…

Spark 开启动态资源分配

一 为什么要开启动态资源分配 ⽤户提交Spark应⽤到Yarn上时,可以通过spark-submit的num-executors参数显示地指定executor个数,随后, ApplicationMaster会为这些executor申请资源,每个executor作为⼀个Container在Yarn上运⾏。 S…

【数据结构】并查集(路径压缩)

文章目录 并查集1.朴素版本2.路径压缩3.按秩合并4.启发式合并5.练习题 并查集 1.朴素版本 1. 并查集解决的是连通块的问题,常见操作有,判断两个元素是否在同一个连通块当中,两个非同一连通块的元素合并到一个连通块当中。 并查集和堆的结构…

单臂路由实验(华为)

思科设备参考&#xff1a; 单臂路由实验&#xff08;思科&#xff09; 一&#xff0c;实验目的 在路由器的一个接口上通过配置子接口的方式&#xff0c;实现相互隔离的不同vlan之间互通。 ​ 二&#xff0c;设备配置 Switch1 <Huawei>sys [Huawei]vlan batch 10 20…

JUC并发工具类的应用场景详解

目录 常用并发同步工具类的真实应用场景 1. ReentrantLock 1.1 常用API 1.2 ReentrantLock使用 独占锁&#xff1a;模拟抢票场景 公平锁和非公平锁 可重入锁 结合Condition实现生产者消费者模式 1.3 应用场景总结 2. Semaphore 2.1 常用API 2.2 Semaphore使…

Http请求Cookie失效问题

Http请求Cookie失效问题记录 一、问题现象 在开发功能的过程中&#xff0c;业务依赖cookie进行取之&#xff0c;项目进行交互时会对前端http请求携带的cookies进行解析操作&#xff0c;但在自测调试对过程中出现账户的授权失效的报错问题。 二、问题排查 用arthas进行代码方…

Hadoop3.x基础(3)- Yarn

来源&#xff1a;B站尚硅谷 目录 Yarn资源调度器Yarn基础架构Yarn工作机制作业提交全过程Yarn调度器和调度算法先进先出调度器&#xff08;FIFO&#xff09;容量调度器&#xff08;Capacity Scheduler&#xff09;公平调度器&#xff08;Fair Scheduler&#xff09; Yarn常用命…

蓝桥杯每日一题-----数位dp

前言 今天浅谈一下数位dp的板子&#xff0c;我最初接触到数位dp的时候&#xff0c;感觉数位dp老难了&#xff0c;一直不敢写&#xff0c;最近重新看了一些数位dp&#xff0c;发现没有想象中那么难&#xff0c;把板子搞会了&#xff0c;变通也会变的灵活的多&#xff01; 引入…

png图片怎么转换成jpg?四个方法搞定不求人

在数字图像处理领域&#xff0c;PNG和JPG是两种常见的图片格式。PNG以无损压缩而闻名&#xff0c;适用于保存透明背景和保留图像细节&#xff1b;而JPG以有损压缩而著称&#xff0c;适用于在较小的文件大小下保持照片质量。有时候&#xff0c;您可能需要将PNG格式的图片转换为J…

QXlsx Qt操作excel

QXlsx 是一个用于处理Excel文件的开源C库。它允许你在你的C应用程序中读取和写入Microsoft Excel文件&#xff08;.xlsx格式&#xff09;。该库支持多种操作&#xff0c;包括创建新的工作簿、读取和写入单元格数据、格式化单元格、以及其他与Excel文件相关的功能。 支持跨平台…

【git指南】git 远程代码控制

文章目录 git 远程代码控制1 创建远程仓库2 创建SSH通道3 上传文件到github4 从 github 下载项目5 对链接github的本地项目pull/push git 远程代码控制 ​ 上面介绍了 git 在本地的使用&#xff0c;下面介绍结合 github 的 git 的远程控制。 1 创建远程仓库 打开 github.com …

【lesson9】高并发内存池Page Cache层释放内存的实现

文章目录 Page Cache层释放内存的流程Page Cache层释放内存的实现 Page Cache层释放内存的流程 如果central cache释放回一个span&#xff0c;则依次寻找span的前后page id的没有在使用的空闲span&#xff0c;看是否可以合并&#xff0c;如果合并继续向前寻找。这样就可以将切…

备战蓝桥杯---搜索(应用基础1)

话不多说&#xff0c;直接看题&#xff1a; 显然&#xff0c;我们直接用深搜&#xff0c;我们可以先把空位用结构体存&#xff0c;然后打表存小方块&#xff0c;再用数组存行列。 下面是AC代码&#xff1a; #include<bits/stdc.h> using namespace std; int a[12][12];…

【Springcloud篇】学习笔记六(十一、十二章):Config分布式配置中心、Bus消息总线

第十一章_Config分布式配置中心 1.Config分布式配置中心介绍 1.1分布式系统面临的配置问题 微服务意味着要将单体应用中的业务拆分成一个个子服务&#xff0c;每个服务的粒度相对较小&#xff0c;因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行&…

图论练习2

内容&#xff1a;路径计数DP&#xff0c;差分约束 最短路计数 题目大意 给一个个点条边的无向无权图&#xff0c;问从出发到其他每个点的最短路有多少条有自环和重边&#xff0c;对答案 解题思路 设边权为1&#xff0c;跑最短路 表示的路径数自环和重边不影…

Blender教程(基础)-面的切割-10

快捷键K切割&#xff0c;菜单选项切割. 一、随意切割 物体在编辑模式下&#xff0c;按键盘K建切割物体。 二、中点切割 先按K键&#xff0c;再按shift键&#xff0c;会自动吸附到每条边的中点进行切割。 三、取消吸附 切割时会自动吸附到顶点或边 关闭快速吸附 按K键再按…

山东淄博刑侦大队利用无人机抓获盗窃团伙

山东淄博刑侦大队利用无人机抓获盗窃团伙 近期&#xff0c;山东淄博临淄区发生多起盗窃案件。通过视频追踪和调查访问&#xff0c;推断临淄区某村可能为嫌疑人藏匿地点。刑侦大队无人机应急小组迅速到达现场&#xff0c;经无人机高空侦查&#xff0c;发现并锁定了嫌疑人的藏匿…