SpringBoot | 第三十二章:事件的發(fā)布和監(jiān)聽

原文出處: oKong

前言

今天去官網(wǎng)查看spring boot資料時惨险,在特性中看見了系統(tǒng)的事件及監(jiān)聽章節(jié)讹语。想想搞糕,spring的事件應(yīng)該是在3.x版本就發(fā)布的功能了勇吊,并越來越完善,其為beanbean之間的消息通信提供了支持窍仰。比如汉规,我們可以在用戶注冊成功后,發(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)然了未辆,也可以使用像guavaeventbus或者異步框架Reactor來處理此類業(yè)務(wù)需求的窟绷。本文僅僅談?wù)?code>ApplicationEvent以及Listener的使用。

一點知識

示例前咐柜,我們來了解下相關(guān)知識點兼蜈。

Java的事件機(jī)制

java中的事件機(jī)制一般包括3個部分:EventObjectEventListenerSource拙友。

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以及ListenerSpring為我們提供的一個事件監(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)該接口
  • ApplicationEventPublisherSpring的事件發(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)啟動事件

SpringBoot1.5.x中,提供了幾種事件姥卢,供我們在開發(fā)過程中進(jìn)行更加便捷的擴(kuò)展及差異化操作卷要。

  • ApplicationStartingEvent:springboot啟動開始的時候執(zhí)行的事件

  • ApplicationEnvironmentPreparedEventspring boot對應(yīng)Enviroment已經(jīng)準(zhǔn)備完畢渣聚,但此時上下文context還沒有創(chuàng)建。在該監(jiān)聽中獲取到ConfigurableEnvironment后可以對配置信息做操作僧叉,例如:修改默認(rèn)的配置信息奕枝,增加額外的配置信息等等。

  • ApplicationPreparedEventspring boot上下文context創(chuàng)建完成瓶堕,但此時spring中的bean是沒有完全加載完成的隘道。在獲取完上下文后,可以將上下文傳遞出去做一些額外的操作郎笆。值得注意的是:在該監(jiān)聽器中是無法獲取自定義bean并進(jìn)行操作的谭梗。

  • ApplicationReadyEventspringboot加載完成時候執(zhí)行的事件。

  • ApplicationFailedEventspring boot啟動異常時執(zhí)行事件宛蚓。

1.5.15版本

從官網(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)行包裝了一層盅抚。這點和guavaeventBus類似漠魏。

而且,使用@EventListenercondition可以實現(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事件

ApplicationStartingEvent

首先訪問下:http://127.0.0.1:8080/push?code=lqdev&message=趔趄的猿,可以看見事件已經(jīng)被監(jiān)聽到了您访,而監(jiān)聽了codeoKong的監(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之后

參考資料

  1. https://docs.spring.io/spring-boot/docs/1.5.15.RELEASE/reference/htmlsingle/#boot-features-application-events-and-listeners

  2. https://blog.csdn.net/eos2009/article/details/77773551

  3. https://www.cnblogs.com/senlinyang/p/8496099.html

總結(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ù)公共。同時對這種觀察者模式树肃,大家還可以看看eventbusreactor了蒸矛。后者沒用過,有時間倒是可以看看胸嘴。最近買了本RxJava2書籍雏掠,確實要好好補(bǔ)課下了。

最后

目前互聯(lián)網(wǎng)上很多大佬都有SpringBoot系列教程劣像,如有雷同乡话,請多多包涵了。原創(chuàng)不易驾讲,碼字不易蚊伞,還希望大家多多支持。若文中有所錯誤之處吮铭,還望提出时迫,謝謝。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谓晌,一起剝皮案震驚了整個濱河市掠拳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纸肉,老刑警劉巖溺欧,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喊熟,死亡現(xiàn)場離奇詭異,居然都是意外死亡姐刁,警方通過查閱死者的電腦和手機(jī)芥牌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來聂使,“玉大人壁拉,你說我怎么就攤上這事“匕校” “怎么了弃理?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長屎蜓。 經(jīng)常有香客問我痘昌,道長,這世上最難降的妖魔是什么炬转? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任辆苔,我火速辦了婚禮,結(jié)果婚禮上返吻,老公的妹妹穿的比我還像新娘姑子。我一直安慰自己,他們只是感情好测僵,可當(dāng)我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布街佑。 她就那樣靜靜地躺著,像睡著了一般捍靠。 火紅的嫁衣襯著肌膚如雪沐旨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天榨婆,我揣著相機(jī)與錄音磁携,去河邊找鬼。 笑死良风,一個胖子當(dāng)著我的面吹牛谊迄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播烟央,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼统诺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了疑俭?” 一聲冷哼從身側(cè)響起粮呢,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后啄寡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體豪硅,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年挺物,在試婚紗的時候發(fā)現(xiàn)自己被綠了懒浮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡姻乓,死狀恐怖嵌溢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蹋岩,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布学少,位于F島的核電站剪个,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏版确。R本人自食惡果不足惜扣囊,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望绒疗。 院中可真熱鬧侵歇,春花似錦、人聲如沸吓蘑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽磨镶。三九已至溃蔫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間琳猫,已是汗流浹背伟叛。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留脐嫂,地道東北人统刮。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像账千,于是被迫代替她去往敵國和親侥蒙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容