Spring Boot 解耦之事件驅(qū)動

參考:SpringBoot使用ApplicationEvent&Listener完成業(yè)務解耦
參考:Spring Boot 解耦之事件驅(qū)動

一、前言

1壹置、1 使用場景

日常開發(fā)中竞思,常見的比如用戶注冊賬號操作,當用戶注冊完畢之后蒸绩,可能還需要處理以下一些事情:

  • 發(fā)送確認郵件
  • 贈送用戶積分衙四、成長值
  • 贈送優(yōu)惠券

問題

  • 假如以上所有的操作全部都耦合在一個service業(yè)務處理代碼中铃肯,后續(xù)操作一直沒有完成患亿,那么用戶是不是要長時間等待
  • 如果郵件服務器掛了,注冊還能成功嗎
  • 后期維護起來相關(guān)代碼也會非常麻煩押逼,甚至會出現(xiàn)一些漏洞等

從上述例子可以看出步藕,當用戶注冊完畢之后,發(fā)布一個命令給第三方的觀察者挑格,觀察者接收到相關(guān)命令之后咙冗,就可以來處理之后的相關(guān)事件,那么程序就可以解耦各個環(huán)節(jié)的依賴關(guān)系漂彤,這就是事件驅(qū)動模型雾消,內(nèi)部實現(xiàn)原理是觀察者設(shè)計模式。

1挫望、2 事件驅(qū)動定義

事件驅(qū)動模型也就是我們常說的觀察者立润,或者發(fā)布-訂閱模型;理解它的幾個關(guān)鍵點:

  • 首先是一種對象間的一對多的關(guān)系媳板;最簡單的如交通信號燈桑腮,信號燈是目標(一方),行人注視著信號燈(多方)蛉幸;
  • 當目標發(fā)送改變(發(fā)布)破讨,觀察者(訂閱者)就可以接收到改變丛晦;
  • 觀察者如何處理(如行人如何走,是快走/慢走/不走提陶,目標不會管的)烫沙,目標無需干涉;所以就松散耦合了它們之間的關(guān)系隙笆。

spring主要是通過ApplicationEvent以及Listener為我們提供事件監(jiān)聽斧吐、訂閱等相關(guān)事件處理。

二仲器、項目中應用

2煤率、1 搭建springboot項目

略。

2乏冀、2 定義事件

/**
 * 自定義訂單監(jiān)聽事件蝶糯,繼承了ApplicationEvent,并重載構(gòu)造函數(shù)
 * <p>
 * 構(gòu)造函數(shù)的參數(shù)可以任意指定辆沦,其中source參數(shù)指的是發(fā)生事件的對象昼捍,而第二個參數(shù)是我們自定義的注冊事件對象污桦,該對象可以在監(jiān)聽內(nèi)被獲取腰素。
 */
 
@Getter
public class OrderCancelEvent extends ApplicationEvent {

    // 注入訂單業(yè)務對象
    private OmsOrder omsOrder;

    public OrderCancelEvent(Object source, OmsOrder omsOrder) {
        super(source);
        this.omsOrder = omsOrder;
    }
}

事件是事件驅(qū)動的核心,上述OrderCancelEvent就是自定義的一個事件迷殿,繼承了ApplicationEvent蔚晨,并重寫了其構(gòu)造函數(shù)乍钻,第一個參數(shù)一般我們在發(fā)布事件時使用的是this關(guān)鍵字代替本類對象,而第二個參數(shù)則根據(jù)具體業(yè)務具體定義铭腕,主要就是為了使監(jiān)聽器可以監(jiān)聽到相關(guān)事件银择。

2、3 創(chuàng)建事件監(jiān)聽器

事件監(jiān)聽器的創(chuàng)建方式有好多種累舷,eg:@EventListener注解浩考、實現(xiàn)ApplicationListener泛型接口、實現(xiàn)SmartApplicationListener接口等被盈,我們下面來講解下這三種方式分別如何實現(xiàn)析孽。

@EventListener
使用該注解是最簡單的一種方式,只需在方法上加上此注解即可只怎。

/**
 * 用途:消息事件監(jiān)聽器
 * 作者: jingwenhao
 * 時間: 2019/7/26  15:29
 */
@Component
@Slf4j
public class MsgSendListener {

    @Autowired
    private IImsNotificationService notificationService;

    /**
     * 支付成功之后袜瞬,異步發(fā)送消息事件
     *
     * @param event 消息事件
     */
    @EventListener
    @Async
    public void sendMsgAfterPaySucc(MsgOrderPayEvent event) {
        try {
            log.debug("——發(fā)送消息事件開始執(zhí)行——");
            Thread.sleep(3000);// 休息3秒
            // 獲取事件實際對象
            Map<String, Object> eventMap = event.getEventMap();
            // 具體的業(yè)務邏輯方法
            notificationService.sendMsgAfterPaySucc(eventMap);
            log.debug("——發(fā)送消息事件結(jié)束——");
        } catch (Exception e) {
             e.printStackTrace();
        }
    }

    /**
     * 超時訂單關(guān)閉,異步發(fā)送消息事件
     *
     * @param event 消息事件
     */
    @EventListener
    @Async
    public void sendMsgAfterAutoCancel(MsgOrderCancelEvent event) {
        try {
            log.debug("——發(fā)送消息事件開始執(zhí)行——");
            Thread.sleep(3000);// 休息3秒
            Map<String, Object> eventMap = event.getEventMap();
            notificationService.sendMsgAfterAutoCancel(eventMap);
            log.debug("——發(fā)送消息事件結(jié)束——");
        } catch (Exception e) {
             e.printStackTrace();
        }
    }
}
  • 實現(xiàn)ApplicationListener泛型接口
@Component
public class RegisterListener implements ApplicationListener<UserRegisterEvent>
{
    /**
     * 實現(xiàn)監(jiān)聽
     * @param userRegisterEvent
     */
    @Override
    public void onApplicationEvent(UserRegisterEvent userRegisterEvent) {
        //獲取注冊用戶對象
        UserBean user = userRegisterEvent.getUser();

        //../省略邏輯

        //輸出注冊用戶信息
        System.out.println("注冊信息尝盼,用戶名:"+user.getName()+"吞滞,密碼:"+user.getPassword());
    }
}

這里直接復制了博客上的寫法,這種寫法主要是實現(xiàn)了ApplicationListener接口,并將事先定義好的事件作為泛型對象傳遞了過去裁赠,UserRegisterEvent事件發(fā)布時監(jiān)聽程序會自動調(diào)用onApplicationEvent方法并且將UserRegisterEvent對象作為參數(shù)傳遞殿漠。

  • 實現(xiàn)SmartApplicationListener
/**
 * 訂單超時自動關(guān)閉監(jiān)聽任務
 */
@Component
@Slf4j
public class OrderAutoCloseListener implements SmartApplicationListener {

    @Autowired
    private IOmsOrderService orderService;//注入訂單業(yè)務接口

    /**
     * 該方法返回true&supportsSourceType同樣返回true時,才會調(diào)用該監(jiān)聽內(nèi)的onApplicationEvent方法
     *
     * @param aClass 接收到的監(jiān)聽事件類型
     * @return
     */
    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
        //只有OrderEvent監(jiān)聽類型才會執(zhí)行下面邏輯
        return aClass == OrderCancelEvent.class;
    }

    /**
     * 該方法返回true&supportsEventType同樣返回true時佩捞,才會調(diào)用該監(jiān)聽內(nèi)的onApplicationEvent方法
     *
     * @param aClass
     * @return
     */
    @Override
    public boolean supportsSourceType(Class<?> aClass) {
        //只有在OmsOrderServiceImpl內(nèi)發(fā)布的UserRegisterEvent事件時才會執(zhí)行下面邏輯
        return aClass == OmsOrderServiceImpl.class;
    }

    /**
     * supportsEventType & supportsSourceType 兩個方法返回true時調(diào)用該方法執(zhí)行業(yè)務邏輯
     *
     * @param applicationEvent 具體監(jiān)聽實例绞幌,這里是orderEvent
     */
    @Override
    @Async
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        log.debug("————訂單超時關(guān)閉事件開始準備,倒計時1分鐘");
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        //轉(zhuǎn)換事件類型
        OrderCancelEvent orderCancelEvent = (OrderCancelEvent) applicationEvent;
        //獲取訂單對象
        OmsOrder order = orderCancelEvent.getOmsOrder();
        Runnable payTimeoutTask = new Runnable() {
            @Override
            public void run() {
                //執(zhí)行訂單超時一忱,系統(tǒng)取消訂單操作
                orderService.cancelOrderAutoById(order.getId());
            }
        };
        executor.schedule(payTimeoutTask, 2, TimeUnit.MINUTES);
        try {
            //每分鐘檢查任務是否完成莲蜘,完成后關(guān)閉任務線程
            executor.awaitTermination(1, TimeUnit.MINUTES); 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executor.shutdown();
        log.debug("————訂單超時關(guān)閉事件執(zhí)行結(jié)束");
    }

    public void SyncAndAsync() throws ExecutionException, InterruptedException {
        AsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
        System.out.println("First");
        //Future<String> future = executor.submit(new CallableTask("Second")); //同步
        executor.execute(new RunnableTask("go!"));
        //System.out.println(future.get());
        System.out.println("Third");
    }

    class RunnableTask implements Runnable{
        private String parameter;
        public RunnableTask(String parameter) {
            super();
            this.parameter = parameter;
        }
        @Override
        public void run() {
            try {
                Thread.sleep(5 * 1000);
                System.out.println(parameter+ "Second");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    class CallableTask implements Callable<String> {
        private String parameter;
        public CallableTask(String parameter) {
            super();
            this.parameter = parameter;
        }
        @Override
        public String call() throws Exception {
            Thread.sleep(5 * 1000);
            return parameter+ " finished!";
        }
    }

    /**
     * 同步情況下監(jiān)聽執(zhí)行的順序
     *
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

SmartApplicationListener接口繼承了全局監(jiān)聽ApplicationListener,并且泛型對象使用的ApplicationEvent來作為全局監(jiān)聽帘营,可以理解為使用SmartApplicationListener作為監(jiān)聽父接口的實現(xiàn)票渠,監(jiān)聽所有事件發(fā)布。
既然是監(jiān)聽所有的事件發(fā)布芬迄,那么SmartApplicationListener接口添加了兩個方法supportsEventType问顷、supportsSourceType來作為區(qū)分是否是我們監(jiān)聽的事件,只有這兩個方法同時返回true時才會執(zhí)行onApplicationEvent方法禀梳。

可以看到除了上面的方法杜窄,還提供了一個getOrder方法,這個方法就可以解決執(zhí)行監(jiān)聽的順序問題算途,return的數(shù)值越小證明優(yōu)先級越高塞耕,執(zhí)行順序越靠前。

2嘴瓤、4 使用@Async實現(xiàn)異步監(jiān)聽

@Aysnc其實是Spring內(nèi)的一個組件扫外,可以完成對類內(nèi)單個或者多個方法實現(xiàn)異步調(diào)用,這樣可以大大的節(jié)省等待耗時纱注。內(nèi)部實現(xiàn)機制是線程池任務ThreadPoolTaskExecutor畏浆,通過線程池來對配置@Async的方法或者類做出執(zhí)行動作胆胰。

2狞贱、5 具體業(yè)務場景

  • 訂單創(chuàng)建時,發(fā)布超時未支付自動關(guān)閉事件:
@Service
public class OrderService
{
    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 生成訂單
     * @param order 訂單對象
     */
    public void generateOrder(OmsOrder order)
    {
        //....省略邏輯

        //發(fā)布訂單超時關(guān)閉事件
       applicationContext.publishEvent(new OrderCancelEvent(this, order));
    }
}
  • 成功付款時蜀涨,發(fā)布推送消息事件:
 @Service
public class OrderService
{
    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 支付訂單
     * @param order 訂單對象
     */
    public void pay(OmsOrder order)
    {
        //....省略邏輯

       // 發(fā)送支付成功消息事件
      Map<String, Object> eventMap = Maps.newHashMap();
      eventMap.put("order", order);
      applicationContext.publishEvent(new MsgOrderPayEvent(this, eventMap));
    }
}

2.6 拓展

上述2.5中描述了一個場景:訂單創(chuàng)建時瞎嬉,發(fā)布超時未支付自動關(guān)閉事件,在這里厚柳,介紹幾種電商項目中如何處理超時未支付自動關(guān)閉訂單的方案:

  • 第一種:數(shù)據(jù)庫加兩個字段, 一個字段標記: 是否付款,一個字段標記:過期時間氧枣,查詢時去判斷是否 付款和超時,然后更新狀態(tài)别垮。但是這種方案導致占用商品資源便监。
  • 第二種:定時任務一直掃描,掃描到滿足條件的就進行更新操作。但是這種方案導致不能準確處理訂單狀態(tài)烧董。
  • 第三種:TODO 依賴于第三方框架毁靶,比如框架Quartz、rabbitMQ等逊移,不太懂预吆,就不解釋了哈。

我目前是這樣處理超時未支付自動關(guān)閉:當訂單創(chuàng)建完成時胳泉,發(fā)布超時未支付自動關(guān)閉事件拐叉,同時系統(tǒng)中還有一個每1分鐘執(zhí)行一次的掃描超時未支付訂單的定時任務,兩種方案結(jié)合目前是可以解決上述問題扇商。采用異步事件定時任務結(jié)合的方案凤瘦,有以下好處:

  • 更加準確的處理超時未支付訂單,及時釋放庫存
  • 防止系統(tǒng)宕機導致的進程丟失案铺,原有的事件任務無法執(zhí)行廷粒,通過定時任務可以有效解決此問題

三、小結(jié)

使用事件驅(qū)動模型可以大大降低我們實際項目中的代碼耦合红且,降低了前后端交互的響應耗時坝茎,而且還減少了后期業(yè)務變更引起的代碼調(diào)整的難度。具體使用步驟如下:

  • 定義事件
  • 創(chuàng)建事件監(jiān)聽器
  • 業(yè)務代碼中發(fā)布事件
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末暇番,一起剝皮案震驚了整個濱河市嗤放,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌壁酬,老刑警劉巖次酌,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異舆乔,居然都是意外死亡岳服,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門希俩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吊宋,“玉大人,你說我怎么就攤上這事颜武×眩” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵鳞上,是天一觀的道長这吻。 經(jīng)常有香客問我,道長篙议,這世上最難降的妖魔是什么唾糯? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上移怯,老公的妹妹穿的比我還像新娘拒名。我一直安慰自己,他們只是感情好芋酌,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布增显。 她就那樣靜靜地躺著,像睡著了一般脐帝。 火紅的嫁衣襯著肌膚如雪同云。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天堵腹,我揣著相機與錄音炸站,去河邊找鬼。 笑死疚顷,一個胖子當著我的面吹牛旱易,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播腿堤,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼阀坏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了笆檀?” 一聲冷哼從身側(cè)響起忌堂,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎酗洒,沒想到半個月后士修,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡樱衷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年棋嘲,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片矩桂。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡沸移,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出耍鬓,到底是詐尸還是另有隱情阔籽,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布牲蜀,位于F島的核電站,受9級特大地震影響绅这,放射性物質(zhì)發(fā)生泄漏涣达。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望度苔。 院中可真熱鬧匆篓,春花似錦、人聲如沸寇窑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽甩骏。三九已至窗市,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饮笛,已是汗流浹背咨察。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留福青,地道東北人摄狱。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像无午,于是被迫代替她去往敵國和親媒役。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容