掌握 Spring 之事件處理
- 2019 年 10 月 5 日
- 筆記
1 前言2.1 Spring 標準事件處理2.1.1 註解驅動的事件偵聽引入 @EventListener事件的傳遞2.1.2 偵聽器優先順序2.2 自定義事件2.2.1 自定義 Application Event2.2.2 自定義 Publisher2.2.3 自定義 Listener2.2.4 非同步事件處理2.3 Spring Boot 事件與偵聽3 結語4 參考
1 前言
本次我們來學習 Spring 的事件處理,源於實際工作中遇到的項目需求:在一個支付的下單場景中,當用戶真正支付成功,伺服器收到回調後就需要及時更新訂單數據狀態來保證數據一致。通常做法就是在回調方法里直接使用訂單服務更新數據, 然而這樣實現上兩個模組出現了緊密耦合,如果訂單更新的操作需要進行調整,那麼在支付回調的程式碼塊中也需要被修改。
為了避免這樣情況發生,我採用了 Spring 事件發布與訂閱的方式來實現接受支付回調,發布通知更新訂單狀態的這個功能,讓訂單服務更新數據的操作只依賴特定的事件,而不用關心具體的觸發對象,也能達到程式碼復用的目的。
本文主要內容涉及如下:
- Spring 標準事件的處理
- Spring 中自定義事件擴展實現
- Spring Boot 的事件與偵聽
示例項目:
- spring-events:https://github.com/wrcj12138aaa/spring-events
環境支援:
- JDK 8
- SpringBoot 2.1.4
- Maven 3.6.0
2.1 Spring 標準事件處理
Spring 程式啟動過程中會有不同的事件通知,內置標準的事件有 5 種:
當 Spring 容器處於初始化或者刷新階段時就會觸發,事實是ApplicationContext#refresh()方法被調用時,此時容器已經初始化完畢。
知道了 Spring 自帶的事件有哪些後,我們就可以針對一些場景利用事件機制來實現需求,比如說在 Spring 啟動後初始化資源,載入快取數據到記憶體中等等。程式碼實現也很簡單,如下:通常情況下,Spring 程式都會接收到 ContextRefreshedEvent
, ContextClosedEvent
事件的通知。
@Component public class InitalizeListener implements ApplicationListener<ContextRefreshedEvent> { @Override public void onApplicationEvent(ContextRefreshedEvent event) { ApplicationContext applicationContext = event.getApplicationContext(); System.out.println("Spring 容器啟動 獲取到 Application Context 對象 " + applicationContext); //TODO 初始化資源,載入快取數據到記憶體 } } // 啟動 Spring 程式後,控制台出現如下日誌: // Spring 容器啟動 獲取到 Application Context 對象 org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@6950ed69, started on Sun May 26 12:19:33 CST 2019
我們可以從 ContextRefreshedEvent
事件中獲取到 ApplicationContext
對象,從而獲取 Spring 容器中任何已經裝載的 Bean 進行自定義的操作。
2.1.1 註解驅動的事件偵聽
引入 @EventListener
從 Spring 4.2 開始,Spring 又提供了更靈活的,註解驅動的事件偵聽處理方式。主要使用 @EventListener
註解來標記需要監聽程式事件的方法,底層由 EventListenerMethodProcessor
對象將標註的方法轉為成 ApplicationListener
實例。
為什麼說這個註解方式偵聽事件更加靈活呢,我們可以先看下 @EventListener
註解的源碼。
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface EventListener { @AliasFor("classes") Class<?>[] value() default {}; @AliasFor("value") Class<?>[] classes() default {}; String condition() default ""; }
EventListener
註解主要有兩個屬性:classes
和 condition
。 classes
表示所需要偵聽的事件類型,是個數組,所以允許在單個方法里進行多個不同事件的偵聽,以此做到復用的效果;condition
顧名思義就是用來定義所偵聽事件是否處理的前置條件,這裡需要注意的是使用 Spring Expression Language (SpEL)定義條件,比如 #root.event
表示了具體的 ApplicationEvent
對象, 使用方式可以參考下方示例程式碼:
@Component public class AnnotationListener { @EventListener(value = {ContextRefreshedEvent.class, ContextStartedEvent.class, ContextStoppedEvent.class, ContextClosedEvent.class, RequestHandledEvent.class}, condition = "#root.event != null") public void listener(ApplicationEvent event) { System.out.println(Thread.currentThread() + " 接收到 Spring 事件:" + event); } }
這裡需要注意的是,註解 @EventListener
標記的方法參數類型不再限制必須是 ApplicationEvent
的子類,沒有實現 ApplicationListener
介面方法的約束,也讓事件變得更加靈活。
事件的傳遞
另外,使用 @EventListener
還支援事件的傳遞,將當前事件處理好的結果封裝後發布一個新的事件,實現的方式就是讓偵聽方法返回非 null
值時,就視為事件繼續傳播,如下面的示例程式碼:
@Component @Order(2) public class CustomEventListener { @EventListener public SecondCustomEvent listener(CustomEvent event) { System.out.println(Thread.currentThread() + "CustomEventListener接受到自定義事件:" + event); return new SecondCustomEvent(this, event.toString()); } }
2.1.2 偵聽器優先順序
當我們對單個事件存在多個偵聽器時,可能會由於需求想要指定偵聽器的執行順序,這一點 Spring 也為我們考慮到了,只要使用 @Order
註解聲明監聽類或者監聽方法即可,根據 @Order
的 value
大小來確定執行順序,越小越優先執行。
@EventListener @Order(42) public void processEvent(Event event) { }
2.2 自定義事件
在了解如何偵聽 Spring 事件後,我們再來看下如何實現自定義的事件發布和偵聽處理。首先就要介紹 Spring 中事件機制的三類對象:
Event
:所需要觸發的具體事件對象,通常擴展ApplicationEvent
實現。Publisher
:觸發事件發布的對象,Spring 提供了ApplicationEventPublisher
對象供我們使用,使用它的publishEvent()
方法就可以發布該事件。Listener
:偵聽事件發生的對象,也就是接受回調進行處理的地方,可以通過 實現ApplicationListener
介面,或者使用前面提到的@EventListener
註解聲明為事件的偵聽器。
接下來就簡單看下,一個自定義事件從聲明到發布訂閱的程式碼示例。
2.2.1 自定義 Application Event
public class CustomEvent extends ApplicationEvent { private String data; public CustomEvent(Object source, String data) { super(source); this.data = data; } public String getData() { return data; } public void setData(String data) { this.data = data; } @Override public String toString() { return "CustomEvent{" + "data='" + data + ''' + ", source=" + source + '}'; } }
2.2.2 自定義 Publisher
@Component public class CustomeEventPublisher implements ApplicationEventPublisherAware { private ApplicationEventPublisher applicationEventPublisher; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } public void publishEvent(String message) { System.out.println("開始發布事件 " + message); applicationEventPublisher.publishEvent(new CustomEvent(this, message)); } }
創建事件發布者有兩種方式,一種是使用 @Autowire
註解,通過 Spring 容器的依賴注入功能,直接注入 ApplicationEventPublisher
對象,或者實現 ApplicationEventPublisherAware
介面,在 Spring 容器啟動時由 Spring 設置。
2.2.3 自定義 Listener
@Component public class CustomEventListener implements ApplicationListener<CustomEvent> { @Override public void onApplicationEvent(CustomEvent event) { System.out.println(Thread.currentThread()+"CustomEventListener接受到自定義事件:" + event); } }
定義事件偵聽器時,我們通過實現 ApplicationListener
介面,指定了事件類型,這樣在處理事件時就不避免了事件類型判斷和轉換。
關於事件偵聽器還需要注意的一點是:Spring 事件處理默認是同步的,這一點在 Spring 官方文檔所有提及,我們先解讀下官方描述:
You can register as many event listeners as you wish, but note that, by default, event listeners receive events synchronously. This means that the
publishEvent()
method blocks until all listeners have finished processing the event. One advantage of this synchronous and single-threaded approach is that, when a listener receives an event, it operates inside the transaction context of the publisher if a transaction context is available. If another strategy for event publication becomes necessary, See the javadoc for Spring』sApplicationEventMulticaster
interface.
當發布者執行了 publishEvent()
方法,默認情況下方法所在的當前執行緒就會阻塞,直到所有該事件相關的偵聽器將事件處理完成。而這樣採用單執行緒同步方式處理的好處主要是可以保證讓事件處理與發布者處於同一個事務環境里,如果多個偵聽方法涉及到資料庫操作時保證了事務的存在。
2.2.4 非同步事件處理
當然 Spring 也提供了非同步偵聽事件的方式,這裡主要依賴 ApplicationEventMulticaster
介面,可以理解為廣播方式,為了便於使用,Spring 提供一個簡易的實現類 SimpleApplicationEventMulticaster
供我們直接使用,只需要將這個對象註冊到 Spring 容器即可。
@Configuration public class AsynchronousSpringEventsConfig { @Bean(name = "applicationEventMulticaster") public ApplicationEventMulticaster simpleApplicationEventMulticaster() { SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster(); eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor()); return eventMulticaster; } }
這裡 ApplicationEventMulticaster
Bean 需要一個 java.util.concurrent.Executor
對象作為事件處理的執行緒池,我們直接使用 Spring 提供的 SimpleAsyncTaskExecutor
對象,每次事件處理都會有創建新的執行緒。
注意:註冊
ApplicationEventMulticaster
Bean 後所有的事件偵聽處理都會變成的非同步形式,如果需要針對特定的事件偵聽採用非同步方式的話:可以使用@EventListener
和@Async
組合來實現。(前提是 Spring 程式啟用@EnableAsync
註解)
這裡再提下使用非同步方式處理事件的利弊,好處在於讓我們程式在處理事件更加有效率,而缺點就在針對異常發生的處理更加複雜,需要藉助 AsyncUncaughtExceptionHandler
介面實現。
2.3 Spring Boot 事件與偵聽
學習了那麼多 Spring Framework 的事件處理相關的內容後,我們現在再來看看在 Spring Boot 里事件處理有什麼需要額外學習的地方。還是一樣,我們先從 Spring Boot 官方文檔下手,在 Spring Boot
Doc 的 23.5 Application Events and Listeners 一節中提到了事件處理:
- In addition to the usual Spring Framework events, such as
ContextRefreshedEvent
, aSpringApplication
sends some additional application events. - Application events are sent by using Spring Framework』s event publishing mechanism.
可以看出 Spring Boot 仍是基於 Spring Framework 的事件發布機制去處理事件,只是在此基礎了新增了幾個 SpringApplication
相關的事件:
ApplicationStartingEvent
:程式啟動時發生。ApplicationEnvironmentPreparedEvent
:程式中Environment
對象就緒時發生。ApplicationPreparedEvent
:程式啟動後但還未刷新時發生。ApplicationStartedEvent
:程式啟動刷新後發生。ApplicationReadyEvent
:程式啟動完畢,等待請求時發生。ApplicationFailedEvent
:程式啟動過程中出現異常時發生。
並且它們的執行順序也是列舉書順序依次觸發的。
另外,需要注意的是,當需要觸發的事件是在 ApplicationContext
創建之前發生時,用 @Bean
方式註冊的偵聽器就不會執行,而 Spring Boot 為此提供了三種方式來處理這種情況:
- 使用
SpringApplication.addListeners(…)
方法註冊偵聽器 SpringApplication springApplication = new SpringApplication(SpringEventsApplication.class); springApplication.addListeners(new NormalCustomEventListener()); springApplication.run(args); - 使用
SpringApplicationBuilder.listeners(…)
方法註冊偵聽器 SpringApplicationBuilder springApplicationBuilder = new SpringApplicationBuilder(SpringEventsApplication.class); springApplicationBuilder.listeners(new NormalCustomEventListener()).run(args); - 在應用資源文件夾新建文件
META-INF/spring.factories
,並將org.springframework.context.ApplicationListener
作為鍵,指定需要註冊的偵聽器類,如:org.springframework.context.ApplicationListener=
com.one.learn.spring.springevents.listener.NormalSecondCutomEventListener
3 結語
到這裡我們學習 Spring 事件相關的內容就結束了,了解 Spring 的事件機制,並適當應用,可以為我們完成程式的某個功能時提供一個更加解耦,靈活的實現方式。
如果讀完覺得有收穫的話,歡迎點【好看】,點擊文章頭圖,掃碼關注【聞人的技術部落格】???。
4 參考
Spring context-functionality-events: https://docs.spring.io/spring/docs/5.1.6.RELEASE/spring-framework-reference/core.html#context-functionality-events
Spring boot-features-application-events-and-listeners:https://docs.spring.io/spring-boot/docs/2.1.4.RELEASE/reference/htmlsingle/#boot-features-application-events-and-listeners
Spring Expression Language: https://docs.spring.io/spring/docs/4.3.10.RELEASE/spring-framework-reference/html/expressions.html
SpringEvents: https://www.baeldung.com/spring-events
Better application events in Spring Framework 4.2: https://spring.io/blog/2015/02/11/better-application-events-in-spring-framework-4-2