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

“解耦神器”之SpringEvents領(lǐng)域事件

開(kāi)發(fā) 前端
領(lǐng)域事件是一種用于表示領(lǐng)域模型中發(fā)生的重要事件的機(jī)制。它們用于通知其他相關(guān)的聚合或服務(wù),以便它們可以采取相應(yīng)的行動(dòng)。

大家好,我是Jensen。一個(gè)想和大家一起打怪升級(jí)的程序員朋友。

在DDD項(xiàng)目的落地過(guò)程中,除了聚合、模型等等重要概念,領(lǐng)域事件在其中扮演了一個(gè)非常重要的角色,它不僅能解耦領(lǐng)域?qū)优c其他層,作為“跳出”領(lǐng)域?qū)拥奶?,還是一種策略模式的高級(jí)用法。即便你的項(xiàng)目沒(méi)有DDD,領(lǐng)域事件在傳統(tǒng)的MVC分層架構(gòu)也大有妙用。

下面我們一起來(lái)解鎖這個(gè)“解耦神器”。

1.什么是領(lǐng)域事件

領(lǐng)域事件是一種用于表示領(lǐng)域模型中發(fā)生的重要事件的機(jī)制。它們用于通知其他相關(guān)的聚合或服務(wù),以便它們可以采取相應(yīng)的行動(dòng)。

領(lǐng)域事件通常由聚合根( Aggregate Root)發(fā)布。當(dāng)聚合根內(nèi)部發(fā)生重要的狀態(tài)更改時(shí),它會(huì)發(fā)布一個(gè)領(lǐng)域事件。其他聚合或服務(wù)可以訂閱這些事件,并在事件發(fā)生時(shí)采取相應(yīng)的行動(dòng)。

以下是使用領(lǐng)域事件的四大步:

  • 定義領(lǐng)域事件:領(lǐng)域事件是一個(gè)簡(jiǎn)單的對(duì)象,它包含事件的名稱(chēng)、發(fā)生時(shí)間和相關(guān)的數(shù)據(jù)。例如,一個(gè)訂單已完成的領(lǐng)域事件可能包含訂單的 ID 和完成時(shí)間。
  • 發(fā)布領(lǐng)域事件:當(dāng)聚合根內(nèi)部發(fā)生重要的狀態(tài)更改時(shí),它會(huì)發(fā)布一個(gè)領(lǐng)域事件。例如,當(dāng)訂單完成時(shí),訂單聚合根會(huì)發(fā)布一個(gè) OrderCompletedEvent 事件。
  • 訂閱領(lǐng)域事件:其他聚合或服務(wù)可以訂閱領(lǐng)域事件,并在事件發(fā)生時(shí)采取相應(yīng)的行動(dòng)。例如,一個(gè)訂單跟蹤服務(wù)可以訂閱 OrderCompletedEvent 事件,并在訂單完成時(shí)發(fā)送通知給客戶。
  • 處理領(lǐng)域事件:當(dāng)領(lǐng)域事件被發(fā)布時(shí),訂閱者會(huì)收到通知,并可以根據(jù)事件的數(shù)據(jù)采取相應(yīng)的行動(dòng)。例如,訂單跟蹤服務(wù)可以在收到 OrderCompletedEvent 事件時(shí)發(fā)送通知給客戶。

領(lǐng)域事件的使用可以幫助保持領(lǐng)域模型的解耦和一致性。通過(guò)使用領(lǐng)域事件,不同的聚合或服務(wù)可以獨(dú)立地處理事件,而不需要直接相互依賴。這有助于提高系統(tǒng)的可維護(hù)性和靈活性。

(以上內(nèi)容由豆包AI生成,描述還是蠻契合的,理由我就不過(guò)多掩飾了)

2.領(lǐng)域事件的定義、發(fā)布與訂閱

在DDD工程中,領(lǐng)域事件定義在領(lǐng)域?qū)?,具體來(lái)說(shuō)是放在領(lǐng)域契約下面,如:domain.contract.event,它不屬于某個(gè)聚合私有,由該系統(tǒng)下的所有聚合共享。

為什么要這樣劃分呢?

我認(rèn)為,領(lǐng)域事件不僅能在領(lǐng)域?qū)影l(fā)布,也可能在應(yīng)用層發(fā)布,甚至在接入層發(fā)布,而在領(lǐng)域聚合之外發(fā)布的事件,必然會(huì)存在跨聚合的事件屬性。

我舉個(gè)預(yù)約的場(chǎng)景:

工單中臺(tái)下的預(yù)約業(yè)務(wù)需要設(shè)計(jì)一個(gè)支付回調(diào)接口,由商城系統(tǒng)支付成功后進(jìn)行回調(diào),此時(shí)商城系統(tǒng)傳入的回調(diào)命令參數(shù)在處理完核心業(yè)務(wù)后(如設(shè)置預(yù)約單狀態(tài)為待服務(wù)),再發(fā)布支付回調(diào)成功事件,以執(zhí)行后續(xù)的非核心業(yè)務(wù)邏輯(比如提醒服務(wù)店員需要聯(lián)系客戶到店等等)。

工單中臺(tái)和商城系統(tǒng)已然進(jìn)行了服務(wù)拆分,工單中臺(tái)本身并不包含支付業(yè)務(wù),領(lǐng)域?qū)樱ㄈ珙I(lǐng)域服務(wù))并沒(méi)有發(fā)布這個(gè)支付回調(diào)成功的事件的入口,那么,發(fā)布領(lǐng)域事件的最佳位置是在應(yīng)用層。

至此,事件的定義、事件的發(fā)布已經(jīng)確定好了位置,但事件在哪里訂閱也有講究。

我在DDD落地過(guò)程中,曾多次調(diào)整領(lǐng)域事件訂閱的位置,有試過(guò)放在領(lǐng)域?qū)泳酆舷旅?,也有試過(guò)抽取到SDK工程里,最終在前段時(shí)間確定下來(lái)了,事件訂閱就放在應(yīng)用層的listener包下面,意為事件監(jiān)聽(tīng)器。

至于命名規(guī)則,需要看系統(tǒng)的復(fù)雜度,一般小而美的微服務(wù),以聚合Listener或以外部系統(tǒng)Listener命名足以,如工單中臺(tái)(WorkOrder)下的預(yù)約領(lǐng)域聚合(Appointment),其監(jiān)聽(tīng)器以AppointmentListener命名,訂單領(lǐng)域聚合(Order)是商城系統(tǒng)(如Mall)外部聚合,其監(jiān)聽(tīng)器以MallListener命名而非OrderListener。

特別強(qiáng)調(diào)一點(diǎn),在高內(nèi)聚的架構(gòu)設(shè)計(jì)中,外部系統(tǒng)的調(diào)用不會(huì)設(shè)計(jì)特別多,如果存在大量的跨系統(tǒng)交互,我們?cè)摲此家幌率遣皇俏⒎?wù)拆分得太細(xì)了,大量的外部系統(tǒng)調(diào)用會(huì)存在跨線程的分布式事務(wù)等問(wèn)題等。

當(dāng)然,隨著業(yè)務(wù)快速發(fā)展,系統(tǒng)復(fù)雜度隨之上升,事件監(jiān)聽(tīng)listener也可能跟著拆分,這時(shí)候我們的原則還是往大了拆,不宜拆得太細(xì)。

對(duì)于非DDD工程,可以考慮在根目錄定義一個(gè)event包,包括entity和listener:entity下定義領(lǐng)域事件,listener下定義領(lǐng)域事件監(jiān)聽(tīng)器,這樣一來(lái)我們寫(xiě)代碼就更加簡(jiǎn)單清晰。

3.領(lǐng)域事件解耦實(shí)戰(zhàn)

下圖是我在DDD工程落地的案例,我們要先約定好代碼放哪里才能更好地規(guī)劃后續(xù)的編碼工作。

上面所說(shuō)的領(lǐng)域事件,一直停留在概念層面,事件的發(fā)布訂閱只是設(shè)計(jì)模式,那具體要怎么實(shí)現(xiàn),才是核心技術(shù)。

發(fā)布訂閱有很多種實(shí)現(xiàn)方式,如Java自帶的觀察者模型java.util.Observer,事件驅(qū)動(dòng)模型java.util.EventListener,還有基于第三方跨線程的消息隊(duì)列模型(如Kafka、RabbitMQ、RocketMQ、Redis等),以及Spring的發(fā)布訂閱模型SpringEvents。

在這里,我認(rèn)為領(lǐng)域事件在工程內(nèi)部解耦即可,用不上第三方跨線程的MQ模型,所以我選了SpringEvents作為發(fā)布訂閱的底層實(shí)現(xiàn),而且Spring事件有個(gè)好處,它可以在Idea工具中鏈接消息發(fā)布和訂閱,對(duì)于編程還是非常友好的。

在系統(tǒng)內(nèi)部事件滿天飛的情況下,解耦完還能保證代碼可讀性,可謂是錦上添花。

SpringEvents的常規(guī)打開(kāi)方式:

  • 定義事件:定義一個(gè)事件類(lèi),該類(lèi)應(yīng)該繼承自ApplicationEvent類(lèi)。你可以在事件類(lèi)中添加任何需要的數(shù)據(jù),這些數(shù)據(jù)將在事件發(fā)布時(shí)傳遞給訂閱者。
  • 發(fā)布事件:使用ApplicationEventPublisher發(fā)布事件。你可以通過(guò)ApplicationContext獲取ApplicationEventPublisher實(shí)例,并使用其publishEvent方法發(fā)布事件。
  • 訂閱事件:使用@EventListener注解來(lái)訂閱事件。將@EventListener注解應(yīng)用于一個(gè)方法上,并指定要訂閱的事件類(lèi)型。該方法將在事件發(fā)布時(shí)被調(diào)用,并接收事件對(duì)象作為參數(shù)。

領(lǐng)域事件還要解決一個(gè)問(wèn)題,如果我們通過(guò)@Async+@EventListener實(shí)現(xiàn)異步監(jiān)聽(tīng),需要跨線程傳遞信息,那我們就要對(duì)領(lǐng)域事件做一層小小的封裝了。

首先,寫(xiě)一個(gè)領(lǐng)域事件抽象類(lèi),該類(lèi)由其他事件繼承:

public abstract class DomainEvent extends ApplicationEvent {
    // 本地線程變量池,用于存儲(chǔ)跨線程信息
    private final Map<String, Object> THREAD_LOCALS = ThreadContext.getValues();
    /**
     * 領(lǐng)域事件構(gòu)造器
     *
     * @param source 事件內(nèi)容
     * @param <T>    任意類(lèi)型
     */
    public <T> DomainEvent(T source) {
        super(source);
    }
    /**
     * 獲取事件內(nèi)容
     *
     * @param <T> 任意類(lèi)型
     * @return 事件內(nèi)容
     */
    public <T> T get() {
        ThreadContext.setValues(THREAD_LOCALS);
        return (T) super.getSource();
    }
    /**
     * 租戶判斷
     * 使用方式:監(jiān)聽(tīng)方法標(biāo)注@EventListener(condition = "#event.tenantIn('xxx', 'xxx')")
     *
     * @param tenantIds 指定租戶ID才能訂閱
     * @return 該租戶能否監(jiān)聽(tīng)
     */
    public boolean tenantIn(String... tenantIds) {
        ThreadContext.setValues(THREAD_LOCALS);
        String tenantId = ThreadContext.getOrDefault("tenant-id", "");
        return Arrays.asList(tenantIds).contains(tenantId);
    }
}

以上代碼,把本地線程變量存進(jìn)了領(lǐng)域事件內(nèi),在監(jiān)聽(tīng)器獲取事件內(nèi)容時(shí),把本地線程變量塞到另一個(gè)線程里。

細(xì)心的同學(xué)發(fā)現(xiàn),該類(lèi)封裝的tenantIn方法有什么作用?

這是為了控制指定的租戶才能監(jiān)聽(tīng)到該事件,比如某個(gè)租戶需要監(jiān)聽(tīng)下單完成后,推到他自己的ERP系統(tǒng),但是其他租戶并沒(méi)有這個(gè)需求,那么我們就可以使用這種方式控制不同租戶的行為,這樣解耦也不會(huì)對(duì)業(yè)務(wù)主流程產(chǎn)生太大影響。

除了SaaS系統(tǒng)的租戶隔離監(jiān)聽(tīng),我們也可以利用這一特性做些別的策略。

以上代碼我們?cè)俪橄笠惠啠?/p>

/**
 * 領(lǐng)域事件
 * 1. 異步事件透?jìng)骶€程變量
 * 2. 租戶策略
 * 3. 條件策略
 */
public abstract class DomainEvent extends ApplicationEvent {
    // 本地線程變量池,用于存儲(chǔ)跨線程信息
    private final Map<String, Object> THREAD_LOCALS = ThreadContext.getValues();
    /**
     * 領(lǐng)域事件構(gòu)造器
     *
     * @param source 事件內(nèi)容
     * @param <T>    任意類(lèi)型
     */
    public <T> DomainEvent(T source) {
        super(source);
    }
    /**
     * 獲取事件內(nèi)容
     *
     * @param <T> 任意類(lèi)型
     * @return 事件內(nèi)容
     */
    public <T> T get() {
        ThreadContext.setValues(THREAD_LOCALS);
        return (T) super.getSource();
    }
    /**
     * 租戶判斷
     * 使用方式:監(jiān)聽(tīng)方法標(biāo)注@EventListener(condition = "#event.tenantIn('xxx', 'xxx')")
     *
     * @param tenantIds 指定租戶ID才能訂閱
     * @return 該租戶能否監(jiān)聽(tīng)
     */
    public boolean tenantIn(String... tenantIds) {
        ThreadContext.setValues(THREAD_LOCALS);
        String tenantId = ThreadContext.getOrDefault("tenant-id", "");
        return Arrays.asList(tenantIds).contains(tenantId);
    }
    // 監(jiān)聽(tīng)者能否執(zhí)行的條件,用于控制事件監(jiān)聽(tīng)器能否執(zhí)行(策略模式)
    private Collection supports;
    /**
     * 領(lǐng)域事件構(gòu)造器
     *
     * @param source   事件內(nèi)容
     * @param supports 支持執(zhí)行的條件,配合supports方法使用
     * @param <T>      任意類(lèi)型
     */
    public <T> DomainEvent(T source, Collection supports) {
        super(source);
        this.supports = supports;
    }
    /**
     * 條件判斷(策略模式)
     * 使用方式:監(jiān)聽(tīng)方法標(biāo)注@EventListener(condition = "#event.supports('xxx', 'xxx')")
     *
     * @param supports 支持的類(lèi)型
     * @param <T>      任意類(lèi)型
     * @return 該條件下能否監(jiān)聽(tīng)
     */
    public <T> boolean supports(T... supports) {
        if (this.supports == null) return false;
        ThreadContext.setValues(THREAD_LOCALS);
        List<T> supportList = Arrays.asList(supports);
        for (Object support : this.supports) {
            if (supportList.contains(support)) {
                return true;
            }
        }
        return false;
    }
    /**
     * 發(fā)布事件,方便但降低代碼可讀性
     * 建議使用原生的SpringContext.getApplicationContext().publishEvent()方法
     */
    public void publish() {
        SpringContext.getApplicationContext().publishEvent(this);
    }
}

我們加入了新的成員變量supports,有什么作用呢?來(lái)看一個(gè)消息中心的例子就一目了然。

業(yè)務(wù)需求是:消息中心需要寫(xiě)一個(gè)事件發(fā)布的接口,聚合站內(nèi)信、極光推送、小程序訂閱消息、公眾號(hào)模板消息、郵件、短信功能等等,并且后續(xù)支持?jǐn)U展。

首先設(shè)計(jì)一下整個(gè)消息中心,DDD領(lǐng)域圖如下:

對(duì)應(yīng)的領(lǐng)域事件定義和監(jiān)聽(tīng)器:

領(lǐng)域事件定義

public class PublishEventMessageEvent extends DomainEvent {
    public PublishEventMessageEvent(EventMessage eventMessage) {
        super(eventMessage, Collections.singleton(eventMessage.getPushChannel()));
    }
}

發(fā)布事件的核心代碼:

// 存儲(chǔ)事件消息
EventMessage eventMessage = EventMessage.builder().eventCode(messageDefine.getEventCode()).notify(messageDefine.getNotify())
  .pushChannel(pushChannel).content(contentCopy).target(targetCopy)
  .categoryCode(messageDefine.getCategoryCode()).categoryName(messageDefine.getCategoryName())
  .pushConfig(messageDefine.getPushConfig())
  .build();
eventMessage.save();
// 發(fā)布事件消息事件
SpringContext.getApplicationContext().publishEvent(new PublishEventMessageEvent(eventMessage));

事件消息事件監(jiān)聽(tīng)器:

/**
 * 極光推送監(jiān)聽(tīng)器
 */
@Component
public class JPushListener {
    /**
     * 發(fā)送極光消息
     *
     * @param event
     */
    @EventListener(condition = "#event.supports('jpush')")
    public void sendJPushMessage(PublishEventMessageEvent event) {
        EventMessage eventMessage = event.get();
        // 下面是核心的推送邏輯
    }
}

上面以極光推送監(jiān)聽(tīng)器為例,其他監(jiān)聽(tīng)器也是同樣的實(shí)現(xiàn)方式,后續(xù)如果還有別的推送實(shí)現(xiàn),再寫(xiě)一個(gè)推送監(jiān)聽(tīng)器即可,消息定義里把對(duì)應(yīng)的推送通道pushChannel給加上。

需要注意的是,使用事件作為策略模式,一般是單向的通知,不宜接收監(jiān)聽(tīng)器的返回結(jié)果做后續(xù)處理。你可能會(huì)說(shuō),那可以在事件的數(shù)據(jù)里定義返回值啊,方法層傳遞引用對(duì)象就行了,但再細(xì)想一下,如果在推送監(jiān)聽(tīng)器上做了異步處理,那由事件發(fā)布者處理這個(gè)結(jié)果就變得不可控了。

4.寫(xiě)在最后

基于SpringEvents實(shí)現(xiàn)的領(lǐng)域事件作為一種跨層解耦的手段,可以讓我們的代碼可讀性變得更高,擴(kuò)展性更強(qiáng),無(wú)論新老項(xiàng)目都是使用即見(jiàn)效的舉措。

上述領(lǐng)域事件DomainEvent已集成到我的D3Boot開(kāi)源基礎(chǔ)框架,大家需要可以移步Gitee抄作業(yè)。

Gitee源碼地址:

https://gitee.com/jensvn/d3boot(例行賒Star)

D3boot基礎(chǔ)框架具體的使用方式見(jiàn)源碼的README.md文件,這里不再贅述。

責(zé)任編輯:姜華 來(lái)源: 架構(gòu)師修行錄
相關(guān)推薦

2023-09-26 01:18:55

解密系統(tǒng)業(yè)務(wù)

2017-12-26 15:52:31

MQ互聯(lián)網(wǎng)耦合

2022-09-02 08:23:12

軟件開(kāi)發(fā)解耦架構(gòu)

2024-11-15 11:01:45

2021-08-27 08:44:52

MQ架構(gòu)耦合

2013-09-16 10:19:08

htmlcssJavaScript

2016-11-30 15:30:42

架構(gòu)工具和方案

2012-07-10 01:47:14

代碼架構(gòu)設(shè)計(jì)

2020-11-20 15:22:32

架構(gòu)運(yùn)維技術(shù)

2021-03-10 05:50:06

IOCReact解耦組件

2023-11-20 23:02:36

Spring系統(tǒng)

2022-04-15 11:46:09

輕量系統(tǒng)解耦鴻蒙操作系統(tǒng)

2018-04-18 08:47:17

Alluxio構(gòu)建存儲(chǔ)

2022-12-28 07:45:17

2022-06-07 07:58:16

流程解耦封裝

2017-11-15 09:32:27

解耦戰(zhàn)術(shù)架構(gòu)

2020-10-16 18:41:43

command設(shè)計(jì)模式代碼

2021-06-01 09:38:19

消息隊(duì)列核心系統(tǒng)下游系統(tǒng)

2024-12-05 09:13:55

Go項(xiàng)目模塊

2018-01-01 06:41:44

耦合互聯(lián)網(wǎng)架構(gòu)配置中心
點(diǎn)贊
收藏

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