在實際開發中,業務代碼與輔助代碼的解耦是一個熱點話題,如:通過AOP記錄入參出參、使用事件監聽記錄錯誤信息等是一個不錯的選擇。
概述:
事件的發佈與監聽從屬於觀察者模式;和MQ相比,事件的發佈與監聽偏向於處理“體系內”的某些邏輯。事件的發佈與監聽總體分爲以下幾個步驟:
步驟 | 相關事宜 |
---|---|
1 | 定義事件 |
2 | 定義(用於處理某種事件的)監聽器 |
3 | 註冊監聽器 |
4 | 發佈事件(,監聽到了該事件的監聽器自動進行相關邏輯處理) |
詳細(示例)說明:
第一步:通過繼承ApplicationEvent來自定義事件。
import org.springframework.context.ApplicationEvent;
/**
* 自定義事件
*
* 注:繼承ApplicationEvent即可。
*
* @author JustryDeng
* @date 2019/11/19 6:36
*/
public class MyEvent extends ApplicationEvent {
/**
* 構造器
*
* @param source
* 該事件的相關數據
*
* @date 2019/11/19 6:40
*/
public MyEvent(Object source) {
super(source);
}
}
注:構造器的參數爲該事件的相關數據對象,監聽器可以獲取到該數據對象,進而進行相關邏輯處理。
第二步:自定義監聽器。
- 方式一(推薦): 通過實現ApplicationListener來自定義監聽器,其中E爲此監聽器要監聽的事件。
注:需要重寫onApplicationEvent方法來自定義相關事件的處理邏輯。/** * 自定義監聽器 * * 注:實現ApplicationListener<E extends ApplicationEvent>即可, * 其中E爲此監聽器要監聽的事件。 * * @author JustryDeng * @date 2019/11/19 6:44 */ public class MyListenerOne implements ApplicationListener<MyEvent> { /** * 編寫處理事件的邏輯 * * @param event * 當前事件對象 */ @Override public void onApplicationEvent(MyEvent event) { /// 當前事件對象攜帶的數據 /// Object source = event.getSource(); System.out.println( "線程-【" + Thread.currentThread().getName() + "】 => " + "監聽器-【MyListenerOne】 => " + "監聽到的事件-【" + event + "】" ); } }
- 方式二: 在某個方法上使用@EventListener註解即可。
注:在某個方法上使用@EventListener註解即可。import org.springframework.context.event.EventListener; /** * 自定義監聽器 * * 注:在某個方法上使用@EventListener註解即可。 * 追注一: 這個方法必須滿足: 最多能有一個參數。 * 追注二: 若只是監聽一種事件,那麼這個方法的參數類型應爲該事 * 件對象類;P.S.:該事件的子類事件,也屬於該事件,也會被監聽到。 * 若要監聽多種事件,那麼可以通過@EventListener註解 * 的classes屬性指定多個事件,且保證這個方法無參; * * * * @author JustryDeng * @date 2019/11/19 6:44 */ public class MyListenerTwo { /** * 編寫處理事件的邏輯 * * @param event * 當前事件對象 */ @EventListener public void abc(MyEvent event) { /// 當前事件對象攜帶的數據 /// Object source = event.getSource(); System.out.println( "線程-【" + Thread.currentThread().getName() + "】 => " + "監聽器-【MyListenerTwo】 => " + "監聽到的事件-【" + event + "】" ); } }
注:被@EventListener註解方法必須滿足: 最多能有一個參數。
注:若只是監聽一種事件,那麼這個方法的參數類型應爲該事件對象類;
P.S.:該事件的子類事件,也屬於該事件,也會被監聽到。
注:若要監聽多種事件,那麼可以通過@EventListener註解的classes屬性指定多個事件,
且保證這個方法無參。
提示:還可通過設置@EventListener註解的condition屬性來對事件進行選擇性處理(P.S.:當然用代碼也能做到)。
第三步:註冊監聽器。
所謂註冊監聽器,其實質就是將監聽器進行IOC處理,讓容器管理監聽器的生命週期。 在SpringBoot中,有以下方式可以達到這些效果:
方式 | 具體操作 | 適用範圍 | 能否搭配@Async註解,進行異步監聽 | 優先級(即:當這三種方式註冊的(監聽同一類型事件的)監聽器都同時存在時,那麼一個事件發佈後,哪一種方式先監聽到) |
① | 在SpringBoot啓動類的main方法中,使用SpringApplication的addListeners方法進行註冊 提示:當在進行單元測試時,(由於不會走SpringBoot啓動的類main方法,所以)此方式不生效。 |
實現了ApplicationListener的監聽器 | 不能 | 取決於Bean被Ioc的先後順序。可通過設置@Order優先級的方式,來達到調整監聽器優先級的目的。
提示:實際開發中,考慮優先級的意義不大。 |
②(推薦) | 通過@Component或類似註解,將監聽器(或監聽器方法所在的)類IOC | 實現了ApplicationListener的監聽器以及通過@EventListener註解指定的監聽器 | 能 | |
③ | 在SpringBoot配置文件(.properties文件或.yml文件)中指定監聽器(或監聽器方法所在的)類
注:多個監聽器,使用逗號分割即可。 |
實現了ApplicationListener的監聽器 | 不能 |
-
方式①示例:
-
方式②示例:
或者 -
方式③示例:
注:爲了美觀,建議換行(在properties文件中使用\換行):
第四步:發佈事件,觸發監聽。
使用實現了ApplicationEventPublisher接口的類(常用ApplicationContext)的publishEvent(ApplicationEvent event)方法發佈事件。
注:SpringBoot啓動時,返回的ConfigurableApplicationContext是ApplicationContext的子類,所以如果想在SpringBoot啓動後就立馬發佈事件的話,可以這樣寫:
驗證測試:
- 按上述示例監聽邏輯,編寫示例:
- 運行main方法,啓動SpringBoot:
- 控制檯輸出:
SpringBoot中事件的發佈與監聽初步學習完畢!
同步監聽與異步監聽:
同步監聽:
按上文中的配置,實際上默認是同步監聽機制。所謂同步監聽,即:業務邏輯與監聽器的邏輯在同一個線程上、按順序執行。
-
舉例說明一:
假設某線程α,線程β都有發佈各自的事件,那麼α線程發佈的事件會被α線程進行監聽器邏輯處理,β線程發佈的事件會被β線程進行監聽器邏輯處理。 -
舉例說明二:
假設某線程β要做的總體邏輯流程是,做A => 發事件x => 做B => 發事件y => 發事件z => 返回響應,那麼同步監聽下是這樣的: 線程β先做A,(A做完後)接着做事件x對應的監聽器邏輯,(x的監聽器邏輯做完後,線程β才能)接着做B,(B做完後)接着做事件y對應的監聽器邏輯,(y的監聽器邏輯做完後,線程β才能)接着做事件z對應的監聽器邏輯,最後才能返回響應。
異步監聽:
如果需要異步監聽,那麼需要開啓異步功能(見下文示例),所謂異步監聽即:業務邏輯與監聽器的邏輯不在同一個線程上,處理監聽器邏輯的事會被線程池中的某些線程異步併發着做。
- 舉例說明:
假設某線程β要做的總體邏輯流程是,做A => 發事件x => 做B => 發事件y => 發事件z => 返回響應,那麼異步監聽下是這樣的:線程β先做A,(A做完後)發佈事件x,(線程β不管x的監聽器邏輯)緊接着做B,(B做完後,線程β)接着發佈事件y,(線程β不管y的監聽器邏輯)緊接着發佈事件z,(線程β不管z的監聽器邏輯)緊接着直接返回響應。而線程β發佈的事件對應的各個監聽器邏輯,會由線程池中的某些線程異步併發着做。
開啓異步功能:
- 第一步:@EnableAsync啓用異步功能。
- 第二步:@Async指定異步方法。
- 第三步(可選): 自定義配置線程池執行器Executor(提示:配置Executor後,在使用@Async註解時,可以通過設置其屬性來指定使用哪一個Executor)。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; /** * 自定義線程池Executor。 * * 注: 我們常用的ExecutorService就繼承自Executor。 * * 注:關於線程池的各個參數的介紹、各個參數的關係,可詳見<linked>https://blog.csdn.net/justry_deng/article/details/89331199</linked> * * @author JustryDeng * @date 2019/11/25 11:05 */ @Configuration public class SyncExecutor { /** 核心線程數 */ private static final int CORE_POOL_SIZE = 5; /** 最大線程數 */ private static final int MAX_POOL_SIZE = 100; /** 阻塞隊列容量 */ private static final int QUEUE_CAPACITY = 20; @Bean public Executor myAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(CORE_POOL_SIZE); executor.setMaxPoolSize(MAX_POOL_SIZE); executor.setQueueCapacity(QUEUE_CAPACITY); executor.setThreadNamePrefix("JustryDeng-Executor-"); // 設置,當任務滿額時將新任務(如果有的話),打回到原線程去執行。 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }
注:@Async指定異步方法時,就可以選擇使用哪一個線程池Executor了,如:
同步監聽與異步監聽的比較:
SpringBoot中事件的發佈與監聽學習完畢!
^_^ 如有不當之處,歡迎指正
^_^ 測試代碼託管鏈接
https://github.com/JustryDeng…Abc_EventListener_Demo
^_^ 本文已經被收錄進《程序員成長筆記(六)》,筆者JustryDeng