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 {

    @Resource
    private ApplicationContext applicationContext;

    @GetMapping("/publishEvent")
    public void publishEvent() {
        applicationContext.publishEvent(new PersonEvent(new Person("why"), "add"));
    }
}

最後來一個監聽器:

@Slf4j
@Component
public class EventListenerService {

    @EventListener
    public 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;
    }

    @Override
    public 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 事件對應的封裝的時候,任何一個來開發的人都覺得這樣寫是不是有點冗餘的時候,你就應該考慮一下是不是有更加優雅的解決方案。

假設這個方案由於某些原因不能使用或者不敢使用是一回事。

但是知不知道這個方案,是另一回事。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章