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.pswp.cn/web/11339.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/11339.shtml
英文地址,請注明出處:http://en.pswp.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 消除抖動 一、什么是定時器? …

怎么解決ModuleNotFoundError: No module named ‘httpx_sse‘

解決方案 pip install httpx_sseLooking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple Collecting httpx_sse Downloading https://pypi.tuna.tsinghua.edu.cn/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-n…

Android 14.0 frameworks添加自定義服務

1.概述 在14.0的系統rom定制化產品開發中,對于提供系統接口來給app調用,來控制系統的某些功能,所以需要添加自定義服務也是常有功能,因此需要來在frameworks層中添加自定義系統服務的功能 2.frameworks添加自定義服務的核心類 frameworks\base\services\java\com\android…

Midjourney Imagine API 申請及使用

Midjourney Imagine API 申請及使用 申請流程 要使用 Midjourney Imagine API&#xff0c;首先可以到 Midjourney Imagine API 頁面點擊「Acquire」按鈕&#xff0c;獲取請求所需要的憑證&#xff1a; 如果你尚未登錄或注冊&#xff0c;會自動跳轉到登錄頁面邀請您來注冊和登…

多線程【LeetCode】

多線程【LeetCode】 前言前言推薦多線程信號量1114.按序打印1115.交替打印FooBar1116.打印零與奇偶數1117.H2O生成1188.設計有限阻塞隊列Plus1195.交替打印字符串1226.哲學家進餐 最后 前言 這是陳舊已久的草稿2022-11-27 20:44:17 這個是刷算法&#xff0c;也是準備寒假實習…

語音轉文字服務的調用接口

語音轉文字&#xff08;Speech-to-Text&#xff0c;STT&#xff09;技術允許將口語化的語音轉換成書面文字。以下是一些提供語音轉文字服務的調用接口及其特點。北京木奇移動技術有限公司&#xff0c;專業的軟件外包開發公司&#xff0c;歡迎交流合作。 1.訊飛開放平臺語音轉寫…

[貓頭虎分享21天微信小程序基礎入門教程]第1天:微信小程序概述與開發環境搭建教程

第1天&#xff1a;微信小程序概述與開發環境搭建 &#x1f63a; 文章目錄 第1天&#xff1a;微信小程序概述與開發環境搭建 &#x1f63a;自我介紹微信小程序概述特點 開發環境搭建步驟1: 注冊微信小程序賬號步驟2: 安裝開發者工具步驟3: 熟悉開發者工具界面 今日學習總結小測試…

UnityDOTS備忘

Unity DOTS中創建一個AssetBundle并將其用作Entity 創建一個新的Unity項目&#xff0c;并確保已啟用DOTS功能。 創建一個AssetBundle&#xff0c;可以通過在Project視圖中右鍵單擊文件夾并選擇“Create > AssetBundle”來創建。 將您想要轉換為Entity的資源&#xff08;例…

炒股開戶傭金最低萬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=…

修正牛頓法求解無約束問題

function [x,val,k]revisenm(fun,gfun, Hess, x0) %功能:用修正牛頓法求解無約束問題:min f(x) %輸入:x0是初始點&#xff0c;fun, gfun,Hess分別是求目標函數值&#xff0c;梯度&#xff0c;Hesse矩陣的函數 %輸出:x,val分別是近似最優點和最優值&#xff0c;k是迭代次數 nl…

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繪圖坐標體系 坐標體系-介紹 坐標體系-像素 計算機在屏幕上顯示的內容都是由屏幕上的每一個像素組成的像素是一…