成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

“解耦神器”之SpringEvents領域事件

開發 前端
領域事件是一種用于表示領域模型中發生的重要事件的機制。它們用于通知其他相關的聚合或服務,以便它們可以采取相應的行動。

大家好,我是Jensen。一個想和大家一起打怪升級的程序員朋友。

在DDD項目的落地過程中,除了聚合、模型等等重要概念,領域事件在其中扮演了一個非常重要的角色,它不僅能解耦領域層與其他層,作為“跳出”領域層的跳板,還是一種策略模式的高級用法。即便你的項目沒有DDD,領域事件在傳統的MVC分層架構也大有妙用。

下面我們一起來解鎖這個“解耦神器”。

1.什么是領域事件

領域事件是一種用于表示領域模型中發生的重要事件的機制。它們用于通知其他相關的聚合或服務,以便它們可以采取相應的行動。

領域事件通常由聚合根( Aggregate Root)發布。當聚合根內部發生重要的狀態更改時,它會發布一個領域事件。其他聚合或服務可以訂閱這些事件,并在事件發生時采取相應的行動。

以下是使用領域事件的四大步:

  • 定義領域事件:領域事件是一個簡單的對象,它包含事件的名稱、發生時間和相關的數據。例如,一個訂單已完成的領域事件可能包含訂單的 ID 和完成時間。
  • 發布領域事件:當聚合根內部發生重要的狀態更改時,它會發布一個領域事件。例如,當訂單完成時,訂單聚合根會發布一個 OrderCompletedEvent 事件。
  • 訂閱領域事件:其他聚合或服務可以訂閱領域事件,并在事件發生時采取相應的行動。例如,一個訂單跟蹤服務可以訂閱 OrderCompletedEvent 事件,并在訂單完成時發送通知給客戶。
  • 處理領域事件:當領域事件被發布時,訂閱者會收到通知,并可以根據事件的數據采取相應的行動。例如,訂單跟蹤服務可以在收到 OrderCompletedEvent 事件時發送通知給客戶。

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

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

2.領域事件的定義、發布與訂閱

在DDD工程中,領域事件定義在領域層,具體來說是放在領域契約下面,如:domain.contract.event,它不屬于某個聚合私有,由該系統下的所有聚合共享。

為什么要這樣劃分呢?

我認為,領域事件不僅能在領域層發布,也可能在應用層發布,甚至在接入層發布,而在領域聚合之外發布的事件,必然會存在跨聚合的事件屬性。

我舉個預約的場景:

工單中臺下的預約業務需要設計一個支付回調接口,由商城系統支付成功后進行回調,此時商城系統傳入的回調命令參數在處理完核心業務后(如設置預約單狀態為待服務),再發布支付回調成功事件,以執行后續的非核心業務邏輯(比如提醒服務店員需要聯系客戶到店等等)。

工單中臺和商城系統已然進行了服務拆分,工單中臺本身并不包含支付業務,領域層(如領域服務)并沒有發布這個支付回調成功的事件的入口,那么,發布領域事件的最佳位置是在應用層。

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

我在DDD落地過程中,曾多次調整領域事件訂閱的位置,有試過放在領域層聚合下面,也有試過抽取到SDK工程里,最終在前段時間確定下來了,事件訂閱就放在應用層的listener包下面,意為事件監聽器。

至于命名規則,需要看系統的復雜度,一般小而美的微服務,以聚合Listener或以外部系統Listener命名足以,如工單中臺(WorkOrder)下的預約領域聚合(Appointment),其監聽器以AppointmentListener命名,訂單領域聚合(Order)是商城系統(如Mall)外部聚合,其監聽器以MallListener命名而非OrderListener。

特別強調一點,在高內聚的架構設計中,外部系統的調用不會設計特別多,如果存在大量的跨系統交互,我們該反思一下是不是微服務拆分得太細了,大量的外部系統調用會存在跨線程的分布式事務等問題等。

當然,隨著業務快速發展,系統復雜度隨之上升,事件監聽listener也可能跟著拆分,這時候我們的原則還是往大了拆,不宜拆得太細。

對于非DDD工程,可以考慮在根目錄定義一個event包,包括entity和listener:entity下定義領域事件,listener下定義領域事件監聽器,這樣一來我們寫代碼就更加簡單清晰。

3.領域事件解耦實戰

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

上面所說的領域事件,一直停留在概念層面,事件的發布訂閱只是設計模式,那具體要怎么實現,才是核心技術。

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

在這里,我認為領域事件在工程內部解耦即可,用不上第三方跨線程的MQ模型,所以我選了SpringEvents作為發布訂閱的底層實現,而且Spring事件有個好處,它可以在Idea工具中鏈接消息發布和訂閱,對于編程還是非常友好的。

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

SpringEvents的常規打開方式:

  • 定義事件:定義一個事件類,該類應該繼承自ApplicationEvent類。你可以在事件類中添加任何需要的數據,這些數據將在事件發布時傳遞給訂閱者。
  • 發布事件:使用ApplicationEventPublisher發布事件。你可以通過ApplicationContext獲取ApplicationEventPublisher實例,并使用其publishEvent方法發布事件。
  • 訂閱事件:使用@EventListener注解來訂閱事件。將@EventListener注解應用于一個方法上,并指定要訂閱的事件類型。該方法將在事件發布時被調用,并接收事件對象作為參數。

領域事件還要解決一個問題,如果我們通過@Async+@EventListener實現異步監聽,需要跨線程傳遞信息,那我們就要對領域事件做一層小小的封裝了。

首先,寫一個領域事件抽象類,該類由其他事件繼承:

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

以上代碼,把本地線程變量存進了領域事件內,在監聽器獲取事件內容時,把本地線程變量塞到另一個線程里。

細心的同學發現,該類封裝的tenantIn方法有什么作用?

這是為了控制指定的租戶才能監聽到該事件,比如某個租戶需要監聽下單完成后,推到他自己的ERP系統,但是其他租戶并沒有這個需求,那么我們就可以使用這種方式控制不同租戶的行為,這樣解耦也不會對業務主流程產生太大影響。

除了SaaS系統的租戶隔離監聽,我們也可以利用這一特性做些別的策略。

以上代碼我們再抽象一輪:

/**
 * 領域事件
 * 1. 異步事件透傳線程變量
 * 2. 租戶策略
 * 3. 條件策略
 */
public abstract class DomainEvent extends ApplicationEvent {
    // 本地線程變量池,用于存儲跨線程信息
    private final Map<String, Object> THREAD_LOCALS = ThreadContext.getValues();
    /**
     * 領域事件構造器
     *
     * @param source 事件內容
     * @param <T>    任意類型
     */
    public <T> DomainEvent(T source) {
        super(source);
    }
    /**
     * 獲取事件內容
     *
     * @param <T> 任意類型
     * @return 事件內容
     */
    public <T> T get() {
        ThreadContext.setValues(THREAD_LOCALS);
        return (T) super.getSource();
    }
    /**
     * 租戶判斷
     * 使用方式:監聽方法標注@EventListener(condition = "#event.tenantIn('xxx', 'xxx')")
     *
     * @param tenantIds 指定租戶ID才能訂閱
     * @return 該租戶能否監聽
     */
    public boolean tenantIn(String... tenantIds) {
        ThreadContext.setValues(THREAD_LOCALS);
        String tenantId = ThreadContext.getOrDefault("tenant-id", "");
        return Arrays.asList(tenantIds).contains(tenantId);
    }
    // 監聽者能否執行的條件,用于控制事件監聽器能否執行(策略模式)
    private Collection supports;
    /**
     * 領域事件構造器
     *
     * @param source   事件內容
     * @param supports 支持執行的條件,配合supports方法使用
     * @param <T>      任意類型
     */
    public <T> DomainEvent(T source, Collection supports) {
        super(source);
        this.supports = supports;
    }
    /**
     * 條件判斷(策略模式)
     * 使用方式:監聽方法標注@EventListener(condition = "#event.supports('xxx', 'xxx')")
     *
     * @param supports 支持的類型
     * @param <T>      任意類型
     * @return 該條件下能否監聽
     */
    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;
    }
    /**
     * 發布事件,方便但降低代碼可讀性
     * 建議使用原生的SpringContext.getApplicationContext().publishEvent()方法
     */
    public void publish() {
        SpringContext.getApplicationContext().publishEvent(this);
    }
}

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

業務需求是:消息中心需要寫一個事件發布的接口,聚合站內信、極光推送、小程序訂閱消息、公眾號模板消息、郵件、短信功能等等,并且后續支持擴展。

首先設計一下整個消息中心,DDD領域圖如下:

對應的領域事件定義和監聽器:

領域事件定義

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

發布事件的核心代碼:

// 存儲事件消息
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();
// 發布事件消息事件
SpringContext.getApplicationContext().publishEvent(new PublishEventMessageEvent(eventMessage));

事件消息事件監聽器:

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

上面以極光推送監聽器為例,其他監聽器也是同樣的實現方式,后續如果還有別的推送實現,再寫一個推送監聽器即可,消息定義里把對應的推送通道pushChannel給加上。

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

4.寫在最后

基于SpringEvents實現的領域事件作為一種跨層解耦的手段,可以讓我們的代碼可讀性變得更高,擴展性更強,無論新老項目都是使用即見效的舉措。

上述領域事件DomainEvent已集成到我的D3Boot開源基礎框架,大家需要可以移步Gitee抄作業。

Gitee源碼地址:

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

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

責任編輯:姜華 來源: 架構師修行錄
相關推薦

2023-09-26 01:18:55

解密系統業務

2017-12-26 15:52:31

MQ互聯網耦合

2022-09-02 08:23:12

軟件開發解耦架構

2024-11-15 11:01:45

2021-08-27 08:44:52

MQ架構耦合

2013-09-16 10:19:08

htmlcssJavaScript

2016-11-30 15:30:42

架構工具和方案

2012-07-10 01:47:14

代碼架構設計

2020-11-20 15:22:32

架構運維技術

2021-03-10 05:50:06

IOCReact解耦組件

2025-05-20 07:13:22

Spring異步解耦Event

2023-11-20 23:02:36

Spring系統

2022-04-15 11:46:09

輕量系統解耦鴻蒙操作系統

2018-04-18 08:47:17

Alluxio構建存儲

2022-12-28 07:45:17

2022-06-07 07:58:16

流程解耦封裝

2017-11-15 09:32:27

解耦戰術架構

2012-10-12 14:28:32

BYOD安全網絡

2020-10-16 18:41:43

command設計模式代碼

2021-06-01 09:38:19

消息隊列核心系統下游系統
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 在线观看亚洲精品 | 欧美精品区| 天天搞天天搞 | 国产精品毛片久久久久久久 | 黄色一级大片在线免费看产 | 国产精品久久久亚洲 | 国产精品视频播放 | 国产精品久久久久久久久久三级 | 伊人网在线播放 | 国产一区二区三区高清 | 成人国产在线视频 | 日韩国产一区二区三区 | 精品乱码一区二区三四区 | 黄免费观看视频 | 日韩一区二区三区在线观看 | 黄色片免费在线观看 | 视频一区二区在线观看 | 中文在线一区二区 | 成年人在线观看 | 亚洲成人精品国产 | 欧美精品久久久久 | 一区二区三区四区在线视频 | 日韩精品一区二区三区免费视频 | 精品在线免费看 | 久久久免费毛片 | 久久久免费| 午夜成人免费视频 | 羞羞视频网站免费看 | 精品国产一区探花在线观看 | 九色视频网站 | 一级黄色在线 | 日韩高清一区 | 精品欧美一区二区精品久久 | 国产精品国色综合久久 | 99tv| 亚洲成人日韩 | 一区二区三区四区在线播放 | 亚洲成人综合网站 | 国产二区三区 | 影音先锋亚洲资源 | 成人精品在线观看 |