SpringBoot | 第三十二章:事件的釋出和監聽
前言
今天去官網檢視
spring boot
資料時,在特性中看見了系統的事件及監聽
章節。想想,spring
的事件應該是在3.x
版本就釋出的功能了,並越來越完善,其為bean
和bean
之間的訊息通訊提供了支援。比如,我們可以在使用者註冊成功後,傳送一份註冊成功的郵件至使用者郵箱或者傳送簡訊。使用事件其實最大作用,應該還是為了業務解耦,畢竟使用者註冊成功後,註冊服務的事情就做完了,只需要釋出一個使用者註冊成功的事件,讓其他監聽了此事件的業務系統去做剩下的事件就好了。對於事件釋出者而言,不需要關心誰監聽了該事件,以此來解耦業務。今天,我們就來講講spring boot
中事件的使用和釋出。當然了,也可以使用像guava
的eventbus
或者非同步框架Reactor
來處理此類業務需求的。本文僅僅談論ApplicationEvent
以及Listener
的使用。
一點知識
示例前,我們來了解下相關知識點。
Java的事件機制
java中的事件機制一般包括3個部分:EventObject
,EventListener
Source
。
EventObject
java.util.EventObject是事件狀態物件的基類,它封裝了事件源物件以及和事件相關的資訊。所有java的事件類都需要繼承該類。
EventListener
java.util.EventListener是一個標記介面,就是說該介面內是沒有任何方法的。所有事件監聽器都需要實現該介面。事件監聽器註冊在事件源上,當事件源的屬性或狀態改變的時候,呼叫相應監聽器內的回撥方法。
Source
事件源不需要實現或繼承任何介面或類,它是事件最初發生的地方。因為事件源需要註冊事件監聽器,所以事件源內需要有相應的盛放事件監聽器的容器。
java
Spring的事件
ApplicationEvent
以及Listener
是Spring
為我們提供的一個事件監聽、訂閱的實現,內部實現原理是觀察者設計模式,設計初衷也是為了系統業務邏輯之間的解耦,提高可擴充套件性以及可維護性。
ApplicationEvent
就是Spring
的事件介面ApplicationListener
就是Spring
的事件監聽器介面,所有的監聽器都實現該介面ApplicationEventPublisher
是Spring
的事件釋出介面,ApplicationContext
實現了該介面ApplicationEventMulticaster
就是Spring
事件機制中的事件廣播器,預設實現SimpleApplicationEventMulticaster
在Spring
中通常是ApplicationContext
本身擔任監聽器登錄檔的角色,在其子類AbstractApplicationContext
中就聚合了事件廣播器ApplicationEventMulticaster
和事件監聽器ApplicationListnener
,並且提供註冊監聽器的addApplicationListnener
方法。
其執行的流程大致為:
當一個事件源產生事件時,它通過事件釋出器
ApplicationEventPublisher
釋出事件,然後事件廣播器ApplicationEventMulticaster
會去事件登錄檔ApplicationContext
中找到事件監聽器ApplicationListnener
,並且逐個執行監聽器的onApplicationEvent
方法,從而完成事件監聽器的邏輯。
在Spring
中,使用註冊監聽介面,除了繼承ApplicationListener
介面外,還可以使用註解@EventListener
來監聽一個事件,同時該註解還支援SpEL
表示式,來觸發監聽的條件,比如只接受編碼為001
的事件,從而實現一些個性化操作。下文示例中會簡單舉例下。
簡單來說,在Java中,通過java.util. EventObject來描述事件,通過java.util. EventListener來描述事件監聽器,在眾多的框架和元件中,建立一套事件機制通常是基於這兩個介面來進行擴充套件。
SpringBoot的預設啟動事件
在
SpringBoot
的1.5.x
中,提供了幾種事件,供我們在開發過程中進行更加便捷的擴充套件及差異化操作。
ApplicationStartingEvent
:springboot啟動開始的時候執行的事件ApplicationEnvironmentPreparedEvent
:spring boot
對應Enviroment已經準備完畢,但此時上下文context
還沒有建立。在該監聽中獲取到ConfigurableEnvironment
後可以對配置資訊做操作,例如:修改預設的配置資訊,增加額外的配置資訊等等。ApplicationPreparedEvent
:spring boot
上下文context
建立完成,但此時spring
中的bean
是沒有完全載入完成的。在獲取完上下文後,可以將上下文傳遞出去做一些額外的操作。值得注意的是:在該監聽器中是無法獲取自定義bean並進行操作的。ApplicationReadyEvent
:springboot
載入完成時候執行的事件。ApplicationFailedEvent
:spring boot
啟動異常時執行事件。
從官網文件中,我們可以知道,由於一些事件實在上下文為載入完觸發的,所以無法使用註冊bean
的方式來宣告,文件中可以看出,可以通過SpringApplication.addListeners(…)
或者SpringApplicationBuilder.listeners(…)
來新增,或者新增META-INF/spring.factories
檔案z中新增監聽類也是可以的,這樣會自動載入。
org.springframework.context.ApplicationListener=com.example.project.MyListener
啟動類中新增:
@SpringBootApplication
public class Application {
public static void main(String[] args){
SpringApplication app =new SpringApplication(Application.class);
app.addListeners(new MyApplicationStartingEventListener());//加入自定義的監聽類
app.run(args);
}
}
所以在需要的時候,可以通過適當的監聽以上事件,來完成一些業務操作。
自定義事件釋出和監聽
通過以上的介紹,我們來定義一個自定義事件的釋出和監聽。
0.加入POM依賴,這裡為了演示加入了web
依賴。事件相關類都在spring-context
包下。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
1.自定義事件源和實體。
MessageEntity.java
/**
* 訊息實體類
* @author oKong
*
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageEntity {
String message;
String code;
}
CustomEvent.java
/**
* 編寫事件源
* @author oKong
*
*/
@SuppressWarnings("serial")
public class CustomEvent extends ApplicationEvent{
private MessageEntity messageEntity;
public CustomEvent(Object source, MessageEntity messageEntity) {
super(source);
this.messageEntity = messageEntity;
}
public MessageEntity getMessageEntity() {
return this.messageEntity;
}
}
2.編寫監聽類
使用@EventListener
方式。
/**
* 監聽配置類
*
* @author oKong
*
*/
@Configuration
@Slf4j
public class EventListenerConfig {
@EventListener
public void handleEvent(Object event) {
//監聽所有事件 可以看看 系統各類時間 釋出了哪些事件
//可根據 instanceof 監聽想要監聽的事件
// if(event instanceof CustomEvent) {
//
// }
log.info("事件:{}", event);
}
@EventListener
public void handleCustomEvent(CustomEvent customEvent) {
//監聽 CustomEvent事件
log.info("監聽到CustomEvent事件,訊息為:{}, 釋出時間:{}", customEvent.getMessageEntity(), customEvent.getTimestamp());
}
/**
* 監聽 code為oKong的事件
*/
@EventListener(condition="#customEvent.messageEntity.code == 'oKong'")
public void handleCustomEventByCondition(CustomEvent customEvent) {
//監聽 CustomEvent事件
log.info("監聽到code為'oKong'的CustomEvent事件,訊息為:{}, 釋出時間:{}", customEvent.getMessageEntity(), customEvent.getTimestamp());
}
@EventListener
public void handleObjectEvent(MessageEntity messageEntity) {
//這個和eventbus post方法一樣了
log.info("監聽到物件事件,訊息為:{}", messageEntity);
}
}
注意:Spring
中,事件源不強迫繼承ApplicationEvent
介面的,也就是可以直接釋出任意一個物件類。但內部其實是使用PayloadApplicationEvent
類進行包裝了一層。這點和guava
的eventBus
類似。
而且,使用@EventListener
的condition
可以實現更加精細的事件監聽,condition
支援SpEL
表示式,可根據事件源的引數來判斷是否監聽。
使用ApplicationListener
方式。
@Component
@Slf4j
public class EventListener implements ApplicationListener<CustomEvent>{
@Override
public void onApplicationEvent(CustomEvent event) {
//這裡也可以監聽所有事件 使用 ApplicationEvent 類即可
//這裡僅僅監聽自定義事件 CustomEvent
log.info("ApplicationListener方式監聽事件:{}", event);
}
}
3.編寫控制類,示例釋出事件。
/**
* 模擬觸發事件
* @author oKong
*
*/
@RestController
@RequestMapping("/push")
@Slf4j
public class DemoController {
/**
* 注入 事件釋出類
*/
@Autowired
ApplicationEventPublisher eventPublisher;
@GetMapping
public String push(String code,String message) {
log.info("釋出applicationEvent事件:{},{}", code, message);
eventPublisher.publishEvent(new CustomEvent(this, MessageEntity.builder().code(code).message(message).build()));
return "事件釋出成功!";
}
@GetMapping("/obj")
public String pushObject(String code,String message) {
log.info("釋出物件事件:{},{}", code, message);
eventPublisher.publishEvent(MessageEntity.builder().code(code).message(message).build());
return "物件事件釋出成功!";
}
}
4.編寫啟動類。
/**
* 事件監聽
*
* @author oKong
*
*/
@SpringBootApplication
@Slf4j
public class EventAndListenerApplication {
public static void main(String[] args) throws Exception {
SpringApplication app =new SpringApplication(EventAndListenerApplication.class);
app.addListeners(new MyApplicationStartingEventListener());//加入自定義的監聽類
app.run(args);
log.info("spring-boot-event-listener-chapter32啟動!");
}
}
這裡,建立了個ApplicationStartingEvent
事件監聽類。
/**
* 示例-啟動事件
* @author oKong
*
*/
public class MyApplicationStartingEventListener implements ApplicationListener<ApplicationStartingEvent>{
@Override
public void onApplicationEvent(ApplicationStartingEvent event) {
// TODO Auto-generated method stub
//由於 log相關還未載入 使用了也輸出不了的
// log.info("ApplicationStartingEvent事件釋出:{}", event);
System.out.println("ApplicationStartingEvent事件釋出:" + event.getTimestamp());
}
}
5.啟動應用,控制檯可以看出,在啟動時,我們監聽到了ApplicationStartingEvent
事件
首先訪問下:http://127.0.0.1:8080/push?code=lqdev&message=趔趄的猿
,可以看見事件已經被監聽到了,而監聽了code
為oKong
的監聽未觸發。
然後訪問下:http://127.0.0.1:8080/push?code=oKong&message=趔趄的猿
,可以看見此時三個監聽事件都接收到了事件了。
此時,由於寫了一個監聽所有事件的方法,可以看見請求結束後,會發佈一個事件ServletRequestHandledEvent
,裡面記錄了請求的時間、請求url、請求方式等等資訊。
事件:ServletRequestHandledEvent: url=[/push]; client=[127.0.0.1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]
非同步監聽處理
預設情況下,監聽事件都是同步執行的。在需要非同步處理時,可以在方法上加上@Async
進行非同步化操作。此時,可以定義一個執行緒池,同時開啟非同步功能,加入@EnableAsync
。
對於非同步處理,可以檢視之前釋出的文章:《第二十一章:非同步開發之非同步呼叫》。裡面有詳細的介紹非同步呼叫,這裡就不闡述了。
非同步簡單示例:
/**
* 監聽 code為oKong的事件
*/
@Async
@EventListener(condition="#customEvent.messageEntity.code == 'oKong'")
public void handleCustomEventByCondition(CustomEvent customEvent) {
//監聽 CustomEvent事件
log.info("監聽到code為'oKong'的CustomEvent事件,訊息為:{}, 釋出時間:{}", customEvent.getMessageEntity(), customEvent.getTimestamp());
}
關於事務繫結事件
當一些場景下,比如在使用者註冊成功後,即資料庫事務提交了,之後再非同步傳送郵件等,不然會發生資料庫插入失敗,但事件卻釋出了,也就是郵件傳送成功了的情況。此時,我們可以使用@TransactionalEventListener
註解或者TransactionSynchronizationManager
類來解決此類問題,也就是:事務成功提交後,再發布事件。當然也可以利用返回上層(事務提交後)再發布事件的方式了,只是不夠優雅而已罷了,其實能起作用就好了,是吧~
本例中未使用到資料庫,就不示例了,都在Spring-tx
包下。
具體可檢視文章:Spring Event 事件中的事務控制
參考資料
總結
本章節主要簡單介紹了
spring
的事件機制。感興趣的同學,可以編寫一個監聽所有事件的方法,然後看看系統執行各類請求或者相關操作時,系統會發布哪些事件,瞭解後可以在之後碰見一些特殊業務需求時,可以適當的監聽相關的事件來完成特定的業務公共。同時對這種觀察者模式,大家還可以看看eventbus
和reactor
了。後者沒用過,有時間倒是可以看看。最近買了本RxJava2
書籍,確實要好好補課下了。
最後
目前網際網路上很多大佬都有
SpringBoot
系列教程,如有雷同,請多多包涵了。原創不易,碼字不易,還希望大家多多支援。若文中有所錯誤之處,還望提出,謝謝。
老生常談
- 個人QQ:
499452441
- 微信公眾號:
lqdevOps
個人部落格:http://blog.lqdev.cn
完整示例:https://github.com/xie19900123/spring-boot-learning/tree/master/chapter-32
原文地址:https://blog.lqdev.cn/2018/11/06/springboot/chapter-thirty-two/