趣談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就收到響應了。
郵件傳送成功! —-這個時候郵箱才發完
簡訊傳送成功!從以上的測試結果我們,我們的郵箱傳送和簡訊傳送都分別單獨地非同步完成了,大大縮短了我們主業務處理事件,也提高了使用者體驗
小結
- 從本例可以看出,不同業務功能的生硬組合,會出現邏輯處理混亂的嚴重耦合現象,比如userService類既處理自己的使用者邏輯,還要處理郵箱等傳送的邏輯,這是不是也意味著,如果以後我們拓展更多的功能,我們的userService類還要出現更多的邏輯處理,來個大雜燴?,這同時還可能會為我們主要業務處理帶來不必要的阻塞。當然,為了防止阻塞,我們還可以建立新的執行緒來非同步,但這樣原來的類就顯得更加雜亂臃腫了。
- 使用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事件源觸發的邏輯)、資料訪問層(主要用來進行資料訪問),並配合數模型據進行資料傳遞。三層架構對於大型團隊大型專案的並行開發遠遠不能成為支撐點。故又將三層架構進行細化,分為五層