Spring事件注解@EventListener【观察】

一、背景

在开发工作中,我们常常会遇到这样一种情况:完成一项任务后,需要向其他模块广播消息或通知,以触发其他事件的处理。逐个发送请求固然可行,但更好的方式是采用事件监听,它是设计模式中的发布-订阅模式和观察者模式的一种实现。

观察者模式简单来说就是一种角色扮演,你在做某件事时有人在一旁观察你。当这个人观察到你有兴趣的特定事件发生时,他们会根据这个事件做一些其他的事情。但请注意,任何想要观察你的人都必须先到你这儿进行登记,否则你将无法通知到他们,或者他们没有资格来观察你。

在Spring容器中,我们可以监听并触发各种事件。通常有两种方法可以实现这一目标:使用ApplicationListener接口和使用@EventListener注解。这两种方法都能帮助我们更好地管理和响应应用程序中的各种事件。

二、简介

2.1 用途

为了实现一个能够监听应用程序事件的监听器方法,我们可以使用特定的注解来标记该方法,并定义其支持的事件类型。

如果监听器方法只支持单一的事件类型,我们可以将其参数声明为该事件类型的唯一实例。例如,如果我们的监听器方法只监听ApplicationEvent实例,则可以将其参数声明为ApplicationEvent类型。

如果监听器方法支持多种事件类型,我们可以使用注解的classes属性来指定一个或多个支持的事件类型。这样,该方法就可以监听到在classes属性中指定的任何事件类型。

2.2 事件处理条件

可以通过 condition 属性指定一个SpEL表达式,如果返回 “true”, “on”, “yes”, or “1” 中的任意一个,则事件会被处理,否则不会。

2.3 处理器

在Spring框架中,@EventListener注解的处理是通过内部的EventListenerMethodProcessor类进行的。这个类负责寻找带有@EventListener注解的方法,并在适当的时候触发它们。

当使用Java配置时,EventListenerMethodProcessor会自动注册到Spring容器中。你只需要在带有@EventListener注解的方法上添加@Component或@Service等注解,Spring就会自动检测并注册这个方法。

2.4 返回值

被标注的方法可以没有返回值,也可以有返回值。当有返回值是,其返回值会被当作为一个新的事件发送。如果返回类型是数组或集合,那么数组或集合中的每个元素都作为一个新的单独事件被发送。

2.5 异常处理

同步监听器抛出的所有checked异常都会被封装成 UndeclaredThrowableException ,因为事件发布者只能处理运行时异常(unchecked异常)。

2.6 异步监听器

当需要异步处理监听器时,可以在监听器方法上再增加另外的一个Spring注解 @Async,但需要注意以下限制:

监听器报错不会传递给事件发起者,因为双方已经不在同一个线程了。
异步监听器的非空返回值不会被当作新的事件发布。如果需要发布新事件,需要注入 ApplicationEventPublisher后手动发布。

2.7 监听器排序

如果同一个事件可能会被多个监听器监听处理,那么我们可以使用 @Order 注解对各个监听器进行排序。

2.8 源码

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EventListener {/*** Alias for {@link #classes}.*/@AliasFor("classes")Class<?>[] value() default {};/*** 可以处理的事件类型*/@AliasFor("value")Class<?>[] classes() default {};/*** SpEL表达式判断是否满足处理条件*/String condition() default "";/*** 可以给监听器指定一个id,默认是方法的全限定名,如:mypackage.MyClass.myMethod()*/String id() default "";}

三、示例详解

3.1 单一事件监听器

发布事件

@Service
public class EventPublisher {private ApplicationEventPublisher  eventPublisher;@Autowiredpublic void setEventPublisher(ApplicationEventPublisher eventPublisher) {this.eventPublisher = eventPublisher;}public void publishPersonSaveEvent(){PersonSaveEvent  saveEvent = new PersonSaveEvent();saveEvent.setId(1);saveEvent.setName("i余数");saveEvent.setAge(18);eventPublisher.publishEvent(saveEvent);}
}

监听事件

@Slf4j
@Service
public class EventListenerService {@EventListenerpublic void handleForPersonSaveEvent(PersonSaveEvent saveEvent){log.info("saveEvent -> {}", saveEvent);}
}

结果验证

saveEvent -> PersonSaveEvent(id=1, name=i余数, age=18)

3.2 使用classes实现多事件监听器

发布事件
在上一个示例的基础上,再多加一个PersonUpdateEvent事件。

public void publishPersonUpdateEvent(){PersonUpdateEvent  updateEvent = new PersonUpdateEvent();updateEvent.setId(1);updateEvent.setName("i余数");updateEvent.setAge(19);eventPublisher.publishEvent(updateEvent);
}

监听事件

@EventListener(classes = {PersonSaveEvent.class, PersonUpdateEvent.class})
public void handleForPersonSaveAndUpdateEvent(Object event){log.info("multi handle event -> {}", event);
}

验证结果
可以监听到多个事件

multi handle event -> PersonSaveEvent(id=1, name=i余数, age=18)
multi handle event -> PersonUpdateEvent(id=1, name=i余数, age=19)

3.3 使用condition筛选监听的事件

发布事件

public void publishPersonSaveEvent(){PersonSaveEvent  saveEvent = new PersonSaveEvent();saveEvent.setId(1);saveEvent.setName("i余数");saveEvent.setAge(18);eventPublisher.publishEvent(saveEvent);PersonSaveEvent  saveEvent2 = new PersonSaveEvent();saveEvent2.setId(2);saveEvent2.setName("i余数");saveEvent2.setAge(18);eventPublisher.publishEvent(saveEvent2);
}

监听事件

@EventListener(condition = "#root.event.getPayload().getId() == 1")
public void handleByCondition(PersonSaveEvent saveEvent){log.info("只处理id等于1的 -> {}", saveEvent);
}

结果验证
id为2的事件不满足条件,所以不会执行。

只处理id等于1的 -> PersonSaveEvent(id=1, name=i余数, age=18)

3.4 有返回值的监听器

3.4.1 返回一个单一对象

发布事件

public void publishPersonSaveEvent(){PersonSaveEvent  saveEvent = new PersonSaveEvent();saveEvent.setId(1);saveEvent.setName("i余数");saveEvent.setAge(18);eventPublisher.publishEvent(saveEvent);
}

监听事件

@EventListener
public void handleForPersonUpdateEvent(PersonUpdateEvent updateEvent){log.info("handle update event -> {}", updateEvent);
}@EventListener
public PersonUpdateEvent handleHaveReturn(PersonSaveEvent saveEvent){log.info("handle save event -> {}", saveEvent);PersonUpdateEvent updateEvent = new PersonUpdateEvent();updateEvent.setId(saveEvent.getId());updateEvent.setName(saveEvent.getName());updateEvent.setAge(saveEvent.getAge());return updateEvent;
}

验证结果
可以看到我们监听到了2个事件,PersonSaveEvent是我们主动发布的事件,PersonUpdateEvent 是 handleHaveReturn 方法的返回值,会被 Spring 自动当作一个事件被发送。

handle save event -> PersonSaveEvent(id=1, name=i余数, age=18)
handle update event -> PersonUpdateEvent(id=1, name=i余数, age=18)
3.4.2 返回一个集合

将监听器稍作修改,使其返回一个集合。

@EventListener
public List<PersonUpdateEvent> handleHaveReturn(PersonSaveEvent saveEvent){log.info("handle save event -> {}", saveEvent);List<PersonUpdateEvent> events = new ArrayList<>();PersonUpdateEvent updateEvent = new PersonUpdateEvent();updateEvent.setId(saveEvent.getId());updateEvent.setName(saveEvent.getName());updateEvent.setAge(saveEvent.getAge());events.add(updateEvent);PersonUpdateEvent updateEvent2 = new PersonUpdateEvent();BeanUtils.copyProperties(updateEvent, updateEvent2);events.add(updateEvent2);return events;}

查看结果可以发现,集合中的每个对象都被当作一个单独的事件进行发送。

handle save event -> PersonSaveEvent(id=1, name=i余数, age=18)
handle update event -> PersonUpdateEvent(id=1, name=i余数, age=18)
handle update event -> PersonUpdateEvent(id=1, name=i余数, age=18)
3.4.3 返回一个数组

和返回值为集合一样,数组中的每个对象都被当作一个单独的事件进行发送。

3.4.4 异步监听器

创建两个监听器,一个同步一个异步,异步监听器就是在方法上加一个 @Async 标签即可(你可以指定线程池)。

@EventListener
public void handleForPersonSaveEvent(PersonSaveEvent saveEvent){log.info("handle event -> {}", saveEvent);
}@Async
@EventListener
public void handleForPersonSaveEventAsync(PersonSaveEvent saveEvent){log.info("async handle event -> {}", saveEvent);
}

从执行结果可以看出,异步线程是 task-1,不是主线程 main,即异步是生效的。

INFO 3851 --- [           main] i.k.s.e.listener.EventListenerService    : handle event -> PersonSaveEvent(id=1, name=i余数, age=18)
INFO 3851 --- [         task-1] i.k.s.e.listener.EventListenerService    : async handle event -> PersonSaveEvent(id=1, name=i余数, age=18)

3.5 监听器异常处理

3.5.1 同步异常处理

使用 SimpleApplicationEventMulticaster 处理同步监听器抛出异常。
先定义一个ErrorHandler:

@Slf4j
@Component
public class MyErrorHandler implements ErrorHandler {@Overridepublic void handleError(Throwable t) {log.info("handle error -> {}", t.getClass());}
}

将自定义ErrorHandler绑定到 SimpleApplicationEventMulticaster。

@Slf4j
@Service
public class EventListenerService {@Autowiredprivate SimpleApplicationEventMulticaster simpleApplicationEventMulticaster;@Autowiredprivate MyErrorHandler errorHandler;@PostConstructpublic void init(){simpleApplicationEventMulticaster.setErrorHandler(errorHandler);}@Order(1)@EventListenerpublic void handleForPersonSaveEvent(PersonSaveEvent saveEvent) throws AuthException {log.info("handle event -> {}", saveEvent);throw new AuthException("test exception");}
}   

结果:可以看到捕获的异常是 UndeclaredThrowableException。

handle event -> PersonSaveEvent(id=1, name=i余数, age=18)
handle error -> class java.lang.reflect.UndeclaredThrowableException
3.5.2 异步异常处理

使用 SimpleAsyncUncaughtExceptionHandler 来处理 @Async 抛出的异常。

@Configuration
public class AsyncConfig implements AsyncConfigurer {@Overridepublic AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {return new SimpleAsyncUncaughtExceptionHandler();}
}

监听器代码:人为的抛出一个异常。

@Async
@EventListener
public void handleForPersonSaveEvent(PersonSaveEvent saveEvent) throws AuthException {log.info("handle event -> {}", saveEvent);throw new AuthException("test exception");
}

结果: SimpleAsyncUncaughtExceptionHandler捕获到了 @Async 方法抛出的异常

 INFO 4416 --- [         task-1] i.k.s.e.listener.EventListenerService    : handle event -> PersonSaveEvent(id=1, name=i余数, age=18)
ERROR 4416 --- [         task-1] .a.i.SimpleAsyncUncaughtExceptionHandler : Unexpected exception occurred invoking async method: public void xxxx.handleForPersonSaveEvent(xxxx.PersonSaveEvent) throws javax.security.auth.message.AuthException

3.6 监听器排序

如果同时有多个监听器监听同一个事件,默认情况下监听器的执行顺序是随机的,如果想要他们按照某种顺序执行,可以借助Spring的另外一个注解 @Order 实现。

创建三个监听器,并使用@Order 排好序。

@Order(1)
@EventListener
public void handleForPersonSaveEvent(PersonSaveEvent saveEvent){log.info("handle event1 -> {}", saveEvent);
}@Order(2)
@EventListener
public void handleForPersonSaveEvent2(PersonSaveEvent saveEvent){log.info("handle event2 -> {}", saveEvent);
}@Order(3)
@EventListener
public void handleForPersonSaveEvent3(PersonSaveEvent saveEvent){log.info("handle event3 -> {}", saveEvent);
}

从执行结果可以看到,确实是按照@Order中指定的顺序执行的。

handle event1 -> PersonSaveEvent(id=1, name=i余数, age=18)
handle event2 -> PersonSaveEvent(id=1, name=i余数, age=18)
handle event3 -> PersonSaveEvent(id=1, name=i余数, age=18)

四、总结

在Spring框架中,@EventListener注解用于标识监听器方法,可以监听Spring ApplicationEvent或其他任意对象的事件。

当一个方法被标注为@EventListener时,它就被视为一个事件监听器,可以接收和响应特定类型的事件。这个方法可以在事件发布时被调用,以处理该事件。在Spring框架中,EventListenerMethodProcessor负责寻找和调用带有@EventListener注解的方法。

@EventListener注解可以设置两个属性:value和classes,用于指定监听器监听的事件类型。如果value和classes没有设置值,那么被标注的方法必须有一个且只能有一个参数。如果value或classes设置了值,单个Class值表示监听器可以不用设置参数;多个Class值则要求被标注的方法一定不要设置参数。这是因为在监听事件时,通过反射监听方法处理事件,事件转换成方法参数时,若类型不一致,可能会发生类型转换异常。

此外,还有一个注解@TransactionalEventListener,它继承了@EventListener的功能,并添加了新的特性。这个注解可以用于在事务处理期间监听事件,并且可以根据需要自动回滚事务。

总的来说,EventListener是一种非常有用的技术,可以帮助我们更好地管理和响应应用程序中的事件。在Spring框架中,我们可以使用@EventListener注解来方便地创建和注册事件监听器,从而实现更高效的事件处理和控制。

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

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

相关文章

Unity 关于生命周期函数的一些认识

Unity 生命周期函数主要有以下一些&#xff1a; Awake(): 在脚本被加载时调用。用于初始化对象的状态和引用。 OnEnable(): 在脚本组件被启用时调用。在脚本组件被激活时执行一次&#xff0c;以及在脚本组件被重新激活时执行。 Reset(): 在脚本组件被重置时调用。用于重置脚本…

微信小程序Vue+nodejs教室自习室座位预约系统68u2m

本文从管理员、用户的功能要求出发&#xff0c;教室预约系统小程序中的功能模块主要是实现管理端&#xff1b;首页、个人中心、教室信息管理、教室设备管理、用户管理、教室预约管理、管理员管理、系统管理&#xff0c;微信端&#xff1b;首页、教室信息、教室设备、教室预约、…

西南科技大学信号与系统A实验二(信号频谱分析)

一、实验目的 1.掌握用 matlab 软件绘制信号频谱的方法; 2.进一步理解抽样定理; 3.理解傅里叶变换的性质(频移特性). 二、实验原理 (一)fft 函数的调用 matlab 提供 fft 函数来计算信号 x(n)的快速离散傅里叶变换 (FFT). z 格式:y=fft(x) 计算信号 x 的快速离散傅里叶…

链路追踪详解(二):链路追踪技术的演进

目录 早期的链路追踪 分布式系统的挑战 链路追踪技术的诞生 链路追踪系统的开源和标准化 微服务架构对链路追踪技术的促进 云原生时代下的链路追踪技术 链路追踪技术未来的趋势 小结 链路追踪的发展历程可以追溯到几十年前&#xff0c;随着互联网技术的不断发展&#x…

【Rust】结构体与枚举

结构体struct 基础用法 一个存储用户账号信息的结构体&#xff1a; struct User {active: bool,username: String,email: String,sign_in_count: u64, }声明一个User的实例变量&#xff1a; struct User {active: bool,username: String,email: String,sign_in_count: u64,…

若依vue-修改标题和图标

因为我们拉下来的代码,图标和logo是若依的,这和我们需要做出来的效果有差别 这个时候就需要去对应的文件内去修改标题和图标 (主要就是这两个地方的图标和标题) 修改菜单里面的logo以及文字 修改文字 位置: src/layout/component/Sidebar/Logo.vue 此处的title文字是定义在…

ESXi vSAN 整合多主机磁盘

VSAN 与 RAID区别&#xff1a; vSAN 可以管理 ESXi 主机&#xff0c;且只能与 ESXi 主机配合使用。一个 vSAN 实例仅支持一个群集。vSAN 不需要外部网络存储来远程存储虚拟机文件&#xff0c;例如光纤通道 (FC) 或存储区域网络 (SAN) 使用传统存储&#xff0c;存储管理员可以…

如何给echarts的legend设置不同的样式和位置 legend分组显示

legend分组显示 给每一个图例设置不一样的位置和样式 样式如下 demo代码 option {title: {text: Stacked Line},tooltip: {trigger: axis},// legend写为数组可以给一些给某些组分配一些不一样的样式legend: [{data: [// 使用svg画任意的图形{name:Email,icon: path://"…

科学与工程计算基础(数值计算)知识点总结

数值计算 第1章 概论1.2 数值计算中的误差1.2.1 误差的来源和分类1.2.2 误差与有效数字1.2.3 数值运算的误差估计 1.3 误差定性分析和避免误差危害1.3.1 算法的数值稳定性1.3.3 避免误差危害 1.4 数值计算中算法设计的技术1.5 习题1.5.1 判断题1.5.2 计算题 第2章 插值法2.2 拉…

Helm3部署kubeview资源可视化工具

一、KubeView 介绍 KubeView 是一个 Kubernetes 集群可视化工具和可视化资源管理器。它提供了跨命名空间的 Kubernetes 对象的完整概述&#xff0c;以及它们如何通过直观的 UI 和资源对象相互连接。它允许用户在集群内部运行命令&#xff0c;并查看集群内部的资源使用情况、容器…

详解原生Spring中的控制反转和依赖注入-构造注入和Set注入

&#x1f609;&#x1f609; 学习交流群&#xff1a; ✅✅1&#xff1a;这是孙哥suns给大家的福利&#xff01; ✨✨2&#xff1a;我们免费分享Netty、Dubbo、k8s、Mybatis、Spring...应用和源码级别的视频资料 &#x1f96d;&#x1f96d;3&#xff1a;QQ群&#xff1a;583783…

力扣labuladong一刷day18天差分数组

力扣labuladong一刷day18天差分数组 文章目录 力扣labuladong一刷day18天差分数组一、370. 区间加法二、1109. 航班预订统计三、1094. 拼车 一、370. 区间加法 题目链接&#xff1a;https://leetcode.cn/problems/range-addition/ 思路&#xff1a;这种频繁改变数组的值&#…

pandas教程:US Baby Names 1880–2010 1880年至2010年美国婴儿姓名

文章目录 14.3 US Baby Names 1880–2010&#xff08;1880年至2010年美国婴儿姓名&#xff09;1 Analyzing Naming Trends&#xff08;分析命名趋势&#xff09;评价命名多样性的增长“最后一个字母”的变革变成女孩名字的男孩名字&#xff08;以及相反的情况&#xff09; 14.3…

【CAN通信】CanTrcv和EcuM的关系及不同类型的Can收发器主要使用场景

目录 前言 正文 1.CanTrcv_SetOpMode被哪个模块调用,在什么场景下调用? 1.1 CanSM模块切换CanTrcv模块状态

Drool 7 SpreadSheet Decision Template 笔记

1 Excel Decision table 1.1 很棒的示意图&#xff0c;来自https://blog.csdn.net/justlpf/article/details/128109731 1.2 参考URL 1.2.1 https://blog.csdn.net/justlpf/article/details/128109731 1.3 多sheet 模式 默认是用第一个sheet如果要支持多sheet&#xff0c;需…

查看各ip下的连接数

netstat -n | awk /^tcp/ {print $5} | awk -F: {print $1} | sort | uniq -c| sort -rn netstat -n&#xff1a;显示所有的网络连接&#xff0c;不包括任何服务名的解释。awk /^tcp/ {print $5}&#xff1a;使用awk命令过滤出tcp协议的连接&#xff0c;并打印出每个连接的第五…

docker限制容器内存的方法

在服务器中使用 docker 时&#xff0c;如果不对 docker 的可调用内存进行限制&#xff0c;当 docker 内的程序出现不可预测的问题时&#xff0c;就很有可能因为内存爆炸导致服务器主机的瘫痪。而对 docker 进行限制后&#xff0c;可以将瘫痪范围控制在 docker 内。 因此&#…

如何运行C/C++程序

一、在线运行C/C 码曰 - 让代码在云端多飞一会&#xff1a;这是一个支持C/C&#xff0c;Java&#xff0c;Python等多种语言的在线编程&#xff0c;编译运行&#xff0c;粘贴分享的平台。你可以在这里输入你的代码&#xff0c;点击运行按钮&#xff0c;就可以看到输出结果。你也…

BGP笔记全

自治系统---AS 定义&#xff1a;由一个单一的机构或者组织所管理的一系列IP网络及其设备所构成的集合。 AS划分的原因 如果整张网络很大&#xff0c;路由数量进一步增加&#xff0c;路由表规模变得太大&#xff0c;会导致路由收敛速度变慢&#xff0c;设备性能消耗加大&#…

智能优化算法应用:基于猫群算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于猫群算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于猫群算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.猫群算法4.实验参数设定5.算法结果6.参考文献7.MATLAB…