SpringEvent優(yōu)雅解耦時(shí)連續(xù)兩個(gè)bug的解決方案
1,基本原理
日常開(kāi)發(fā)中,我們有時(shí)會(huì)使用SpringEvent對(duì)業(yè)務(wù)解耦,使我們的代碼更加高內(nèi)聚低耦合,不過(guò)如果對(duì)其運(yùn)行原理不清楚,那么在使用的過(guò)程中,一不留神就會(huì)出現(xiàn)一些bug。
今天我們回顧一下SpringEvent使用的基本原理,需要優(yōu)化的點(diǎn),以及非常常見(jiàn)的兩種錯(cuò)誤。
Spring的事件模式其實(shí)很簡(jiǎn)單,我們創(chuàng)建一個(gè)Event事件,當(dāng)Event發(fā)生時(shí),廣播器對(duì)事件進(jìn)行發(fā)布,然后對(duì)應(yīng)的Listener進(jìn)行處理即可。
Spring的事件一共有三個(gè)組件:
1,Event:用于定于我們的事件,比如ApplicationEvent或者通過(guò)繼承ApplicationEvent定義我們自己的事件。
2,廣播器Multicaster:當(dāng)事件發(fā)生時(shí),將事件廣播出去。
3,監(jiān)聽(tīng)器Listener:監(jiān)聽(tīng)和處理廣播器廣播的事件。
2,基本用法
第一步,首先定義一個(gè)Event事件,
@Getter @Setter public class MessageEvent extends ApplicationEvent { private String content; public MessageEvent(String content) { super(new Object()); this.content = content; } }
第二步,定義一個(gè)Listener對(duì)事件進(jìn)行監(jiān)聽(tīng),
@Component public class MessageListener { @EventListener public void listen(MessageEvent messageEvent) { System.out.println("收到消息:" + messageEvent.getContent()); } }
最后在我們的業(yè)務(wù)邏輯需要的地方,就可以發(fā)布事件了。
@RestController @RequestMapping(value = "/demo") public class DemoController { @Resource private ApplicationContext applicationContext; @PostMapping(value = "/send") public ResponseEntity sendMessage() { //..... //處理一些業(yè)務(wù)邏輯之后,發(fā)送通知消息 MessageEvent messageEvent = new MessageEvent("發(fā)布一條測(cè)試消息"); this.applicationContext.publishEvent(messageEvent); return ResponseEntity.ok().build(); } }
3,需要注意的點(diǎn)
一,對(duì)于同一個(gè)Event,我們可以定義多個(gè)Listener,多個(gè)Listener之間可以通過(guò)@Order來(lái)指定順序,order的Value值越小,執(zhí)行的優(yōu)先級(jí)就越高。
二,我們可以使用@EventListener輕松標(biāo)記一個(gè)方法作為監(jiān)聽(tīng)器,但是默認(rèn)情況下,它是同步執(zhí)行的,所以如果發(fā)布事件的方法處于事務(wù)中,那么事務(wù)會(huì)在監(jiān)聽(tīng)器方法執(zhí)行完畢之后才提交。
有些情況下,我們希望事件發(fā)布之后就由監(jiān)聽(tīng)器去處理,而不要影響原有的事務(wù),也就是說(shuō)希望事務(wù)及時(shí)提交。
這時(shí)我們可以使用@TransactionalEventListener來(lái)定義一個(gè)監(jiān)聽(tīng)器。
@Component public class MessageListener { //上層事務(wù)執(zhí)行完畢之后再執(zhí)行 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) public void listen(MessageEvent messageEvent) { System.out.println(Thread.currentThread().getName()); System.out.println("收到消息:" + messageEvent.getContent()); } }
三,默認(rèn)情況下,@EventListener定義的方法是同步執(zhí)行的,如果我們想通過(guò)異步的方式執(zhí)行一個(gè)監(jiān)聽(tīng)器的方法,可以在方法上加上@Async注解(記得在啟動(dòng)類上加上@EnableAsync開(kāi)啟異步執(zhí)行配置)。
需要注意的是,使用@Async時(shí),必須為其配置線程池,否則用的還是默認(rèn)的線程。
如@Async(value = "taskExecutor"),此時(shí)Listener就會(huì)被分配到taskExecutor的線程池中執(zhí)行。
使用@Async異步執(zhí)行的同時(shí),還會(huì)帶來(lái)另外兩個(gè)問(wèn)題,需要大家注意:
1,如果Listener執(zhí)行過(guò)程中拋出了異常,由于是異步執(zhí)行,異常并不會(huì)被事件發(fā)布方捕獲。
2,異步執(zhí)行時(shí),方法的返回值不能用來(lái)發(fā)布后續(xù)事件,如果需要處理結(jié)果去發(fā)布另一個(gè)事件,需要我們手動(dòng)去發(fā)布。
4,常見(jiàn)錯(cuò)誤一:錯(cuò)誤的監(jiān)聽(tīng)一個(gè)并不會(huì)拋出的事件
有時(shí)我們希望監(jiān)聽(tīng)Spring的啟動(dòng)事件,做一些初始化操作。于是有的同學(xué)可能定義了這樣一個(gè)Listener:
@Component public class MessageListener { @EventListener public void listen2(ContextStartedEvent event) { System.out.println("Spring啟動(dòng)了," + event.toString()); } }
不過(guò),雖然名字看起來(lái)似乎是一個(gè)上下文啟動(dòng)時(shí)的事件,但是Spring啟動(dòng)時(shí)并不會(huì)發(fā)布這個(gè)事件,我們啟動(dòng)項(xiàng)目看下控制臺(tái)是否會(huì)打印日志:
可以看到,Spring項(xiàng)目啟動(dòng)后,并沒(méi)有打印任何日志。
其實(shí)Spring項(xiàng)目啟動(dòng)后發(fā)布的真正Event是ContextRefreshedEvent,我們修改下代碼再看一下結(jié)果:
這時(shí),控制臺(tái)打印出了我們想要的日志。
在創(chuàng)建監(jiān)聽(tīng)事件時(shí),一定要確保監(jiān)聽(tīng)的Event是正確的,否則就會(huì)監(jiān)聽(tīng)不到對(duì)應(yīng)的事件。
5,常見(jiàn)錯(cuò)誤二:由于異常導(dǎo)致的事件傳播丟失
即使我們保證事件會(huì)被監(jiān)聽(tīng)器真正的捕獲,但是某些情況下,事件會(huì)因?yàn)楫惓?dǎo)致傳播丟失。
如上圖所示,我們定義了兩個(gè)Listener,原本期望按照order定義的順序,將消息傳播依次傳播。然而因?yàn)橐恍┰?,Listener1中的方法拋出了異常,導(dǎo)致Listener2無(wú)法接收到消息了。
這是因?yàn)椋?strong>默認(rèn)情況下處理器的執(zhí)行是順序執(zhí)行的,在執(zhí)行過(guò)程中,如果一個(gè)監(jiān)聽(tīng)器執(zhí)行拋出了異常,則后續(xù)監(jiān)聽(tīng)器就得不到被執(zhí)行的機(jī)會(huì)了。
我們可以通過(guò)SimpleApplicationEventMulticaster中的multicastEvent方法看一下事件是如何傳播的,
通過(guò)上圖可以看出,如果在沒(méi)有定義線程池的情況下,在invokeListener方法中會(huì)調(diào)用doInvokeListener方法去執(zhí)行真正的邏輯,在doInvokeListener方法中,如果拋出了異常,會(huì)導(dǎo)致后的Listener失效。
針對(duì)異常這種情況又該如何處理我們的代碼呢?
有三種方法:
1,使用try catch捕獲異常,只要Listener方法不拋出異常,自然每個(gè)Listener都可以收到廣播的消息。
2,使用@Async異步執(zhí)行,通過(guò)上面的源碼可以看到,如果定義的線程池,那么每一個(gè)Listener都會(huì)在一個(gè)線程中執(zhí)行,每個(gè)線程之后是相互獨(dú)立的,自然不會(huì)影響別人。
3,通過(guò)ErrorHandler處理掉異常,保證后面的Listener不受影響。
總結(jié)
本文主要講解了SpringEvent基本的使用方法,和平常開(kāi)發(fā)中可能會(huì)遇到的一些問(wèn)題??偟膩?lái)說(shuō),Spring為了讓大家用的更輕松,考慮了各種可能發(fā)生的情況,但是如果大家不了解背后的實(shí)現(xiàn)原理,就可能發(fā)生一些本不該出現(xiàn)的bug。
以上就是SpringEvent優(yōu)雅解耦時(shí)連續(xù)兩個(gè)bug的解決方案的詳細(xì)內(nèi)容,更多關(guān)于SpringEvent解耦bug解決的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang分布式應(yīng)用定時(shí)任務(wù)示例詳解
這篇文章主要為大家介紹了Golang分布式應(yīng)用定時(shí)任務(wù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07這些關(guān)于Go中interface{}的注意事項(xiàng)你都了解嗎
這篇文章主要為大家詳細(xì)介紹了學(xué)習(xí)Go語(yǔ)言時(shí)需要了解的interface{}注意事項(xiàng),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-03-03golang開(kāi)發(fā)go包依賴管理godep使用教程
這篇文章主要為大家介紹了golang開(kāi)發(fā)go包依賴管理godep使用教程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-11-11Go語(yǔ)言函數(shù)的延遲調(diào)用(Deferred Code)詳解
本文將介紹Go語(yǔ)言函數(shù)和方法中的延遲調(diào)用,正如名稱一樣,這部分定義不會(huì)立即執(zhí)行,一般會(huì)在函數(shù)返回前再被調(diào)用,我們通過(guò)一些示例來(lái)了解一下延遲調(diào)用的使用場(chǎng)景2022-07-07詳解Golang開(kāi)啟http服務(wù)的三種方式
這篇文章主要介紹了詳解Golang開(kāi)啟http服務(wù)的三種方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06自己動(dòng)手用Golang實(shí)現(xiàn)約瑟夫環(huán)算法的示例
這篇文章主要介紹了自己動(dòng)手用Golang實(shí)現(xiàn)約瑟夫環(huán)算法的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12