1. 程式人生 > >趣談spring事件:業務解耦與非同步呼叫

趣談spring事件:業務解耦與非同步呼叫

分析需求引入事件機制

使用spring的事件機制有助於對我們的專案進一步的解耦。假如現在我們面臨一個需求:
我需要在使用者註冊成功的時候,根據使用者提交的郵箱、手機號資訊,向用戶傳送郵箱認證和手機號簡訊通知。傳統的做法之一是在我們的UserService層注入郵件傳送和簡訊傳送的相關類,然後在完成使用者註冊同時,呼叫對應類方法完成郵件傳送和簡訊傳送
但這樣做的話,會把我們郵件、簡訊傳送的業務與我們的UserService的邏輯業務耦合在了一起。耦合造成的常見缺點是,我(甚至假設很頻繁的)修改了郵件、簡訊傳送的API,我就可能需要在UserService層修改相應的呼叫方法,但這樣做人家UserService就會很無辜並吐槽:你改郵件、簡訊傳送的業務,又不關我的事,幹嘛老改到我身上來了?這就是你的不對了。


對呀!根據職責分明的設計原則,人家UserService就只該管使用者管理部分的業務邏輯,你老讓它幹別人乾的事,它當然不高興了!
那該怎麼拌?涼拌?不不不。。。我們可以通過spring的事件機制來實現解耦呀。利用觀察者設計模式,設定監聽器來監聽userService的註冊事件(同時,我們可以很自然地將userService理解成了事件釋出者),一旦userService註冊了,監聽器就完成相應的郵箱、簡訊傳送工作(同時,我們也可以很自然地將傳送郵件傳送簡訊理解成我們的事件源)。這樣userService就不用管別人的事了,只需要在完成註冊功能時候,當下老大,號令手下(監聽器),讓它完成簡訊、郵箱的傳送工作。

spring的事件通訊常按下列流程進行

Created with Raphaël 2.1.0事件釋出者廣播事件(源)監聽器收到廣播,獲取事件源監聽器根據事件源採取相應的處理措施

事件例項分析

在這裡面,我們涉及到三個主要物件:事件釋出者、事件源、事件監聽器。根據這三個物件,我們來配置我們的註冊事件例項:

1. 定義事件源

利用事件通訊的第一步往往便是定義我們的事件。在spring中,所有事件都必須擴充套件抽象類ApplicationEvent,同時將事件源作為建構函式引數,在這裡,我們定義了發郵件、發簡訊兩個事件如下所示

/*****************郵件傳送事件源*************/
public
class SendEmailEvent extends ApplicationEvent { //定義事件的核心成員:傳送目的地,共監聽器呼叫完成郵箱傳送功能 private String emailAddress; public SendEmailEvent(Object source,String emailAddress ) { //source字面意思是根源,意指傳送事件的根源,即我們的事件釋出者 super(source); this.emailAddress = emailAddress; } public String getEmailAddress() { return emailAddress; } } /*****************簡訊傳送事件源*************/ public class sendMessageEvent extends ApplicationEvent { private String phoneNum; public sendMessageEvent(Object source,String phoneNum ) { super(source); this.phoneNum = phoneNum; } public String getPhoneNum() { return phoneNum; } }

2. 定義事件監聽器

事件監聽類需要實現我們的ApplicationListener介面,除了可以實現ApplicationListener定義事件監聽器外,我們還可以讓事件監聽類實現SmartApplicationListener(智慧監聽器)介面,。關於它的具體用法和實現可參考我的下一篇文章《spring學習筆記(14)趣談spring 事件機制[2]:多監聽器流水線式順序處理 》。而此外,如果我們事件監聽器監聽的事件型別唯一的話,我們可以通過泛型來簡化配置。
現在我們先來看看本例定義:

public class RegisterListener implements  ApplicationListener  {
    /*
    *當我們的釋出者釋出時間時,我們的監聽器收到訊號,就會呼叫這個方法
    *我們對其進行重寫來適應我們的需求
    *@Param event:我們的事件源
    */
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        //我們定義了兩個事件:發簡訊,發郵箱,他們一旦被髮布都會被此方法呼叫
        //於是我們需要判斷當前event的具體型別
        if(event instanceof SendEmailEvent){//如果是發郵箱事件
            System.out.println("正在向" + ((SendEmailEvent) event).getEmailAddress()+ "傳送郵件......");//模擬傳送郵件事件
            try {
                Thread.sleep(1* 1000);//模擬請求郵箱伺服器、驗證賬號密碼,傳送郵件耗時。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("郵件傳送成功!");
        }else if(event instanceof sendMessageEvent){//是發簡訊事件
            event = (sendMessageEvent) event;
            System.out.println("正在向" + ((sendMessageEvent) event).getPhoneNum()+ "傳送簡訊......");//模擬傳送郵簡訊事件
            try {
                Thread.sleep(1* 1000);//模擬傳送簡訊過程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("簡訊傳送成功!");
        }
    }

}
/******************通過泛型配置例項如下******************/
public class RegisterListener implements  ApplicationListener<SendEmailEvent>  {//這裡使用泛型
    @Override//因為使用了泛型,我們的重寫方法入參事件就唯一了。
    public void onApplicationEvent(SendEmailEvent event) {
        .....
    }
    ....
}

3. 定義事件釋出者

事件傳送的代表類是ApplicationEventPublisher我們的事件釋出類常實現ApplicationEventPublisherAware介面,同時需要定義成員屬性ApplicationEventPublisher來發布我們的事件。
除了通過實現ApplicationEventPublisherAware外,我們還可以實現ApplicationContextAware介面來完成定義,ApplicationContext介面繼承了ApplicationEventPublisher。ApplicationContext是我們的事件容器上層,我們釋出事件,也可以通過此容器完成釋出。下面使用兩種方法來定義我們的釋出者
在本例中,我們的時間釋出者自然就是我們的吐槽者,userService:

/**********方法一:實現除了通過實現ApplicationEventPublisherAware介面************/
public class UserService implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher applicationEventPublisher;//底層事件釋出者

    @Override
    public void setApplicationEventPublisher(//通過Set方法完成我們的實際釋出者注入
            ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    public void doLogin(String emailAddress,String phoneNum) throws InterruptedException{
        Thread.sleep(200);//模擬使用者註冊的相關業務邏輯處理
        System.out.println("註冊成功!");
        //下列向用戶傳送郵件
        SendEmailEvent sendEmailEvent = new SendEmailEvent(this,emailAddress);//定義事件
        sendMessageEvent sendMessageEvent = new sendMessageEvent(this, phoneNum);
        applicationEventPublisher.publishEvent(sendEmailEvent);//釋出事件
        applicationEventPublisher.publishEvent(sendMessageEvent);
    }
    //...忽略其他使用者管理業務方法
}
/**********方法二:實現除了通過實現ApplicationContext介面************/
public class UserService2 implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        this.applicationContext = applicationContext;
    }

    public void doLogin(String emailAddress,String phoneNum) throws InterruptedException{
        Thread.sleep(200);//模擬使用者註冊的相關業務邏輯處理
        System.out.println("註冊成功!");
        //下列向用戶傳送郵件
        SendEmailEvent sendEmailEvent = new SendEmailEvent(this,emailAddress);//定義事件
        sendMessageEvent sendMessageEvent = new sendMessageEvent(this, phoneNum);
        applicationContext.publishEvent(sendEmailEvent);//釋出事件
        applicationContext.publishEvent(sendMessageEvent);
    }
    //...忽略其他使用者管理業務方法
}

4. 在IOC容器註冊監聽器

<!-- 在spring容器中註冊事件監聽器,
應用上下文將會識別實現了ApplicationListener介面的Bean,
並在特定時刻將所有的事件通知它們 -->
<bean id="RegisterListener" class="test.event.RegisterListener" />
<!-- 註冊我們的釋出者,後面測試用到 -->                    
<bean id="userService" class="test.event.UserService" />

5. 測試方法

public static void main(String args[]) throws InterruptedException{
    ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:test/event/event.xml");
    UserService userService = (UserService) ac.getBean("userService");
    Long beginTime = System.currentTimeMillis();
    userService.doLogin("[email protected]","12345678911");//完成註冊請求
    System.out.println("處理註冊相關業務耗時" + (System.currentTimeMillis() - beginTime )+ "ms");
    System.out.println("處理其他業務邏輯");
        Thread.sleep(500);//模擬處理其他業務請求耗時
    System.out.println("處理所有業務耗時" + (System.currentTimeMillis() - beginTime )+ "ms");
    System.out.println("向客戶端傳送註冊成功響應");
}

6. 測試結果及分析

呼叫上面測試方法,控制檯列印資訊

註冊成功!
正在向[email protected]傳送郵件……
郵件傳送成功!
正在向12345678911傳送簡訊……
傳送成功!
處理註冊相關業務耗時2201ms
處理其他業務邏輯開始..
處理其他業務邏輯結束..
處理所有業務耗時2701ms
向客戶端傳送註冊成功響應

在本例中,我們通過事件機制完成了userService和郵件、簡訊傳送業務的解耦。但觀察我們的測試結果,我們會發現,這樣的使用者體驗真是糟糕透了:天吶,我去你那註冊個使用者,要我等近3秒鐘!這太久了!
為什麼會這麼久?我們根據方法分析:
1. 註冊查詢資料庫用了200ms(查詢使用者名稱、郵箱、手機號有沒被使用,插入使用者資訊到資料庫等操作)
2. 傳送郵件用了1000ms
3. 傳送簡訊用了1000ms
4. 處理其他業務邏輯(儲存使用者資訊到session,其他資訊資料處理等)
第1,4步的時間耗損我們很難優化,但2,3步是主要耗時的地方,我們能不能想辦法把它縮減掉了,它把我們的正常的業務處理堵塞了。什麼?堵塞,想到堵塞,我們會很自然地想到非堵塞,那就通過非同步來完成2,3唄!

7. 非同步拓展。

在spring3以上,拓展了自己獨立的時間機制,我們可以使用@Async來完成非同步配置。
首先我們需要在我們的IOC容器增加

<!--先在名稱空間中增加我們的task標籤,注意它們的新增位置
xmlns 多加下面的內容:
xmlns:task="http://www.springframework.org/schema/task"
然後xsi:schemaLocation多加下面的內容
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.0.xsd
-->
<!-- 我們的非同步事件配置,非常簡單 -->
 <!--開啟註解排程支援 @Async @Scheduled-->  
<task:annotation-driven/>       

然後在我們的事件監聽器中新增@Async註解

/***************我們可以在類名上新增****************/
@Async
public class RegisterListener implements  ApplicationListener  {
    ......
}
/****************也可以在方法體上新增************/
@Async
public class RegisterListener implements  ApplicationListener  {

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        .....
    }
}

然後,再呼叫我們的同樣的測試方法,這次我們的結果變成:

註冊成功!
正在向[email protected]傳送郵件……
處理註冊相關業務耗時201ms ————此時郵件傳送還沒有結束,和郵件傳送非同步了
正在向12345678911傳送簡訊….. ————–簡訊傳送和郵件傳送和主業務處理程式都非同步了!
處理其他業務邏輯開始..
處理其他業務邏輯結束..
處理所有業務耗時701ms
向客戶端傳送註冊成功響應 ——客戶端耗時701ms就收到響應了。
郵件傳送成功! —-這個時候郵箱才發完
簡訊傳送成功!

從以上的測試結果我們,我們的郵箱傳送和簡訊傳送都分別單獨地非同步完成了,大大縮短了我們主業務處理事件,也提高了使用者體驗

小結

  1. 從本例可以看出,不同業務功能的生硬組合,會出現邏輯處理混亂的嚴重耦合現象,比如userService類既處理自己的使用者邏輯,還要處理郵箱等傳送的邏輯,這是不是也意味著,如果以後我們拓展更多的功能,我們的userService類還要出現更多的邏輯處理,來個大雜燴?,這同時還可能會為我們主要業務處理帶來不必要的阻塞。當然,為了防止阻塞,我們還可以建立新的執行緒來非同步,但這樣原來的類就顯得更加雜亂臃腫了。
  2. 使用spring事件機制能很好地幫助我們消除不同業務間的深耦合關係。它強大的任務排程還能幫助我們簡潔地實現事件非同步。

例項程式碼下載

相關推薦

spring事件:業務非同步呼叫

分析需求引入事件機制使用spring的事件機制有助於對我們的專案進一步的解耦。假如現在我們面臨一個需求: 我需要在使用者註冊成功的時候,根據使用者提交的郵箱、手機號資訊,向用戶傳送郵箱認證和手機號簡訊通知。傳統的做法之一是在我們的UserService層注入郵件

Hystrix斷路器+分散式系統面臨的問題+Hystrix介紹+服務熔斷介紹+服務熔斷案例+ 降級處理介紹+ 降級處理案例

 測試中使用到的程式碼到在這裡https://download.csdn.net/download/zhou920786312/10853300   轉載https://blog.csdn.net/qq_33404395/article/details/8091357

unity利用事件機制程式碼(四)

在某一個類呼叫另一個類裡的方法的時候,往往需要這個類的例項,這在繁複的專案中,往往沒有那麼方便,需要在這個類中不斷的通過物件圖語言導航到我們需要的地方。但如果使用事件機制的,在這個類裡發起一個事件,在另一個類了處理這個事件,就可以不需要這個例項物件,就能完成。 這裡有兩杯水

第二十七章:SpringBoot使用ApplicationEvent&Listener完成業務

ApplicationEvent以及Listener是Spring為我們提供的一個事件監聽、訂閱的實現,內部實現原理是觀察者設計模式,設計初衷也是為了系統業務邏輯之間的解耦,提高可擴充套件性以及可維護性。事件釋出者並不需要考慮誰去監聽,監聽具體的實現內容是什麼,

Spring事件和監聽器-同步非同步

Application下抽象子類ApplicationContextEvent的下面有4個已經實現好的事件  ContextClosedEvent(容器關閉時) ContextRefreshedEvent(容器重新整理是) ContextStartedEvent(容器啟動

spring 為何能

1. IoC理論的背景 我們都知道,在採用面向物件方法設計的軟體系統中,它的底層實現都是由N個物件組成的,所有的物件通過彼此的合作,最終實現系統的業務邏輯。 圖1:軟體系統中耦合的物件 如果我們開啟機械式手錶的後蓋,就會看到與上面類似的情形,各個齒輪分別帶動時針、分針和秒針

SpringBoot使用ApplicationEvent&Listener完成業務

ApplicationEvent以及Listener是Spring為我們提供的一個事件監聽、訂閱的實現,內部實現原理是觀察者設計模式,設計初衷也是為了系統業務邏輯之間的解耦,提高可擴充套件性以及可維護性。事件釋出者並不需要考慮誰去監聽,監聽具體的實現內容是什麼,釋出者的工作只

Spring的事務隔離級別傳播性

這篇文章以一個問題開始,如果你知道答案的話就可以跳過不看啦@(o・ェ・)@ Q:在一個批量任務執行的過程中,呼叫多個子任務時,如果有一些子任務發生異常,只是回滾那些出現異常的任務,而不是整個批量任務,請問在Spring中事務需要如何配置才能實現這一功能呢? 隔離級別 隔離性(Isolation)作為事務特性的

人工智慧 人臉識別 使用MQ實現以及非同步

從之前的人臉識別的文章來看,使用到mq中間處理的主要在捉拍機獲取到的人臉識別的特徵傳送到rabbitMQ,然後單張人臉註冊的服務進行消費,這時候就是實現了服務之間的非同步處理以及解耦的作用 還有之前的批量處理上傳的人臉特徵的服務,使用的是同步的方式,這種方式確實有點low,需要非同步來處理提

Spring Boot2.0之@Async實現非同步呼叫

 補充一個知識點: lombok底層原理使用的是: 位元組碼技術ASM修改位元組碼檔案,生成比如類似於get() set( )方法 一定要在開發工具安裝 在編譯時候修改位元組碼檔案(底層使用位元組碼技術),線上環境使用編譯好的檔案   下面我們學習 Spring Boot 非同步呼

Spring Boot中使用@Async實現非同步呼叫

一 點睛 1 什麼是“非同步呼叫” “非同步呼叫”對應的是“同步呼叫”,同步呼叫指程式按照定義順序依次執行,每一行程式都必須等待上一行程式執行完成之後才能執行;非同步呼叫指程式在順序執行時,不等待非

Zookeeper-Watcher機制非同步呼叫原理

atcher機制:目的是為ZK客戶端操作提供一種類似於非同步獲得資料的操作. 1)在建立Zookeeper例項時,允許接收一個watcher引數,此引數將會賦值給watchMnanger.defaultWatcher,成為當前客戶端的預設Watcher.需要注意此wa

Qt中的中訊號槽非同步呼叫

Qt中使用訊號-槽機制處理跨物件之間的呼叫,該機制的好處有: 1. 使得呼叫關係的繫結和解除十分靈活,不必修改類成員函式程式碼 2. 在不暴露更多全域性變數的情況下實現跨名稱空間呼叫 3. 可以多個訊號對應多個槽,也可以訊號之間繫結,對應於GUI中的邏輯很

同步呼叫非同步呼叫

同步呼叫和非同步呼叫是兩種提交任務的方式 同步呼叫:提交完任務後,就在原地等待任務執行完畢,拿到執行結果/返回值後再執行下一步,同步呼叫下任務是序列執行。 非同步呼叫:提交完任務後,不會再原地等待任務執行完畢,直接執行下一行程式碼,非同步呼叫時併發執行。 非同步呼叫,幾

【轉】Zookeeper-Watcher機制非同步呼叫原理

宣告:本文轉載自http://shift-alt-ctrl.iteye.com/blog/1847320,轉載請務必宣告。 Watcher機制:目的是為ZK客戶端操作提供一種類似於非同步獲得資料的操作. 1)在建立Zookeeper例項時,允許接收一個watc

python 程序池、執行緒池 非同步呼叫、回撥機制

程序池、執行緒池使用案例 程序池與執行緒池使用幾乎相同,只是呼叫模組不同~!! from concurrent.futures import ProcessPoolExecutor # 程序池模組 from concurrent.future

SpringBoot第十二集:度量指標監控非同步呼叫(2020最新最易懂)

SpringBoot第十二集:度量指標監控與非同步呼叫(2020最新最易懂)   Spring Boot Actuator是spring boot專案一個監控模組,提供了很多原生的端點,包含了對應用系統的自省和監控的整合功能,比如應用程式上下文裡全部的Bean、執行狀況檢查、健康指標、環境變數及各類重要度量指

spring原始碼汲取營養:模仿spring事件釋出機制,業務程式碼

前言 最近在專案中做了一項優化,對業務程式碼進行解耦。我們部門做的是警用系統,通俗的說,可理解為110報警。一條警情,會先後經過接警員、處警排程員、一線警員,警情是需要記錄每一步的日誌,是要可追溯的,比如報警人張小三在2019-12-02 00:02:01時間報警,接警員A在1分鐘後,將該警情記錄完成,並分派

mp-redux:小程式中的業務檢視,讓測試更容易

專案地址:點我,歡迎star和issue mp-redux 一個用於小程式和輕量級H5應用的狀態管理工具, 使用方法是一個簡化版本的Redux。之所以是適用於輕量級應用,主要是因為沒有實現元件間的資料共享。因此不適合於複雜,龐大的前端應用。 是否你需要使用它? 如果你也和我有同樣的困惑,那麼你就該嘗試

Android展現層業務層的資料

三層架構是一個非常經典的架構模式,根據系統的職責不同,將系統分成了展現層(主要用來UI展示以及觸發事件源)、業務層(主要用來實現UI事件源觸發的邏輯)、資料訪問層(主要用來進行資料訪問),並配合數模型據進行資料傳遞。三層架構對於大型團隊大型專案的並行開發遠遠不能成為支撐點。故又將三層架構進行細化,分為五層