Spring解決泛型擦除的思路不錯(cuò),現(xiàn)在它是我的了
你好呀,我是歪歪。
Spring 的事件監(jiān)聽機(jī)制,不知道你有沒有用過,實(shí)際開發(fā)過程中用來進(jìn)行代碼解耦簡直不要太爽。
但是我最近碰到了一個(gè)涉及到泛型的場景,常規(guī)套路下,在這個(gè)場景中使用該機(jī)制看起來會(huì)很傻,但是最終了解到 Spring 有一個(gè)優(yōu)雅的解決方案,然后去了解了一下,感覺有點(diǎn)意思。
和你一起盤一盤。
Demo
首先,第一步啥也別說,先搞一個(gè) Demo 出來。
需求也很簡單,假設(shè)我們有一個(gè) Person 表,每當(dāng) Person 表新增或者修改一條數(shù)據(jù)的時(shí)候,給指定服務(wù)同步一下。
偽代碼非常的簡單:
boolean success = addPerson(person)
if(success){
//發(fā)送person,add代表新增
sendToServer(person,"add");
}
這代碼能用,完全沒有任何問題。
但是,你仔細(xì)想,“發(fā)給指定服務(wù)同步一下”這樣的動(dòng)作按理來說,不應(yīng)該和用戶新增和更新的行為“耦合”在一起,他們應(yīng)該是兩個(gè)獨(dú)立的邏輯。
所以從優(yōu)雅實(shí)現(xiàn)的角度出發(fā),我們可以用 Spring 的事件機(jī)制進(jìn)行解耦。
比如改成這樣:
boolean success = addPerson(person)
if(success){
publicAddPersonEvent(person,"add");
}
addPerson 成功之后,直接發(fā)布一個(gè)事件出去,然后“發(fā)給指定服務(wù)同步一下”這件事情就可以放在事件監(jiān)聽器去做。
對(duì)應(yīng)的代碼也很簡單,新建一個(gè) SpringBoot 工程。
首先我們先搞一個(gè) Person 對(duì)象:
@Data
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
由于我們還要告知是新增還是修改,所以還需要搞個(gè)對(duì)象封裝一層:
@Data
public class PersonEvent {
private Person person;
private String addOrUpdate;
public PersonEvent(Person person, String addOrUpdate) {
this.person = person;
this.addOrUpdate = addOrUpdate;
}
}
然后搞一個(gè)事件發(fā)布器:
@Slf4j
@RestController
public class TestController {
@Resource
private ApplicationContext applicationContext;
@GetMapping("/publishEvent")
public void publishEvent() {
applicationContext.publishEvent(new PersonEvent(new Person("why"), "add"));
}
}
最后來一個(gè)監(jiān)聽器:
@Slf4j
@Component
public class EventListenerService {
@EventListener
public void handlePersonEvent(PersonEvent personEvent) {
log.info("監(jiān)聽到PersonEvent: {}", personEvent);
}
}
Demo 就算是齊活了,你把代碼粘過去,也用不了一分鐘吧。
啟動(dòng)服務(wù)跑一把:
圖片
看起來沒有任何毛病,在監(jiān)聽器里面直接就監(jiān)聽到了。
這個(gè)時(shí)候假設(shè),我還有一個(gè)對(duì)象,叫做 Order,每當(dāng) Order 表新增或者修改一條數(shù)據(jù)的時(shí)候,也要給指定服務(wù)同步一下。
怎么辦?
這還不簡單?
照葫蘆畫瓢唄。
先來一個(gè) Order 對(duì)象:
@Data
public class Order {
private String orderName;
public Order(String orderName) {
this.orderName = orderName;
}
}
再來一個(gè) OrderEvent 封裝一層:
@Data
public class OrderEvent {
private Order order;
private String addOrUpdate;
public OrderEvent(Order order, String addOrUpdate) {
this.order = order;
this.addOrUpdate = addOrUpdate;
}
}
然后再發(fā)布一個(gè)對(duì)應(yīng)的事件:
圖片
新增一個(gè)對(duì)應(yīng)的事件監(jiān)聽:
圖片
發(fā)起調(diào)用:
圖片
完美,兩個(gè)事件都監(jiān)聽到了。
那么問題又來了,假設(shè)我還有一個(gè)對(duì)象,叫做 Account,每當(dāng) Account 表新增或者修改一條數(shù)據(jù)的時(shí)候,也要給指定服務(wù)同步一下。
或者說,我有幾十張表,對(duì)應(yīng)幾十個(gè)對(duì)象,都要做類似的同步。
請(qǐng)問閣下又該如何應(yīng)對(duì)?
你當(dāng)然可以按照前面處理 Order 的方式,繼續(xù)依葫蘆畫瓢。
但是這樣勢必會(huì)來帶的一個(gè)問題是對(duì)象的膨脹,你想啊,畢竟每一個(gè)對(duì)象都需要一個(gè)對(duì)應(yīng)的 xxxxEvent 封裝對(duì)象。
這樣的代碼過于冗余,丑,不優(yōu)雅。
怎么辦?
自然而然的我們能想到泛型,畢竟人家干這個(gè)事兒是專業(yè)的,放一個(gè)通配符,管你多少個(gè)對(duì)象,通通都是“T”,也就是這樣的:
@Data
class BaseEvent<T> {
private T data;
private String addOrUpdate;
public BaseEvent(T data, String addOrUpdate) {
this.data = data;
this.addOrUpdate = addOrUpdate;
}
}
對(duì)應(yīng)的事件發(fā)布的地方也可以用 BaseEvent 來代替:
圖片
這樣用一個(gè) BaseEvent就能代替無數(shù)的 xxxEvent,做到通用,這是它的好處。
同時(shí)對(duì)應(yīng)的監(jiān)聽器也需要修改:
圖片
啟動(dòng)服務(wù),跑一把。
發(fā)起調(diào)用之后你會(huì)發(fā)現(xiàn)控制臺(tái)正常輸出:
圖片
但是,注意我要說但是了。
但是監(jiān)聽這一坨代碼我感覺不爽,全部都寫在一個(gè)方法里面了,需要用非常多的 if 分支去做判斷。
而且,假設(shè)某些對(duì)象在同步之前,還有一些個(gè)性化的加工需求,那么都會(huì)體現(xiàn)在這一坨代碼中,不夠優(yōu)雅。
怎么辦呢?
很簡單,拆開監(jiān)聽:
圖片
但是再次重啟服務(wù),發(fā)起調(diào)用你會(huì)發(fā)現(xiàn):控制臺(tái)沒有輸出了?怎么回事,怎么監(jiān)聽不到了呢?
圖片
官網(wǎng)怎么說?
在 Spring 的官方文檔中,關(guān)于泛型類型的事件通知只有寥寥數(shù)語,但是提到了兩個(gè)解決方案:
https://docs.spring.io/spring-framework/reference/core/beans/context-introduction.html#context-functionality-events-generics
圖片
首先官網(wǎng)給出了這樣的一個(gè)泛型對(duì)象:EntityCreatedEvent
然后說比如我們要監(jiān)聽 Person 這個(gè)對(duì)象創(chuàng)建時(shí)的事件,那么對(duì)應(yīng)的監(jiān)聽器代碼就是這樣的:
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
// ...
}
和我們 Demo 里面的代碼結(jié)構(gòu)是一樣的。
那么怎么才能觸發(fā)這個(gè)監(jiān)聽呢?
第一種方式是:
class PersonCreatedEvent extends EntityCreatedEvent<Person> { … }).
也就是給這個(gè)對(duì)象創(chuàng)造一個(gè)對(duì)應(yīng)的 xxxCreatedEvent,然后去監(jiān)聽這個(gè) xxxCreatedEvent。
和我們前面提到的 xxxxEvent 封裝對(duì)象是一回事。
為什么我們必須要這樣做呢?
官網(wǎng)上提到了這幾個(gè)詞:
Due to type erasure
圖片
type erasure,泛型擦除。
因?yàn)榉盒筒脸詫?dǎo)致直接監(jiān)聽 EntityCreatedEvent事件是不生效的,因?yàn)樵诜盒筒脸?,EntityCreatedEvent變成了 EntityCreatedEvent<?>。
封裝一個(gè)對(duì)象繼承泛型對(duì)象,通過他們之間一一對(duì)應(yīng)的關(guān)系從而繞開泛型擦除這個(gè)問題,這個(gè)方案確實(shí)是可以解決問題。
但是,前面說了,不夠優(yōu)雅。
官網(wǎng)也覺得這個(gè)事情很傻:
圖片
它怎么說的呢?
In certain circumstances, this may become quite tedious if all events follow the same structure.在某些情況下,如果所有事件都遵循相同的結(jié)構(gòu),這可能會(huì)變得相當(dāng) tedious。
好,那么 tedious,是什么意思?哪個(gè)同學(xué)舉手回答一下?
這是個(gè)四級(jí)詞匯,得認(rèn)識(shí),以后考試的時(shí)候要考:
圖片
quite tedious,相當(dāng)啰嗦。
我們都不希望自己的程序看起來是 tedious 的。
所以,官方給出了另外一個(gè)解決方案: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;
}
@Override
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getData()));
}
}
再次啟動(dòng)服務(wù),你會(huì)發(fā)現(xiàn),監(jiān)聽器又好使了:
圖片
那么問題又來了。
這是為什么呢?
為什么?
我也不知道為什么,但是我知道源碼之下無秘密。
所以,先打上斷點(diǎn)再說。
關(guān)于 @EventListener 注解的原理和源碼解析,我之前寫過一篇相關(guān)的文章:《扯下@EventListener這個(gè)注解的神秘面紗?!?/p>
有興趣的可以看看這篇文章,然后再試著按照文章中的方式去找對(duì)應(yīng)的源碼。
我這篇文章就不去抽絲剝繭的一點(diǎn)點(diǎn)找源碼了,直接就是一個(gè)大力出奇跡。
因?yàn)槲覀円阎?ResolvableTypeProvider 這個(gè)接口在搞事情,所以我只需要看看這個(gè)接口在代碼中被使用的地方有哪些:
圖片
除去一些注釋和包導(dǎo)入的地方,整個(gè)項(xiàng)目中只有 ResolvableType 和 MultipartHttpMessageWriter 這個(gè)兩個(gè)中用到了。
直覺告訴我,應(yīng)該是在 ResolvableType 用到的地方打斷點(diǎn),因?yàn)榱硗庖粋€(gè)類看起來是 Http 相關(guān)的,和我的 Demo 沒啥關(guān)系。
所以我直接在這里打上斷點(diǎn),然后發(fā)起調(diào)用,程序果然就停在了斷點(diǎn)處:
org.springframework.core.ResolvableType#forInstance
圖片
我們觀察一下,發(fā)現(xiàn)這幾行代碼核心就干一個(gè)事兒:判斷 instance 是不是 ResolvableTypeProvider 的子類。
如果是則返回一個(gè) type,如果不是則返回 forClass(instance.getClass())。
通過 Debug 我們發(fā)現(xiàn) instance 是 BaseEvent:
圖片
巧了,這就是 ResolvableTypeProvider 的子類,所以返回的 type 是這樣式兒的:
com.example.elasticjobtest.BaseEvent<com.example.elasticjobtest.Person>
圖片
是帶具體的類型的,而這個(gè)類型就是通過 getResolvableType 方法拿到的。
前面我們?cè)趯?shí)現(xiàn) ResolvableTypeProvider 的時(shí)候,就重寫了 getResolvableType 方法,調(diào)用了 ResolvableType.forClassWithGenerics,然后用 data 對(duì)應(yīng)的真正的 T 對(duì)象實(shí)例的類型,作為返回值,這樣泛型對(duì)應(yīng)的真正的對(duì)象類型,就在運(yùn)行期被動(dòng)態(tài)的獲取到了,從而解決了編譯階段泛型擦除的問題。
如果沒有實(shí)現(xiàn) ResolvableTypeProvider 接口,那么這個(gè)方法返回的就是 BaseEvent<?>:
com.example.elasticjobtest.BaseEvent<?>
圖片
看到這里你也就猜到個(gè)七七八八了。
都已經(jīng)拿到具體的泛型對(duì)象了,后面再發(fā)起對(duì)應(yīng)的事件監(jiān)聽,那不是順理成章的事情嗎?
好,現(xiàn)在你在第一個(gè)斷點(diǎn)處就收獲到了一個(gè)這么關(guān)鍵的信息,接下來怎么辦呢?
接著斷點(diǎn)處往下調(diào)試,然后把整個(gè)鏈路都梳理清楚唄。
再往下走,你會(huì)來到這個(gè)地方:
org.springframework.context.event.AbstractApplicationEventMulticaster#getApplicationListeners
圖片
從 cache 里面獲取到了一個(gè) null。
因?yàn)檫@個(gè)緩存里面放的就是在項(xiàng)目啟動(dòng)過程中已經(jīng)觸發(fā)過的框架自帶的 listener 對(duì)象:
圖片
調(diào)用的時(shí)候,如果能從緩存中拿到對(duì)應(yīng)的 listener,則直接返回。而我們 Demo 中的自定義 listener 是第一次觸發(fā),所以肯定是沒有的。
因此關(guān)鍵邏輯就這個(gè)方法的最后一行:retrieveApplicationListeners 方法里面
org.springframework.context.event.AbstractApplicationEventMulticaster#retrieveApplicationListeners
圖片
這個(gè)地方再往下寫,就是我前面我提到的這篇文章中我寫過的內(nèi)容了《扯下@EventListener這個(gè)注解的神秘面紗。》。
和泛型擦除的關(guān)系已經(jīng)不大了,我就不再寫一次了。
只是給大家看一下這個(gè)方法在我們的 Demo 中,最終返回的 allListeners 就是我們自定義的這個(gè)事件監(jiān)聽器:
com.example.elasticjobtest.EventListenerService#handlePersonEvent
圖片
為什么是這個(gè)?
因?yàn)槲耶?dāng)前發(fā)布的事件的主角就是 Person 對(duì)象:
圖片
同理,當(dāng) Order 對(duì)象的事件過來的時(shí)候,這里肯定就是對(duì)應(yīng)的 handleOrderEvent 方法:
圖片
如果我們把 BaseEvent 的 ResolvableTypeProvider 接口拿掉,那么你再看對(duì)應(yīng)的 allListeners,你就會(huì)發(fā)現(xiàn)找不到我們對(duì)應(yīng)的自定義 Listener 了:
圖片
為什么?
因?yàn)楫?dāng)前事件對(duì)應(yīng)的 ResolvableType 是這樣的:
org.springframework.context.PayloadApplicationEvent<com.example.elasticjobtest.BaseEvent<?>>
圖片
而我們并沒有自定義一個(gè)這樣的 Listener:
@EventListener
public void handleAllEvent(BaseEvent<?> orderEvent) {
log.info("監(jiān)聽到Event: {}", orderEvent);
}
所以,這個(gè)事件發(fā)布了,但是沒有對(duì)應(yīng)的消費(fèi)。
大概就是這么個(gè)意思。
核心邏輯就在 ResolvableTypeProvider 接口里面,重寫了 getResolvableType 方法,在運(yùn)行期動(dòng)態(tài)的獲取泛型對(duì)應(yīng)的真正的對(duì)象類型,從而解決了編譯階段泛型擦除的問題。
很好,現(xiàn)在摸清楚了,是個(gè)很簡單的思路,之前是 Spring 的,現(xiàn)在它是我的了。
為什么需要發(fā)布訂閱模式 ?
既然寫到 Spring 的事件通知機(jī)制了,那么就順便聊聊這個(gè)發(fā)布訂閱模式。
也許在看的過程中,你會(huì)冒出這樣一個(gè)問題:為什么要搞這么麻煩?把這些事件監(jiān)聽的業(yè)務(wù)邏輯直接寫在對(duì)應(yīng)的數(shù)據(jù)庫操作語句之后不行么?
要回答這個(gè)問題,我們可以先總結(jié)一下事件通知機(jī)制的使用場景。
- 數(shù)據(jù)變化之后同步清除緩存,這是一種簡單可靠的緩存更新方式。只有在清除失敗,或者數(shù)據(jù)庫主從同步間隙被臟讀才有可能出現(xiàn)緩存臟數(shù)據(jù),概率比較小,一般業(yè)務(wù)上也是可以接受的。
- 通過某種方式告訴下游系統(tǒng)數(shù)據(jù)變化,比如往消息隊(duì)列里面扔消息。
- 數(shù)據(jù)的統(tǒng)計(jì)、監(jiān)控、異步觸發(fā)等場景。當(dāng)然這動(dòng)作似乎用 AOP 也可以做,但是實(shí)際上在某些業(yè)務(wù)場景下,做切面統(tǒng)計(jì),反而沒有通過發(fā)布訂閱機(jī)制來得直接,靈活度也更好。
除了上面這些外,肯定還有一些其他的場景,但是這些場景都有一個(gè)共同點(diǎn):與核心業(yè)務(wù)關(guān)系不大,但是又具備一定的普適性。
比如完成用戶注冊(cè)之后給用戶發(fā)一個(gè)短信,或者發(fā)個(gè)郵件啥的。這個(gè)事情用發(fā)布訂閱機(jī)制來做是再合適不過的了。
編碼過程中牢記單一職責(zé)原則,要知道一個(gè)類該干什么不該干什么,這是面向?qū)ο缶幊?的關(guān)鍵點(diǎn)之一。
當(dāng)你一個(gè)類中注入了大量的 Service 的時(shí)候,你就要考慮考慮,是不是有什么做的不合適的地方了,是不是有些 Service 其實(shí)不應(yīng)該注入進(jìn)來的。
是不是該用用發(fā)布訂閱了?
另外,當(dāng)你的項(xiàng)目中真的出現(xiàn)了文章最開始說的,各種各樣的 xxxEvent 事件對(duì)應(yīng)的封裝的時(shí)候,任何一個(gè)來開發(fā)的人都覺得這樣寫是不是有點(diǎn)冗余的時(shí)候,你就應(yīng)該考慮一下是不是有更加優(yōu)雅的解決方案。
假設(shè)這個(gè)方案由于某些原因不能使用或者不敢使用是一回事。
但是知不知道這個(gè)方案,是另一回事。
好啦,本文的技術(shù)部分就到這里了。