Spring带泛型的ApplicationEvent无法监听问题分析(转载)

news/2024/11/14 18:04:15/文章来源:https://www.cnblogs.com/zhangqingyan/p/18540048

1 背景

在开发过程中,经常遇到发送事件来通知其他模块进行相应的业务处理;笔者实用的是spring自带的ApplicationEventPublisherEventListener进行事件的发收;
但是开发时遇到一个问题:
如果事件很多,但是事件模式都差不多,就需要定义很多事件类来分别表示各种事件,例如,我们进行数据同步,每同步一条数据都要发送对应的事件,伪代码如下:

//事件类
class RegionEvent {private Region region;private OperationEnum operation;
}class UserEvent {private User user;private OperationEnum operation;
}//插入一个区域
regionDao.insert(Region region);
//发送插入区域事件
publisher.publishEvent(new RegionEvent(region, INSERT));//更新一个用户
userDao.update(User user);
//发送更新用户事件
publisher.publishEvent(new UserEvent(user, UPDATE));//区域事件监听器
@EventListener
public void onRegionEvent(RegionEvent event) {log.info("receive event: {}", event);
}//用户事件监听器
@EventListener
public void onUserEvent(UserEvent event) {log.info("receive event: {}", event);
}

此时,我们发现有太多冗余的代码,因为每插入一种类型的数据,就要对应的建立一个和该类型相关的事件类;自然而然地,我们想到可以使用泛型来简化以上逻辑。

1 泛型事件遇到的问题

我们定义一种泛型事件,来重新实现以上的逻辑,此时我们发现一个问题:发送的事件根本监听不到,伪代码如下:

class BaseEvent<T> {private T data;private OperationEnum operation;
}//发送插入区域事件
publisher.publishEvent(new BaseEvent<>(region, INSERT));
//发送更新用户事件
publisher.publishEvent(new BaseEvent<>(user, UPDATE));//区域事件监听器
@EventListener
public void onRegionEvent(BaseEvent<Region> event) {log.info("receive event: {}", event);
}//用户事件监听器
@EventListener
public void onUserEvent(BaseEvent<User> event) {log.info("receive event: {}", event);
}

这是由于spring在解析事件类型时,并没有对事件的泛型进行解析,导致在运行时所有publish的事件都被spring解析成了BaseEvent<?>事件,如果采用如下代码,则会监听到所有事件:

@EventListener
public void onUserEvent(BaseEvent<Object> event) {log.info("receive event: {}", event);
}@EventListener
public void onUserEvent(BaseEvent event) {log.info("receive event: {}", event);
}

2 解决方法

查阅了spring的文档后,发现spring已经考虑到这一点,官方文档原文如下:

In certain circumstances, this may become quite tedious if all events follow the same structure. In such a case, you can implement ResolvableTypeProvider to guide the framework beyond what the runtime environment provides. The following event shows how to do so:
大概翻译一下:
在某些情况下,如果所有事件类型都遵循相同的结构,这会是特别恶心的一件事。在这种情况下,你可以通过实现ResolvableTypeProvider接口,在运行时基于环境提供的信息来引导框架

我们基于spring提供的方法,对原有的泛型事件进行改造:

public class BaseEvent<T> implements ResolvableTypeProvider {private T data;private OperationEnum operation;@Overridepublic ResolvableType getResolvableType() {return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forClass(getData().getClass()));}
}

此时,使用上文的监听器就可以监听到对应的事件了;

3 原理

事件监听器和事件是通过事件类型进行匹配的,而事件类型的publish源码在AbstractApplicationContext类的
protected void publishEvent(Object event, @Nullable ResolvableType eventType)
方法中,如下:

        ApplicationEvent applicationEvent;if (event instanceof ApplicationEvent) {//对于继承ApplicationEvent的事件,applicationEvent = (ApplicationEvent) event;}else {//对于非继承ApplicationEvent的事件,包装成PayloadApplicationEvent,//然后通过getResolvableType()获取事件类型applicationEvent = new PayloadApplicationEvent<>(this, event);if (eventType == null) {eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();}}// Multicast right now if possible - or lazily once the multicaster is initializedif (this.earlyApplicationEvents != null) {this.earlyApplicationEvents.add(applicationEvent);}else {getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);}

然后进入multicastEvent(applicationEvent, eventType)方法:

    @Overridepublic void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {//这里对于ApplicationEvent的子类事件,进行解析事件类型ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));Executor executor = getTaskExecutor();//根据上面解析到的eventType,获取对应的监听器,并依次执行回调方法for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {if (executor != null) {executor.execute(() -> invokeListener(listener, event));}else {invokeListener(listener, event);}}}

可以发现,关键在于如何解析事件类型,分别进入上文中resolveDefaultEventType()方法和getResolvableType()方法,可以看到解析事件类型的具体细节如下:

//针对PayloadApplicationEvent,通过下面的方法处理,可见@Overridepublic ResolvableType getResolvableType() {return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getPayload()));}
//对于继承了ApplicationEvent的事件类private ResolvableType resolveDefaultEventType(ApplicationEvent event) {return ResolvableType.forInstance(event);}

上述两个方法用于根据事件构造事件的ResolvableType,关键代码在ResolvableType.forInstance():

    public static ResolvableType forInstance(Object instance) {Assert.notNull(instance, "Instance must not be null");if (instance instanceof ResolvableTypeProvider) {ResolvableType type = ((ResolvableTypeProvider) instance).getResolvableType();if (type != null) {return type;}}return ResolvableType.forClass(instance.getClass());}

至此,可以看到,如果事件实现了ResolvableTypeProvider接口,则可以通过调用getResolvableType方法获取事件的带泛型类型,如果未实现该接口,则只能获取事件的原始类型,效果如下:

未实现接口的情况下:

实现接口后:

作者:TinyThing
链接:https://www.jianshu.com/p/fd0c358176b9
来源:简书

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

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

相关文章

PG 修改表结构提示有试图依赖的处理方法

ALTER TABLE victim ALTER COLUMN victim_belong_url TYPE varchar(1000) USING victim_belong_url::varchar(1000); 修改字段长度 通过修改 pg_attribute 基表的方式来绕开这个限制#通过表名查出attrelid SELECT relname, attname,attnum,attrelid,attname FROM pg_class c,pg…

OMV安装文件管理器filebrowser和照片管理photoprism插件时Pull不了镜像的解决办法

OMV安装文件管理器filebrowser和照片管理photoprism插件安装后不能启动服务或者PULL不了镜像卡着不动都是因为现在国内pull不了镜像的原因 这里有个迷惑的人的地方是很多朋友认为是用docker来pull的镜像,于是增加了docker国内加速源后发现OMV还是拉取不了镜像。解决方法如下:…

NOIP2024加赛4

NOIP2024加赛4\(T1\) luogu P11267 【MX-S5-T1】王国边缘 \(85pts\)预处理前缀中最后一个 \(1\) 出现的位置然后就可以倍增跳了。点击查看代码 const ll p=1000000007; int nxt[200010][62],f[200010][62],last[200010]; char t[200010]; ll divide(ll s,ll k) {ll ans=0;for(l…

触想染织厂MES产线终端工位机,打造数字化高效车间

一、行业发展背景在纺织细分领域中,印染行业一直是整个产业链的效率短板,因其涉及染色、定型及后整理加工等多个复杂工艺、上百个参数变量,质量波动较大,依赖个人经验和手工操作,常常陷入高成本、低效率发展困境。△某印染工厂生产场景二、行业应用需求印染厂要真正实现效…

超强抗干扰单键触摸/电容式触控IC-VK3601 SOT23-6单通道直接输出/触摸感应方案原厂

产品品牌:永嘉微电/VINKA 产品型号:VK3601 封装形式:SOT23-6 概述 VK3601具有1个触摸按键,可用来检测外部触摸按键上人手的触摸动作。该芯片具有较 高的集成度,仅需极少的外部组件便可实现触摸按键的检测。 提供了1路直接输出功能。芯片内部采用特殊的集成电路,具有高电源…

重载和重写的区别

重载(Overloading)和重写(Overriding)是面向对象编程中两个重要的概念,它们在实现多态性时起着关键作用,但两者之间有明显的区别:定义上的区别:重载(Overloading) 指的是在同一个类中可以有多个方法名相同,但这些方法的参数列表(参数的个数、类型或顺序)不同,或者…

Python 学习记录 (4)

Plotly常见可视化方案:以鸢尾花数据为例 简单介绍:Ploty库也有大量统计可视化方案,并且这些可视化方案具有交互化属性。 主要对鸢尾花数据进行处理与可视化。 所展示的结果为交互界面的截图情况,这里不能进行交互。使用Plotly绘制散点图与箱型图,分类展示“花萼宽度” 说明…

聚丙烯的节奏

晚上7420附近企稳做多 损7400下方 这两天目标7530以上

低空载功耗,高能源利用率 BDA5-20W BOSHIDA DCDC

低空载功耗,高能源利用率 BDA5-20W BOSHIDA DCDC BDA5-20W系列产品具有以下特点:宽输入电压范围(4:1),可以适应多种输入电压条件;高效率,能够达到88%以上,节能环保;空载功耗低,可以节省能源;隔离电压为500VDC,能够提供良好的电气隔离性能;具有输入欠压保护、输出过…

LCEL与AgentExecutor的局限性分析

在大语言模型(LLM)应用开发领域,LangChain表达式语言(LCEL)和AgentExecutor一直是开发者的得力助手。然而,随着应用场景的复杂化,这些工具的局限性也日益凸显。本文将深入探讨LCEL和AgentExecutor的不足,并引入一个新的解决方案。 LCEL链表达式的局限性 LangChain表达式语言(LC…

SSD201/202D的SPI通讯该怎么操作,触觉智能带来保姆级攻略

本文介绍SigmaStar SSD201/SSD202D中SPI通讯的测试方法,SPI是一种常见的串行通信协议,它被广泛用于微控制器与各种外设之间的数据传输。使用触觉智能Purple Pi R1双网口开发板演示。本文介绍SigmaStarSSD201/SSD202D中SPI通讯的测试方法,SPI全称是Serial Peripheral Interfa…

多线程锁的升级原理是什么

锁的级别:无锁 => 偏向锁 => 轻量级锁 => 重量级锁 无锁:没有对资源进行锁定,所有线程都可以访问,但是只有一个线程能成功修改资源,其他的线程会不断尝试,直至修改成功。 偏向锁:偏向锁是指当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储…