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.xdnf.cn/news/1421672.html

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

相关文章

CoSeg: Cognitively Inspired Unsupervised Generic Event Segmentation

名词解释 1.特征重建 特征重建是一种机器学习中常用的技术&#xff0c;通常用于自监督学习或无监督学习任务。在特征重建中&#xff0c;模型被要求将输入数据经过编码器&#xff08;encoder&#xff09;转换成某种表示&#xff0c;然后再经过解码器&#xff08;decoder&#x…

中学数学研究杂志中学数学研究杂志社中学数学研究编辑部2024年第4期目录

教学纵横 高中数学选择性必修课程函数主线分析 柳双;吴立宝; 1-4 贯彻新课程理念 促学习能力提升——以“三角函数诱导公式”教学为例 陆雨轩; 4-6《中学数学研究》投稿&#xff1a;cn7kantougao163.com 对高中数学新课标教材新增知识点的价值分析 钱伟风;刘瑞美; …

商务分析方法与工具(十):Python的趣味快捷-公司财务数据最炫酷可视化

Tips&#xff1a;"分享是快乐的源泉&#x1f4a7;&#xff0c;在我的博客里&#xff0c;不仅有知识的海洋&#x1f30a;&#xff0c;还有满满的正能量加持&#x1f4aa;&#xff0c;快来和我一起分享这份快乐吧&#x1f60a;&#xff01; 喜欢我的博客的话&#xff0c;记得…

C# WinForm —— 18 NumericUpDown 介绍

1. 简介 数字显示框&#xff0c;通过向上、向下按钮来 增加/减小 显示的数值 2. 常用属性 属性解释(Name)控件ID&#xff0c;在代码里引用的时候会用到,一般以 numUD 开头Hexadecimal数值 up-down 控件的值是否应以十六进制显示Increment每单击一下按钮&#xff0c;增加或减…

c语言实现十进制(整数,小数)转N进制

文章目录 先来说一下整数转N进制小数转N进制栈和队列代码地址← 今天实现了c语言整数和小数转换为对应的N进制 先来说一下整数转N进制 我们只需要不断的取模然后判断num/N是否等于0就可以了,同时我们还要保存每一组的余数 这里我们的余数是从下往上输出的,是不是就相当于后算出…

纯血鸿蒙APP第三方库——MpChart运动健康场景实践案例

介绍 MpChart是一个包含各种类型图表的图表库&#xff0c;主要用于业务数据汇总&#xff0c;例如销售数据走势图&#xff0c;股价走势图等场景中使用&#xff0c;方便开发者快速实现图表UI&#xff0c;MpChart主要包括线形图、柱状图、饼状图、蜡烛图、气泡图、雷达图、瀑布图…

单链表经典算法OJ题---力扣21

1.链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09;【点击即可跳转】 思路&#xff1a;创建新的空链表&#xff0c;遍历原链表。将节点值小的节点拿到新链表中进行尾插操作 遍历的结果只有两种情况&#xff1a;n1为空 或 n2为空 注意&#xff1a;链表为空的情况 代…

C++干货--引用

前言&#xff1a; C的引用&#xff0c;是学习C的重点之一&#xff0c;它与指针的作用有重叠的部分&#xff0c;但是它绝不是完全取代指针(后面我们也会简单的分析)。 引用的概念&#xff1a; 引用 不是新定义一个变量 &#xff0c;而 是给已存在变量取了一个别名 &#xf…

大数据在IT行业的应用与发展趋势及IT行业的现状与未来

大数据在IT行业中的应用、发展趋势及IT行业的现状与未来 一、引言 随着科技的飞速发展&#xff0c;大数据已经成为IT行业的重要驱动力。从数据收集、存储、处理到分析&#xff0c;大数据技术为各行各业带来了深远的影响。本文将详细探讨大数据在IT行业中的应用、发展趋势&#…

24年做抖音小店,你还停留在数据?别人都已经开始注重利润了

大家好&#xff0c;我是电商笨笨熊 一件事情持续做&#xff0c;一个项目持续深耕&#xff0c;意义到底是什么&#xff1f; 这句话我常常说&#xff0c;但很多人似乎走偏了实际意义&#xff1b; 尤其对于新手来说&#xff0c;做抖音小店总是向往某某老玩家多么牛的数据&#…

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

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

SMB/RPC协议分析之-命名/匿名管道pipe

在前面的文章中&#xff0c;介绍了SMB协议共享相关的内容&#xff0c;详见我的专栏《网络攻防协议实战分析》&#xff0c;连接这里。在SMB协议中往往需要连接到对应的远程管道&#xff0c;如果你经常接触到SMB协议&#xff0c;相信你对于lsass&#xff0c;svcctl等多种命名管道…

558、Vue 3 学习笔记 -【常用Composition API(七)】 2024.05.13

目录 一、Composition API的优势1. Options API存在的问题2. Composition API的优势 二、 新的组件1. Fragment2. Teleport3. Suspense 三、其他1. 全局API的转移2. 其他改变 四、参考链接 一、Composition API的优势 1. Options API存在的问题 使用传统OptionsAPI中&#xf…

Android 老年模式功能 放大字体

1 配置属性 <attr name"text_size_16" format"dimension"/><attr name"text_size_18" format"dimension"/><attr name"text_size_14" format"dimension"/><attr name"text_size_12&quo…

朋友在阿里测试岗当HR,给我整理的面试题文档

以下是软件测试相关的面试题及答案&#xff0c;欢迎大家参考! 1、你的测试职业发展是什么? 测试经验越多&#xff0c;测试能力越高。所以我的职业发展是需要时间积累的&#xff0c;一步步向着高级测试工程师奔去。而且我也有初步的职业规划&#xff0c;前3年积累测试经验&…

MySQL5.7压缩包安装图文教程

一、下载 https://dev.mysql.com/downloads/mysql/ 选择5.7版本 二、解压 下载完成后解压&#xff0c;解压后如下&#xff08;zip是免安装的&#xff0c;解压后配置成功即可使用&#xff09; 注意&#xff1a;只有5.6以前的版本才有在线安装&#xff08;install msi&#xf…

MATLAB | 最新版MATLAB绘图速查表来啦!!

之前看大佬Pjer做的MATLAB速查表 http://home.ustc.edu.cn/~pjer1316/matlabplot/ 感觉非常的实用&#xff0c;最近几次MATLAB更新围绕画图方面也有很多新东西&#xff0c;于是就有了自己做一张最新版的速查表的想法&#xff0c;这张表长这样&#xff1a; 这张表的配色基本上…

Unicode字符集和UTF编码

文章目录 前言一、字符集和编码方式二、unicode字符集utf32编码utf8编码utf8编码函数示例utf8解码函数示例 utf16编码utf16编码解码函数示例 总结 前言 本文详细介绍 u n i c o d e unicode unicode 字符集和其相关的三种编码方式&#xff1a; u t f 8 utf8 utf8&#xff0c;…

In Context Learning(ICL)个人记录

In Context Learning&#xff08;ICL&#xff09;简介 In Context Learning&#xff08;ICL&#xff09;的关键思想是从类比中学习。上图给出了一个描述语言模型如何使用 ICL 进行决策的例子。首先&#xff0c;ICL 需要一些示例来形成一个演示上下文。这些示例通常是用自然语言…

如何在群晖NAS中开启FTP并实现使用公网地址远程访问传输文件

文章目录 1. 群晖安装Cpolar2. 创建FTP公网地址3. 开启群晖FTP服务4. 群晖FTP远程连接5. 固定FTP公网地址6. 固定FTP地址连接 本文主要介绍如何在群晖NAS中开启FTP服务并结合cpolar内网穿透工具&#xff0c;实现使用固定公网地址远程访问群晖FTP服务实现文件上传下载。 Cpolar内…