自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Spring解決泛型擦除的思路不錯(cuò),現(xiàn)在它是我的了

開發(fā) 前端
當(dāng)你一個(gè)類中注入了大量的 Service 的時(shí)候,你就要考慮考慮,是不是有什么做的不合適的地方了,是不是有些 Service 其實(shí)不應(yīng)該注入進(jì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ī)制的使用場景。

  1. 數(shù)據(jù)變化之后同步清除緩存,這是一種簡單可靠的緩存更新方式。只有在清除失敗,或者數(shù)據(jù)庫主從同步間隙被臟讀才有可能出現(xiàn)緩存臟數(shù)據(jù),概率比較小,一般業(yè)務(wù)上也是可以接受的。
  2. 通過某種方式告訴下游系統(tǒng)數(shù)據(jù)變化,比如往消息隊(duì)列里面扔消息。
  3. 數(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ù)部分就到這里了。

責(zé)任編輯:武曉燕 來源: why技術(shù)
相關(guān)推薦

2021-07-29 09:20:18

Java泛型String

2024-05-11 14:45:23

MAX基礎(chǔ)服務(wù)C端

2023-03-06 08:33:24

IDEA反編譯類型

2019-09-04 00:20:10

JSON泛型擦除

2021-07-01 06:47:30

Java泛型泛型擦除

2020-12-21 16:18:07

JavaTypeToken泛型擦除

2024-06-07 10:05:31

2021-08-24 08:05:41

泛型類型擦除Class

2022-03-02 14:41:03

泛型反序列化

2021-12-01 08:29:17

Go泛型Maps

2021-09-29 18:17:30

Go泛型語言

2023-11-29 08:19:45

Go泛型缺陷

2023-09-17 23:16:46

緩存數(shù)據(jù)庫

2025-01-13 07:00:00

Java泛型編程

2023-11-08 08:27:30

泛型Java

2013-10-09 09:39:17

開源

2025-01-15 10:44:55

Go泛型接口

2021-06-17 06:51:32

Java泛型Java編程

2009-09-09 14:11:58

Scala泛型

2017-03-06 16:51:52

Java泛型實(shí)現(xiàn)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)