原文出處: oKong
前言
今天去官網(wǎng)查看
spring boot
資料時惨险,在特性中看見了系統(tǒng)的事件及監(jiān)聽
章節(jié)讹语。想想搞糕,spring
的事件應(yīng)該是在3.x
版本就發(fā)布的功能了勇吊,并越來越完善,其為bean
和bean
之間的消息通信提供了支持窍仰。比如汉规,我們可以在用戶注冊成功后,發(fā)送一份注冊成功的郵件至用戶郵箱或者發(fā)送短信驹吮。使用事件其實最大作用针史,應(yīng)該還是為了業(yè)務(wù)解耦,畢竟用戶注冊成功后碟狞,注冊服務(wù)的事情就做完了啄枕,只需要發(fā)布一個用戶注冊成功的事件,讓其他監(jiān)聽了此事件的業(yè)務(wù)系統(tǒng)去做剩下的事件就好了族沃。對于事件發(fā)布者而言频祝,不需要關(guān)心誰監(jiān)聽了該事件,以此來解耦業(yè)務(wù)竭业。今天智润,我們就來講講spring boot
中事件的使用和發(fā)布。當(dāng)然了未辆,也可以使用像guava
的eventbus
或者異步框架Reactor
來處理此類業(yè)務(wù)需求的窟绷。本文僅僅談?wù)?code>ApplicationEvent以及Listener
的使用。
一點知識
示例前咐柜,我們來了解下相關(guān)知識點兼蜈。
Java的事件機(jī)制
java中的事件機(jī)制一般包括3個部分:EventObject
,EventListener
和Source
拙友。
EventObject
java.util.EventObject是事件狀態(tài)對象的基類为狸,它封裝了事件源對象以及和事件相關(guān)的信息。所有java的事件類都需要繼承該類遗契。
EventListener
java.util.EventListener是一個標(biāo)記接口辐棒,就是說該接口內(nèi)是沒有任何方法的。所有事件監(jiān)聽器都需要實現(xiàn)該接口牍蜂。事件監(jiān)聽器注冊在事件源上漾根,當(dāng)事件源的屬性或狀態(tài)改變的時候,調(diào)用相應(yīng)監(jiān)聽器內(nèi)的回調(diào)方法鲫竞。
Source
事件源不需要實現(xiàn)或繼承任何接口或類辐怕,它是事件最初發(fā)生的地方。因為事件源需要注冊事件監(jiān)聽器从绘,所以事件源內(nèi)需要有相應(yīng)的盛放事件監(jiān)聽器的容器寄疏。
java
的事件機(jī)制是一個觀察者模式是牢。大家可以根據(jù)這個模式,自己實現(xiàn)一個陕截〔道猓可以看看這篇博文:《java事件機(jī)制》一個很簡單的實例。
Spring的事件
ApplicationEvent
以及Listener
是Spring
為我們提供的一個事件監(jiān)聽艘策、訂閱的實現(xiàn)蹈胡,內(nèi)部實現(xiàn)原理是觀察者設(shè)計模式,設(shè)計初衷也是為了系統(tǒng)業(yè)務(wù)邏輯之間的解耦朋蔫,提高可擴(kuò)展性以及可維護(hù)性罚渐。
-
ApplicationEvent
就是Spring
的事件接口 -
ApplicationListener
就是Spring
的事件監(jiān)聽器接口,所有的監(jiān)聽器都實現(xiàn)該接口 -
ApplicationEventPublisher
是Spring
的事件發(fā)布接口驯妄,ApplicationContext
實現(xiàn)了該接口 -
ApplicationEventMulticaster
就是Spring
事件機(jī)制中的事件廣播器荷并,默認(rèn)實現(xiàn)SimpleApplicationEventMulticaster
在Spring
中通常是ApplicationContext
本身擔(dān)任監(jiān)聽器注冊表的角色,在其子類AbstractApplicationContext
中就聚合了事件廣播器ApplicationEventMulticaster
和事件監(jiān)聽器ApplicationListnener
青扔,并且提供注冊監(jiān)聽器的addApplicationListnener
方法源织。
其執(zhí)行的流程大致為:
當(dāng)一個事件源產(chǎn)生事件時,它通過事件發(fā)布器
ApplicationEventPublisher
發(fā)布事件微猖,然后事件廣播器ApplicationEventMulticaster
會去事件注冊表ApplicationContext
中找到事件監(jiān)聽器ApplicationListnener
谈息,并且逐個執(zhí)行監(jiān)聽器的onApplicationEvent
方法,從而完成事件監(jiān)聽器的邏輯凛剥。
在Spring
中侠仇,使用注冊監(jiān)聽接口,除了繼承ApplicationListener
接口外犁珠,還可以使用注解@EventListener
來監(jiān)聽一個事件逻炊,同時該注解還支持SpEL
表達(dá)式,來觸發(fā)監(jiān)聽的條件犁享,比如只接受編碼為001
的事件余素,從而實現(xiàn)一些個性化操作。下文示例中會簡單舉例下炊昆。
簡單來說桨吊,在Java中,通過java.util. EventObject來描述事件凤巨,通過java.util. EventListener來描述事件監(jiān)聽器屏积,在眾多的框架和組件中,建立一套事件機(jī)制通常是基于這兩個接口來進(jìn)行擴(kuò)展磅甩。
SpringBoot的默認(rèn)啟動事件
在
SpringBoot
的1.5.x
中,提供了幾種事件姥卢,供我們在開發(fā)過程中進(jìn)行更加便捷的擴(kuò)展及差異化操作卷要。
ApplicationStartingEvent
:springboot啟動開始的時候執(zhí)行的事件ApplicationEnvironmentPreparedEvent
:spring boot
對應(yīng)Enviroment已經(jīng)準(zhǔn)備完畢渣聚,但此時上下文context
還沒有創(chuàng)建。在該監(jiān)聽中獲取到ConfigurableEnvironment
后可以對配置信息做操作僧叉,例如:修改默認(rèn)的配置信息奕枝,增加額外的配置信息等等。ApplicationPreparedEvent
:spring boot
上下文context
創(chuàng)建完成瓶堕,但此時spring
中的bean
是沒有完全加載完成的隘道。在獲取完上下文后,可以將上下文傳遞出去做一些額外的操作郎笆。值得注意的是:在該監(jiān)聽器中是無法獲取自定義bean并進(jìn)行操作的谭梗。ApplicationReadyEvent
:springboot
加載完成時候執(zhí)行的事件。ApplicationFailedEvent
:spring boot
啟動異常時執(zhí)行事件宛蚓。
從官網(wǎng)文檔中激捏,我們可以知道,由于一些事件實在上下文為加載完觸發(fā)的凄吏,所以無法使用注冊bean
的方式來聲明远舅,文檔中可以看出,可以通過SpringApplication.addListeners(…?)
或者SpringApplicationBuilder.listeners(…?)
來添加痕钢,或者添加META-INF/spring.factories
文件z中添加監(jiān)聽類也是可以的图柏,這樣會自動加載。
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());//加入自定義的監(jiān)聽類
app.run(args);
}
}
所以在需要的時候任连,可以通過適當(dāng)?shù)谋O(jiān)聽以上事件蚤吹,來完成一些業(yè)務(wù)操作。
自定義事件發(fā)布和監(jiān)聽
通過以上的介紹课梳,我們來定義一個自定義事件的發(fā)布和監(jiān)聽距辆。
0.加入POM依賴,這里為了演示加入了web
依賴。事件相關(guān)類都在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.編寫監(jiān)聽類
使用@EventListener
方式。
/**
* 監(jiān)聽配置類
*
* @author oKong
*
*/
@Configuration
@Slf4j
public class EventListenerConfig {
@EventListener
public void handleEvent(Object event) {
//監(jiān)聽所有事件 可以看看 系統(tǒng)各類時間 發(fā)布了哪些事件
//可根據(jù) instanceof 監(jiān)聽想要監(jiān)聽的事件
// if(event instanceof CustomEvent) {
//
// }
log.info("事件:{}", event);
}
@EventListener
public void handleCustomEvent(CustomEvent customEvent) {
//監(jiān)聽 CustomEvent事件
log.info("監(jiān)聽到CustomEvent事件椭懊,消息為:{}, 發(fā)布時間:{}", customEvent.getMessageEntity(), customEvent.getTimestamp());
}
/**
* 監(jiān)聽 code為oKong的事件
*/
@EventListener(condition="#customEvent.messageEntity.code == 'oKong'")
public void handleCustomEventByCondition(CustomEvent customEvent) {
//監(jiān)聽 CustomEvent事件
log.info("監(jiān)聽到code為'oKong'的CustomEvent事件诸蚕,消息為:{}, 發(fā)布時間:{}", customEvent.getMessageEntity(), customEvent.getTimestamp());
}
@EventListener
public void handleObjectEvent(MessageEntity messageEntity) {
//這個和eventbus post方法一樣了
log.info("監(jiān)聽到對象事件,消息為:{}", messageEntity);
}
}
注意:Spring
中氧猬,事件源不強(qiáng)迫繼承ApplicationEvent
接口的背犯,也就是可以直接發(fā)布任意一個對象類。但內(nèi)部其實是使用PayloadApplicationEvent
類進(jìn)行包裝了一層盅抚。這點和guava
的eventBus
類似漠魏。
而且,使用@EventListener
的condition
可以實現(xiàn)更加精細(xì)的事件監(jiān)聽妄均,condition
支持SpEL
表達(dá)式柱锹,可根據(jù)事件源的參數(shù)來判斷是否監(jiān)聽哪自。
使用ApplicationListener
方式。
@Component
@Slf4j
public class EventListener implements ApplicationListener<CustomEvent>{
@Override
public void onApplicationEvent(CustomEvent event) {
//這里也可以監(jiān)聽所有事件 使用 ApplicationEvent 類即可
//這里僅僅監(jiān)聽自定義事件 CustomEvent
log.info("ApplicationListener方式監(jiān)聽事件:{}", event);
}
}
3.編寫控制類禁熏,示例發(fā)布事件壤巷。
/**
* 模擬觸發(fā)事件
* @author oKong
*
*/
@RestController
@RequestMapping("/push")
@Slf4j
public class DemoController {
/**
* 注入 事件發(fā)布類
*/
@Autowired
ApplicationEventPublisher eventPublisher;
@GetMapping
public String push(String code,String message) {
log.info("發(fā)布applicationEvent事件:{},{}", code, message);
eventPublisher.publishEvent(new CustomEvent(this, MessageEntity.builder().code(code).message(message).build()));
return "事件發(fā)布成功!";
}
@GetMapping("/obj")
public String pushObject(String code,String message) {
log.info("發(fā)布對象事件:{},{}", code, message);
eventPublisher.publishEvent(MessageEntity.builder().code(code).message(message).build());
return "對象事件發(fā)布成功!";
}
}
4.編寫啟動類。
/**
* 事件監(jiān)聽
*
* @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());//加入自定義的監(jiān)聽類
app.run(args);
log.info("spring-boot-event-listener-chapter32啟動!");
}
}
這里瞧毙,創(chuàng)建了個ApplicationStartingEvent
事件監(jiān)聽類胧华。
/**
* 示例-啟動事件
* @author oKong
*
*/
public class MyApplicationStartingEventListener implements ApplicationListener<ApplicationStartingEvent>{
@Override
public void onApplicationEvent(ApplicationStartingEvent event) {
// TODO Auto-generated method stub
//由于 log相關(guān)還未加載 使用了也輸出不了的
// log.info("ApplicationStartingEvent事件發(fā)布:{}", event);
System.out.println("ApplicationStartingEvent事件發(fā)布:" + event.getTimestamp());
}
}
5.啟動應(yīng)用,控制臺可以看出宙彪,在啟動時矩动,我們監(jiān)聽到了ApplicationStartingEvent
事件
首先訪問下:http://127.0.0.1:8080/push?code=lqdev&message=趔趄的猿
,可以看見事件已經(jīng)被監(jiān)聽到了您访,而監(jiān)聽了code
為oKong
的監(jiān)聽未觸發(fā)铅忿。
然后訪問下:http://127.0.0.1:8080/push?code=oKong&message=趔趄的猿
,可以看見此時三個監(jiān)聽事件都接收到了事件了灵汪。
此時檀训,由于寫了一個監(jiān)聽所有事件的方法,可以看見請求結(jié)束后享言,會發(fā)布一個事件ServletRequestHandledEvent
峻凫,里面記錄了請求的時間、請求url览露、請求方式等等信息荧琼。
事件:ServletRequestHandledEvent: url=[/push]; client=[127.0.0.1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[2ms]; status=[OK]
異步監(jiān)聽處理
默認(rèn)情況下,監(jiān)聽事件都是同步執(zhí)行的差牛。在需要異步處理時命锄,可以在方法上加上@Async
進(jìn)行異步化操作。此時偏化,可以定義一個線程池脐恩,同時開啟異步功能,加入@EnableAsync
侦讨。
對于異步處理驶冒,可以查看之前發(fā)布的文章:《第二十一章:異步開發(fā)之異步調(diào)用》。里面有詳細(xì)的介紹異步調(diào)用韵卤,這里就不闡述了骗污。
異步簡單示例:
/**
* 監(jiān)聽 code為oKong的事件
*/
@Async
@EventListener(condition="#customEvent.messageEntity.code == 'oKong'")
public void handleCustomEventByCondition(CustomEvent customEvent) {
//監(jiān)聽 CustomEvent事件
log.info("監(jiān)聽到code為'oKong'的CustomEvent事件,消息為:{}, 發(fā)布時間:{}", customEvent.getMessageEntity(), customEvent.getTimestamp());
}
關(guān)于事務(wù)綁定事件
當(dāng)一些場景下沈条,比如在用戶注冊成功后需忿,即數(shù)據(jù)庫事務(wù)提交了,之后再異步發(fā)送郵件等,不然會發(fā)生數(shù)據(jù)庫插入失敗贴谎,但事件卻發(fā)布了汞扎,也就是郵件發(fā)送成功了的情況。此時擅这,我們可以使用@TransactionalEventListener
注解或者TransactionSynchronizationManager
類來解決此類問題,也就是:事務(wù)成功提交后景鼠,再發(fā)布事件仲翎。當(dāng)然也可以利用返回上層(事務(wù)提交后)再發(fā)布事件的方式了,只是不夠優(yōu)雅而已罷了铛漓,其實能起作用就好了溯香,是吧~
本例中未使用到數(shù)據(jù)庫,就不示例了浓恶,都在Spring-tx
包下玫坛。
具體可查看文章:Spring Event 事件中的事務(wù)控制
spring4.2之前 spring4.2之后參考資料
總結(jié)
本章節(jié)主要簡單介紹了
spring
的事件機(jī)制。感興趣的同學(xué)包晰,可以編寫一個監(jiān)聽所有事件的方法湿镀,然后看看系統(tǒng)運行各類請求或者相關(guān)操作時,系統(tǒng)會發(fā)布哪些事件伐憾,了解后可以在之后碰見一些特殊業(yè)務(wù)需求時勉痴,可以適當(dāng)?shù)谋O(jiān)聽相關(guān)的事件來完成特定的業(yè)務(wù)公共。同時對這種觀察者模式树肃,大家還可以看看eventbus
和reactor
了蒸矛。后者沒用過,有時間倒是可以看看胸嘴。最近買了本RxJava2
書籍雏掠,確實要好好補(bǔ)課下了。
最后
目前互聯(lián)網(wǎng)上很多大佬都有
SpringBoot
系列教程劣像,如有雷同乡话,請多多包涵了。原創(chuàng)不易驾讲,碼字不易蚊伞,還希望大家多多支持。若文中有所錯誤之處吮铭,還望提出时迫,謝謝。