Spring解决泛型擦除的思路不错,现在它是我的了。

你好呀,我是浮生。

Spring 的事件监听机制,不知道你有没有用过,实际开发过程中用来进行代码解耦简直不要太爽。

但是我最近碰到了一个涉及到泛型的场景,常规套路下,在这个场景中使用该机制看起来会很傻,但是最终了解到 Spring 有一个优雅的解决方案,然后去了解了一下,感觉有点意思。

和你一起盘一盘。

Demo

首先,第一步啥也别说,先搞一个 Demo 出来。

需求也很简单,假设我们有一个 Person 表,每当 Person 表新增或者修改一条数据的时候,给指定服务同步一下。

伪代码非常的简单:

boolean success = addPerson(person)
if(success){//发送person,add代表新增sendToServer(person,"add");
}

这代码能用,完全没有任何问题。

但是,你仔细想,“发给指定服务同步一下”这样的动作按理来说,不应该和用户新增和更新的行为“耦合”在一起,他们应该是两个独立的逻辑。

所以从优雅实现的角度出发,我们可以用 Spring 的事件机制进行解耦。

比如改成这样:

boolean success = addPerson(person)
if(success){publicAddPersonEvent(person,"add");
}

addPerson 成功之后,直接发布一个事件出去,然后“发给指定服务同步一下”这件事情就可以放在事件监听器去做。

对应的代码也很简单,新建一个 SpringBoot 工程。

首先我们先搞一个 Person 对象:

@Data
public class Person {private String name;public Person(String name) {this.name = name;}
}

由于我们还要告知是新增还是修改,所以还需要搞个对象封装一层:

@Data
public class PersonEvent {private Person person;private String addOrUpdate;public PersonEvent(Person person, String addOrUpdate) {this.person = person;this.addOrUpdate = addOrUpdate;}
}

然后搞一个事件发布器:

@Slf4j
@RestController
public class TestController {@Resourceprivate ApplicationContext applicationContext;@GetMapping("/publishEvent")public void publishEvent() {applicationContext.publishEvent(new PersonEvent(new Person("why"), "add"));}
}

最后来一个监听器:

@Slf4j
@Component
public class EventListenerService {@EventListenerpublic void handlePersonEvent(PersonEvent personEvent) {log.info("监听到PersonEvent: {}", personEvent);}}

Demo 就算是齐活了,你把代码粘过去,也用不了一分钟吧。

启动服务跑一把:

看起来没有任何毛病,在监听器里面直接就监听到了。

这个时候假设,我还有一个对象,叫做 Order,每当 Order 表新增或者修改一条数据的时候,也要给指定服务同步一下。

怎么办?

这还不简单?

照葫芦画瓢呗。

先来一个 Order 对象:

@Data
public class Order {private String orderName;public Order(String orderName) {this.orderName = orderName;}
}

再来一个 OrderEvent 封装一层:

@Data
public class OrderEvent {private Order order;private String addOrUpdate;public OrderEvent(Order order, String addOrUpdate) {this.order = order;this.addOrUpdate = addOrUpdate;}
}

然后再发布一个对应的事件:

新增一个对应的事件监听:

发起调用:

完美,两个事件都监听到了。

那么问题又来了,假设我还有一个对象,叫做 Account,每当 Account 表新增或者修改一条数据的时候,也要给指定服务同步一下。

或者说,我有几十张表,对应几十个对象,都要做类似的同步。

请问阁下又该如何应对?

你当然可以按照前面处理 Order 的方式,继续依葫芦画瓢。

但是这样势必会来带的一个问题是对象的膨胀,你想啊,毕竟每一个对象都需要一个对应的 xxxxEvent 封装对象。

这样的代码过于冗余,丑,不优雅。

怎么办?

自然而然的我们能想到泛型,毕竟人家干这个事儿是专业的,放一个通配符,管你多少个对象,通通都是“T”,也就是这样的:

@Data
class BaseEvent<T> {private T data;private String addOrUpdate;public BaseEvent(T data, String addOrUpdate) {this.data = data;this.addOrUpdate = addOrUpdate;}}

对应的事件发布的地方也可以用 BaseEvent 来代替:

这样用一个 BaseEvent 就能代替无数的 xxxEvent,做到通用,这是它的好处。

同时对应的监听器也需要修改:

启动服务,跑一把。

发起调用之后你会发现控制台正常输出:

但是,注意我要说但是了。

但是监听这一坨代码我感觉不爽,全部都写在一个方法里面了,需要用非常多的 if 分支去做判断。

而且,假设某些对象在同步之前,还有一些个性化的加工需求,那么都会体现在这一坨代码中,不够优雅。

怎么办呢?

很简单,拆开监听:

但是再次重启服务,发起调用你会发现:控制台没有输出了?怎么回事,怎么监听不到了呢?

官网怎么说?

在 Spring 的官方文档中,关于泛型类型的事件通知只有寥寥数语,但是提到了两个解决方案:

https://docs.spring.io/spring-framework/reference/core/beans/context-introduction.html#context-functionality-events-generics

首先官网给出了这样的一个泛型对象:EntityCreatedEvent

然后说比如我们要监听 Person 这个对象创建时的事件,那么对应的监听器代码就是这样的:

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {// ...
}

和我们 Demo 里面的代码结构是一样的。

那么怎么才能触发这个监听呢?

第一种方式是:

class PersonCreatedEvent extends EntityCreatedEvent<Person> { … }).

也就是给这个对象创造一个对应的 xxxCreatedEvent,然后去监听这个 xxxCreatedEvent。

和我们前面提到的 xxxxEvent 封装对象是一回事。

为什么我们必须要这样做呢?

官网上提到了这几个词:

Due to type erasure

type erasure,泛型擦除。

因为泛型擦除,所以导致直接监听 EntityCreatedEvent 事件是不生效的,因为在泛型擦除之后,EntityCreatedEvent 变成了 EntityCreatedEvent<?>。

封装一个对象继承泛型对象,通过他们之间一一对应的关系从而绕开泛型擦除这个问题,这个方案确实是可以解决问题。

但是,前面说了,不够优雅。

官网也觉得这个事情很傻:

它怎么说的呢?

In certain circumstances, this may become quite tedious if all events follow the same structure.
在某些情况下,如果所有事件都遵循相同的结构,这可能会变得相当 tedious。

好,那么 tedious,是什么意思?哪个同学举手回答一下?

这是个四级词汇,得认识,以后考试的时候要考:

quite tedious,相当啰嗦。

我们都不希望自己的程序看起来是 tedious 的。

所以,官方给出了另外一个解决方案:ResolvableTypeProvider。

我也不知道这是在干什么,反正我拿到了代码样例,那我们就白嫖一下嘛:

@Data
class BaseEvent<T> implements ResolvableTypeProvider {private T data;private String addOrUpdate;public BaseEvent(T data, String addOrUpdate) {this.data = data;this.addOrUpdate = addOrUpdate;}@Overridepublic ResolvableType getResolvableType() {return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getData()));}
}

再次启动服务,你会发现,监听器又好使了:

那么问题又来了。

这是为什么呢?

为什么?

我也不知道为什么,但是我知道源码之下无秘密。

所以,先打上断点再说。

关于 @EventListener 注解的原理和源码解析,我之前写过一篇相关的文章:《扯下@EventListener这个注解的神秘面纱。》

有兴趣的可以看看这篇文章,然后再试着按照文章中的方式去找对应的源码。

我这篇文章就不去抽丝剥茧的一点点找源码了,直接就是一个大力出奇迹。

因为我们已知是 ResolvableTypeProvider 这个接口在搞事情,所以我只需要看看这个接口在代码中被使用的地方有哪些:

除去一些注释和包导入的地方,整个项目中只有 ResolvableType 和 MultipartHttpMessageWriter 这个两个中用到了。

直觉告诉我,应该是在 ResolvableType 用到的地方打断点,因为另外一个类看起来是 Http 相关的,和我的 Demo 没啥关系。

所以我直接在这里打上断点,然后发起调用,程序果然就停在了断点处:

org.springframework.core.ResolvableType#forInstance

我们观察一下,发现这几行代码核心就干一个事儿:判断 instance 是不是 ResolvableTypeProvider 的子类。

如果是则返回一个 type,如果不是则返回 forClass(instance.getClass())。

通过 Debug 我们发现 instance 是 BaseEvent:

巧了,这就是 ResolvableTypeProvider 的子类,所以返回的 type 是这样式儿的:

com.example.elasticjobtest.BaseEvent<com.example.elasticjobtest.Person>

是带具体的类型的,而这个类型就是通过 getResolvableType 方法拿到的。

前面我们在实现 ResolvableTypeProvider 的时候,就重写了 getResolvableType 方法,调用了 ResolvableType.forClassWithGenerics,然后用 data 对应的真正的 T 对象实例的类型,作为返回值,这样泛型对应的真正的对象类型,就在运行期被动态的获取到了,从而解决了编译阶段泛型擦除的问题。

如果没有实现 ResolvableTypeProvider 接口,那么这个方法返回的就是 BaseEvent<?>:

com.example.elasticjobtest.BaseEvent<?>

看到这里你也就猜到个七七八八了。

都已经拿到具体的泛型对象了,后面再发起对应的事件监听,那不是顺理成章的事情吗?

好,现在你在第一个断点处就收获到了一个这么关键的信息,接下来怎么办呢?

接着断点处往下调试,然后把整个链路都梳理清楚呗。

再往下走,你会来到这个地方:

org.springframework.context.event.AbstractApplicationEventMulticaster#getApplicationListeners

从 cache 里面获取到了一个 null。

因为这个缓存里面放的就是在项目启动过程中已经触发过的框架自带的 listener 对象:

调用的时候,如果能从缓存中拿到对应的 listener,则直接返回。而我们 Demo 中的自定义 listener 是第一次触发,所以肯定是没有的。

因此关键逻辑就这个方法的最后一行:retrieveApplicationListeners 方法里面

org.springframework.context.event.AbstractApplicationEventMulticaster#retrieveApplicationListeners

这个地方再往下写,就是我前面我提到的这篇文章中我写过的内容了《扯下@EventListener这个注解的神秘面纱。》。

和泛型擦除的关系已经不大了,我就不再写一次了。

只是给大家看一下这个方法在我们的 Demo 中,最终返回的 allListeners 就是我们自定义的这个事件监听器:

com.example.elasticjobtest.EventListenerService#handlePersonEvent

为什么是这个?

因为我当前发布的事件的主角就是 Person 对象:

同理,当 Order 对象的事件过来的时候,这里肯定就是对应的 handleOrderEvent 方法:

如果我们把 BaseEvent 的 ResolvableTypeProvider 接口拿掉,那么你再看对应的 allListeners,你就会发现找不到我们对应的自定义 Listener 了:

为什么?

因为当前事件对应的 ResolvableType 是这样的:

org.springframework.context.PayloadApplicationEvent<com.example.elasticjobtest.BaseEvent<?>>

而我们并没有自定义一个这样的 Listener:

@EventListener
public void handleAllEvent(BaseEvent<?> orderEvent) {log.info("监听到Event: {}", orderEvent);
}

所以,这个事件发布了,但是没有对应的消费。

大概就是这么个意思。

核心逻辑就在 ResolvableTypeProvider 接口里面,重写了 getResolvableType 方法,在运行期动态的获取泛型对应的真正的对象类型,从而解决了编译阶段泛型擦除的问题。

很好,现在摸清楚了,是个很简单的思路,之前是 Spring 的,现在它是我的了。

为什么需要发布订阅模式 ?

既然写到 Spring 的事件通知机制了,那么就顺便聊聊这个发布订阅模式。

也许在看的过程中,你会冒出这样一个问题:为什么要搞这么麻烦?把这些事件监听的业务逻辑直接写在对应的数据库操作语句之后不行么?

要回答这个问题,我们可以先总结一下事件通知机制的使用场景。

  1. 数据变化之后同步清除缓存,这是一种简单可靠的缓存更新方式。只有在清除失败,或者数据库主从同步间隙被脏读才有可能出现缓存脏数据,概率比较小,一般业务上也是可以接受的。

  2. 通过某种方式告诉下游系统数据变化,比如往消息队列里面扔消息。

  3. 数据的统计、监控、异步触发等场景。当然这动作似乎用 AOP 也可以做,但是实际上在某些业务场景下,做切面统计,反而没有通过发布订阅机制来得直接,灵活度也更好。

除了上面这些外,肯定还有一些其他的场景,但是这些场景都有一个共同点:与核心业务关系不大,但是又具备一定的普适性。

比如完成用户注册之后给用户发一个短信,或者发个邮件啥的。这个事情用发布订阅机制来做是再合适不过的了。

编码过程中牢记单一职责原则,要知道一个类该干什么不该干什么,这是面向对象编程 的关键点之一。

当你一个类中注入了大量的 Service 的时候,你就要考虑考虑,是不是有什么做的不合适的地方了,是不是有些 Service 其实不应该注入进来的。

是不是该用用发布订阅了?

另外,当你的项目中真的出现了文章最开始说的,各种各样的 xxxEvent 事件对应的封装的时候,任何一个来开发的人都觉得这样写是不是有点冗余的时候,你就应该考虑一下是不是有更加优雅的解决方案。

假设这个方案由于某些原因不能使用或者不敢使用是一回事。

但是知不知道这个方案,是另一回事。

总结

以上我们讲了在高并发场景在如何保证结果一致性方式,在并发量高情况下推荐使用悲观锁的方式,如果并发量不高可以考虑使用乐观锁,推荐使用版本号方式,同时要注意幂等性与aba的问题。

扫描下面的二维码或者关注我们的:

微星公众帐号:灰灰聊架构        回复暗号:321

在微信公众帐号中  回复暗号:321 即可加入到我们的技术讨论群里面共同学习。

 

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

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

相关文章

15、FreeRTOS 软件定时器

文章目录 一、什么是定时器?1.1 定时器的理解1.2 软件定时器的特性 二、 软件定时器的上下文2.1 守护任务2.2 守护任务的调度2.3 回调函数 三、软件定时器的函数3.1 创建3.2 删除3.3 启动/停止3.5 修改周期3.6 定时器ID 四、案例4.1 一般使用4.2 消除抖动 一、什么是定时器? …

Midjourney Imagine API 申请及使用

Midjourney Imagine API 申请及使用 申请流程 要使用 Midjourney Imagine API&#xff0c;首先可以到 Midjourney Imagine API 页面点击「Acquire」按钮&#xff0c;获取请求所需要的凭证&#xff1a; 如果你尚未登录或注册&#xff0c;会自动跳转到登录页面邀请您来注册和登…

语音转文字服务的调用接口

语音转文字&#xff08;Speech-to-Text&#xff0c;STT&#xff09;技术允许将口语化的语音转换成书面文字。以下是一些提供语音转文字服务的调用接口及其特点。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.讯飞开放平台语音转写…

[猫头虎分享21天微信小程序基础入门教程]第1天:微信小程序概述与开发环境搭建教程

第1天&#xff1a;微信小程序概述与开发环境搭建 &#x1f63a; 文章目录 第1天&#xff1a;微信小程序概述与开发环境搭建 &#x1f63a;自我介绍微信小程序概述特点 开发环境搭建步骤1: 注册微信小程序账号步骤2: 安装开发者工具步骤3: 熟悉开发者工具界面 今日学习总结小测试…

炒股开户佣金最低万1和万0.854,融资融券现在利率最低4.0%~5%

​​炒股开户佣金一般是万1和万0.854&#xff0c;万0.854有一定的资金量要求&#xff0c;高于万1的是可以申请降低的。 开户万1佣金和万0.854佣金只需要联系证券公司客户经理协商就行。 开户流程&#xff1a; 1、向客户经理索要开户链接或者扫描二维码、进入申请页面&#x…

本地搭建各大直播平台录屏服务结合内网穿透工具实现远程管理录屏任务

文章目录 1. Bililive-go与套件下载1.1 获取ffmpeg1.2 获取Bililive-go1.3 配置套件 2. 本地运行测试3. 录屏设置演示4. 内网穿透工具下载安装5. 配置Bililive-go公网地址6. 配置固定公网地址 本文主要介绍如何在Windows系统电脑本地部署直播录屏利器Bililive-go&#xff0c;并…

Nachi那智不二越机器人维修技术合集

一、Nachi机械手维护基础知识 1. 定期检查&#xff1a;定期检查机器人的各个部件&#xff0c;如机械手伺服电机、机器人减速器、机械臂传感器等&#xff0c;确保其运行正常。 2. 清洁与润滑&#xff1a;定期清洁Nachi工业机器人表面和内部&#xff0c;并使用合适的润滑油进行润…

VRRP协议-负载分担配置【分别在路由器与交换机上配置】

VRRP在路由器与交换机上的不同配置 一、使用路由器实现负载分担二、使用交换机实现负载分担一、使用路由器实现负载分担 使用R1与R2两台设备分别进行VRRP备份组 VRRP备份组1,虚拟pc1的网关地址10.1.1.254 VRRP备份组2,虚拟pc2的网关地址10.1.1.253 ①备份组1的vrid=1,vrip=…

vue3中使用cherry-markdown

附cherry-markdown官网及api使用示例 官网:https://github.com/Tencent/cherry-markdown/blob/main/README.CN.md api:Cherry Markdown API 考虑到复用性,我在插件的基础上做了二次封装,步骤如下: 1.下载 (一定要指定版本0.8.22,否则会报错: [vitel Internal server e…

初识指针(5)<C语言>

前言 在前几篇文章中&#xff0c;已经介绍了指针一些基本概念、用途和一些不同类型的指针&#xff0c;下文将介绍某些指针类型的运用。本文主要介绍函数指针数组、转移表&#xff08;函数指针的用途&#xff09;、回调函数、qsort使用举例等。 函数指针数组 函数指针数组即每个…

深度学习知识点全面总结

ChatGPT 深度学习是一种使用神经网络来模拟人脑处理数据和创建模式的机器学习方法。下面是深度学习的一些主要知识点的总结&#xff1a; 1. 神经网络基础&#xff1a; - 神经元&#xff1a;基本的计算单元&#xff0c;模拟人脑神经元。 - 激活函数&#xff1a;用于增加神…

【CSP CCF记录】数组推导

题目 过程 思路 每次输入一个Bi即可确定一个Ai值&#xff0c;用temp记录1~B[i-1]&#xff0c;的最大值分为两种情况&#xff1a; 当temp不等于Bi时&#xff0c;则说明Bi值之前未出现过&#xff0c;Ai必须等于Bi才能满足Bi是Ai前缀最大的定义。当temp等于Bi时&#xff0c;则说…

SpringAMQP-消息转换器

这边发送消息接收消息默认是jdk的序列化方式&#xff0c;发送到服务器是以字节码的形式&#xff0c;我们看不懂也很占内存&#xff0c;所以我们要手动设置一下 我这边设置成json的序列化方式&#xff0c;注意发送方和接收方的序列化方式要保持一致 不然回报错。 引入依赖&#…

重磅推出:135届广交会采购商名录,囊括28个行业数据!

5.5日&#xff0c;第135届中国进出口商品交易会&#xff08;简称广交会&#xff09;在广州圆满闭幕&#xff0c;这一全球贸易盛典再次展现了中国制造的卓越实力和文化魅力&#xff0c;成就斐然&#xff0c;吸引了全球目光。 本届广交会线下出口成交额达247亿美元&#xff0c;对…

项目-坦克大战-让坦克动起来

为什么写这个项目 好玩涉及到java各个方面的技术 1&#xff0c;java面向对象 2&#xff0c;多线程 3&#xff0c;文件i/o操作 4&#xff0c;数据库巩固知识 java绘图坐标体系 坐标体系-介绍 坐标体系-像素 计算机在屏幕上显示的内容都是由屏幕上的每一个像素组成的像素是一…

力扣HOT100 - 70. 爬楼梯

解题思路&#xff1a; 动态规划 注意 if 判断和 for 循环 class Solution {public int climbStairs(int n) {if (n < 2) return n;int[] dp new int[n 1];dp[1] 1;dp[2] 2;for (int i 3; i < n; i) {dp[i] dp[i - 1] dp[i - 2];}return dp[n];} }

品牌设计理念和logo设计方法

一 品牌设计的目的 设计是为了传播&#xff0c;让传播速度更快&#xff0c;传播效率更高&#xff0c;减少宣传成本 二 什么是好的品牌设计 好的设计是为了让消费者更容易看懂、记住的设计&#xff0c; 从而辅助传播&#xff0c; 即 看得懂、记得住。 1 看得懂 就是让别人看懂…

树莓派|采集视频并实时显示画面

1、使用SSH远程连接到树莓派 2、新建存放代码的目录 mkdir /home/pi/my_code_directory 3、进入存放代码的目录 cd /home/pi/my_code_directory 4、新建py文件 nano cv2test.py 5、输入代码 import cv2# 打开摄像头 cap cv2.VideoCapture(0)while True:# 读取视频帧ret…

BGP学习二:BGP通告原则,BGP反射器,BGP路径属性细致讲解,新手小白无负担

目录 一.AS号 二.BGP路由生成 1.network 2.import-route引入 三.BGP通告原则 1.只发布最优且有效的路由 2.从EBGP获取的路由&#xff0c;会发布给所有对等体 3.水平分割原则 4.IBGP学习BGP默认不发送给EBGP&#xff0c;但如果也从IGP学习到了这条路由&#xff0c;就发…

java项目之智慧图书管理系统设计与实现(springboot+vue+mysql)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的智慧图书管理系统设计与实现。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 智慧图书管理…