淺談spring事件業(yè)務(wù)解耦與異步調(diào)用
推薦 + 挑錯(cuò) + 收藏(0) + 用戶評(píng)論(0)
使用spring的事件機(jī)制有助于對(duì)我們的項(xiàng)目進(jìn)一步的解耦。假如現(xiàn)在我們面臨一個(gè)需求:
我需要在用戶注冊(cè)成功的時(shí)候,根據(jù)用戶提交的郵箱、手機(jī)號(hào)信息,向用戶發(fā)送郵箱認(rèn)證和手機(jī)號(hào)短信通知。傳統(tǒng)的做法之一是在我們的UserService層注入郵件發(fā)送和短信發(fā)送的相關(guān)類,然后在完成用戶注冊(cè)同時(shí),調(diào)用對(duì)應(yīng)類方法完成郵件發(fā)送和短信發(fā)送
但這樣做的話,會(huì)把我們郵件、短信發(fā)送的業(yè)務(wù)與我們的UserService的邏輯業(yè)務(wù)耦合在了一起。耦合造成的常見(jiàn)缺點(diǎn)是,我(甚至假設(shè)很頻繁的)修改了郵件、短信發(fā)送的API,我就可能需要在UserService層修改相應(yīng)的調(diào)用方法,但這樣做人家UserService就會(huì)很無(wú)辜并吐槽: 你改郵件、短信發(fā)送的業(yè)務(wù),又不關(guān)我的事,干嘛老改到我身上來(lái)了?這就是你的不對(duì)了。
對(duì)呀!根據(jù)職責(zé)分明的設(shè)計(jì)原則,人家UserService就只該管用戶管理部分的業(yè)務(wù)邏輯,你老讓它干別人干的事,它當(dāng)然不高興了!
那該怎么拌?涼拌?不不不。。。我們可以通過(guò)spring的事件機(jī)制來(lái)實(shí)現(xiàn)解耦呀。利用觀察者設(shè)計(jì)模式,設(shè)置監(jiān)聽(tīng)器來(lái)監(jiān)聽(tīng)userService的注冊(cè)事件(同時(shí),我們可以很自然地將userService理解成了 事件發(fā)布者),一旦userService注冊(cè)了,監(jiān)聽(tīng)器就完成相應(yīng)的郵箱、短信發(fā)送工作(同時(shí),我們也可以很自然地將 發(fā)送郵件、 發(fā)送短信理解成我們的 事件源)。這樣userService就不用管別人的事了,只需要在完成注冊(cè)功能時(shí)候,當(dāng)下老大,號(hào)令手下(監(jiān)聽(tīng)器),讓它完成短信、郵箱的發(fā)送工作。
spring的事件通信常按下列流程進(jìn)行
Created with Rapha?l 2.1.0事件發(fā)布者廣播事件(源)監(jiān)聽(tīng)器收到廣播,獲取事件源監(jiān)聽(tīng)器根據(jù)事件源采取相應(yīng)的處理措施
事件實(shí)例分析
在這里面,我們涉及到三個(gè)主要對(duì)象:事件發(fā)布者、事件源、事件監(jiān)聽(tīng)器。根據(jù)這三個(gè)對(duì)象,我們來(lái)配置我們的注冊(cè)事件實(shí)例:
1. 定義事件源
利用事件通信的第一步往往便是定義我們的事件。在spring中,所有事件都必須擴(kuò)展抽象類ApplicationEvent,同時(shí)將事件源作為構(gòu)造函數(shù)參數(shù),在這里,我們定義了發(fā)郵件、發(fā)短信兩個(gè)事件如下所示
/*****************郵件發(fā)送事件源*************/publicclassSendEmailEventextendsApplicationEvent{//定義事件的核心成員:發(fā)送目的地,共監(jiān)聽(tīng)器調(diào)用完成郵箱發(fā)送功能privateString emailAddress; publicSendEmailEvent(Object source,String emailAddress ) { //source字面意思是根源,意指發(fā)送事件的根源,即我們的事件發(fā)布者super(source); this.emailAddress = emailAddress; } publicString getEmailAddress() { returnemailAddress; } } /*****************短信發(fā)送事件源*************/publicclasssendMessageEventextendsApplicationEvent{privateString phoneNum; publicsendMessageEvent(Object source,String phoneNum ) { super(source); this.phoneNum = phoneNum; } publicString getPhoneNum() { returnphoneNum; } }
2. 定義事件監(jiān)聽(tīng)器
事件監(jiān)聽(tīng)類需要實(shí)現(xiàn)我們的ApplicationListener接口,除了可以實(shí)現(xiàn)ApplicationListener定義事件監(jiān)聽(tīng)器外,我們還可以讓事件監(jiān)聽(tīng)類實(shí)現(xiàn)SmartApplicationListener(智能監(jiān)聽(tīng)器)接口,。關(guān)于它的具體用法和實(shí)現(xiàn)可參考我的下一篇文章《spring學(xué)習(xí)筆記(14)趣談spring 事件機(jī)制[2]:多監(jiān)聽(tīng)器流水線式順序處理 》。而此外,如果我們事件監(jiān)聽(tīng)器監(jiān)聽(tīng)的事件類型唯一的話,我們可以通過(guò)泛型來(lái)簡(jiǎn)化配置。
現(xiàn)在我們先來(lái)看看本例定義:
publicclassRegisterListenerimplementsApplicationListener{/* *當(dāng)我們的發(fā)布者發(fā)布時(shí)間時(shí),我們的監(jiān)聽(tīng)器收到信號(hào),就會(huì)調(diào)用這個(gè)方法 *我們對(duì)其進(jìn)行重寫(xiě)來(lái)適應(yīng)我們的需求 *@Param event:我們的事件源 */@OverridepublicvoidonApplicationEvent(ApplicationEvent event) { //我們定義了兩個(gè)事件:發(fā)短信,發(fā)郵箱,他們一旦被發(fā)布都會(huì)被此方法調(diào)用//于是我們需要判斷當(dāng)前event的具體類型if(event instanceofSendEmailEvent){//如果是發(fā)郵箱事件System.out.println(“正在向”+ ((SendEmailEvent) event).getEmailAddress()+ “發(fā)送郵件。。.。。.”);//模擬發(fā)送郵件事件try{ Thread.sleep(1* 1000);//模擬請(qǐng)求郵箱服務(wù)器、驗(yàn)證賬號(hào)密碼,發(fā)送郵件耗時(shí)。} catch(InterruptedException e) { e.printStackTrace(); } System.out.println(“郵件發(fā)送成功!”); }elseif(event instanceofsendMessageEvent){//是發(fā)短信事件event = (sendMessageEvent) event; System.out.println(“正在向”+ ((sendMessageEvent) event).getPhoneNum()+ “發(fā)送短信。。.。。.”);//模擬發(fā)送郵短信事件try{ Thread.sleep(1* 1000);//模擬發(fā)送短信過(guò)程} catch(InterruptedException e) { e.printStackTrace(); } System.out.println(“短信發(fā)送成功!”); } } } /******************通過(guò)泛型配置實(shí)例如下******************/publicclassRegisterListenerimplementsApplicationListener《SendEmailEvent》 {//這里使用泛型@Override//因?yàn)槭褂昧朔盒?,我們的重?xiě)方法入?yún)⑹录臀ㄒ涣?。publicvoidonApplicationEvent(SendEmailEvent event) { 。。.。。 } 。。.。 }
3. 定義事件發(fā)布者
事件發(fā)送的代表類是ApplicationEventPublisher我們的事件發(fā)布類常實(shí)現(xiàn)ApplicationEventPublisherAware接口,同時(shí)需要定義成員屬性ApplicationEventPublisher來(lái)發(fā)布我們的事件。
除了通過(guò)實(shí)現(xiàn)ApplicationEventPublisherAware外,我們還可以實(shí)現(xiàn)ApplicationContextAware接口來(lái)完成定義,ApplicationContext接口繼承了ApplicationEventPublisher。ApplicationContext是我們的事件容器上層,我們發(fā)布事件,也可以通過(guò)此容器完成發(fā)布。下面使用兩種方法來(lái)定義我們的發(fā)布者
在本例中,我們的時(shí)間發(fā)布者自然就是我們的吐槽者,userService:
/**********方法一:實(shí)現(xiàn)除了通過(guò)實(shí)現(xiàn)ApplicationEventPublisherAware接口************/publicclassUserServiceimplementsApplicationEventPublisherAware{privateApplicationEventPublisher applicationEventPublisher;//底層事件發(fā)布者@OverridepublicvoidsetApplicationEventPublisher(//通過(guò)Set方法完成我們的實(shí)際發(fā)布者注入 ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } publicvoiddoLogin(String emailAddress,String phoneNum) throwsInterruptedException{ Thread.sleep(200);//模擬用戶注冊(cè)的相關(guān)業(yè)務(wù)邏輯處理System.out.println(“注冊(cè)成功!”); //下列向用戶發(fā)送郵件SendEmailEvent sendEmailEvent = newSendEmailEvent(this,emailAddress);//定義事件sendMessageEvent sendMessageEvent = newsendMessageEvent(this, phoneNum); applicationEventPublisher.publishEvent(sendEmailEvent);//發(fā)布事件applicationEventPublisher.publishEvent(sendMessageEvent); } //。。.忽略其他用戶管理業(yè)務(wù)方法} /**********方法二:實(shí)現(xiàn)除了通過(guò)實(shí)現(xiàn)ApplicationContext接口************/publicclassUserService2implementsApplicationContextAware{privateApplicationContext applicationContext; @OverridepublicvoidsetApplicationContext(ApplicationContext applicationContext) throwsBeansException { this.applicationContext = applicationContext; } publicvoiddoLogin(String emailAddress,String phoneNum) throwsInterruptedException{ Thread.sleep(200);//模擬用戶注冊(cè)的相關(guān)業(yè)務(wù)邏輯處理System.out.println(“注冊(cè)成功!”); //下列向用戶發(fā)送郵件SendEmailEvent sendEmailEvent = newSendEmailEvent(this,emailAddress);//定義事件sendMessageEvent sendMessageEvent = newsendMessageEvent(this, phoneNum); applicationContext.publishEvent(sendEmailEvent);//發(fā)布事件applicationContext.publishEvent(sendMessageEvent); } //。。.忽略其他用戶管理業(yè)務(wù)方法}
4. 在IOC容器注冊(cè)監(jiān)聽(tīng)器
《!-- 在spring容器中注冊(cè)事件監(jiān)聽(tīng)器, 應(yīng)用上下文將會(huì)識(shí)別實(shí)現(xiàn)了ApplicationListener接口的Bean, 并在特定時(shí)刻將所有的事件通知它們 --》《beanid=“RegisterListener”class=“test.event.RegisterListener”/》《!-- 注冊(cè)我們的發(fā)布者,后面測(cè)試用到 --》《beanid=“userService”class=“test.event.UserService”/》
5. 測(cè)試方法
publicstaticvoidmain(String args[]) throwsInterruptedException{ ApplicationContext ac = newClassPathXmlApplicationContext(“classpath:test/event/event.xml”); UserService userService = (UserService) ac.getBean(“userService”); Long beginTime = System.currentTimeMillis(); userService.doLogin(“zenghao@google.com”,“12345678911”);//完成注冊(cè)請(qǐng)求System.out.println(“處理注冊(cè)相關(guān)業(yè)務(wù)耗時(shí)”+ (System.currentTimeMillis() - beginTime )+ “ms”); System.out.println(“處理其他業(yè)務(wù)邏輯”); Thread.sleep(500);//模擬處理其他業(yè)務(wù)請(qǐng)求耗時(shí)System.out.println(“處理所有業(yè)務(wù)耗時(shí)”+ (System.currentTimeMillis() - beginTime )+ “ms”); System.out.println(“向客戶端發(fā)送注冊(cè)成功響應(yīng)”); }
6. 測(cè)試結(jié)果及分析
調(diào)用上面測(cè)試方法,控制臺(tái)打印信息
注冊(cè)成功!
正在向zenghao@google.com發(fā)送郵件……
郵件發(fā)送成功!
正在向12345678911發(fā)送短信……
發(fā)送成功!
處理注冊(cè)相關(guān)業(yè)務(wù)耗時(shí)2201ms
處理其他業(yè)務(wù)邏輯開(kāi)始。。
處理其他業(yè)務(wù)邏輯結(jié)束。。
處理所有業(yè)務(wù)耗時(shí)2701ms
向客戶端發(fā)送注冊(cè)成功響應(yīng)
在本例中,我們通過(guò)事件機(jī)制完成了userService和郵件、短信發(fā)送業(yè)務(wù)的解耦。但觀察我們的測(cè)試結(jié)果,我們會(huì)發(fā)現(xiàn),這樣的用戶體驗(yàn)真是糟糕透了:天吶,我去你那注冊(cè)個(gè)用戶,要我等近3秒鐘!這太久了!
為什么會(huì)這么久?我們根據(jù)方法分析:
1. 注冊(cè)查詢數(shù)據(jù)庫(kù)用了200ms(查詢用戶名、郵箱、手機(jī)號(hào)有沒(méi)被使用,插入用戶信息到數(shù)據(jù)庫(kù)等操作)
2. 發(fā)送郵件用了1000ms
3. 發(fā)送短信用了1000ms
4. 處理其他業(yè)務(wù)邏輯(保存用戶信息到session,其他信息數(shù)據(jù)處理等)
第1,4步的時(shí)間耗損我們很難優(yōu)化,但2,3步是主要耗時(shí)的地方,我們能不能想辦法把它縮減掉了,它把我們的正常的業(yè)務(wù)處理堵塞了。什么?堵塞,想到堵塞,我們會(huì)很自然地想到非堵塞,那就通過(guò)異步來(lái)完成2,3唄!
7. 異步拓展。
在spring3以上,拓展了自己獨(dú)立的時(shí)間機(jī)制,我們可以使用@Async來(lái)完成異步配置。
首先我們需要在我們的IOC容器增加
《!--先在命名空間中增加我們的task標(biāo)簽,注意它們的添加位置 xmlns 多加下面的內(nèi)容: xmlns:task=“http://www.springframework.org/schema/task” 然后xsi:schemaLocation多加下面的內(nèi)容 http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd --》《!-- 我們的異步事件配置,非常簡(jiǎn)單 --》《!--開(kāi)啟注解調(diào)度支持 @Async @Scheduled--》《task:annotation-driven/》
然后在我們的事件監(jiān)聽(tīng)器中添加@Async注解
/***************我們可以在類名上添加****************/@AsyncpublicclassRegisterListenerimplementsApplicationListener{。。.。。. } /****************也可以在方法體上添加************/@AsyncpublicclassRegisterListenerimplementsApplicationListener{@OverridepublicvoidonApplicationEvent(ApplicationEvent event) { 。。.。。 } }
然后,再調(diào)用我們的同樣的測(cè)試方法,這次我們的結(jié)果變成:
注冊(cè)成功!
正在向zenghao@google.com發(fā)送郵件……
處理注冊(cè)相關(guān)業(yè)務(wù)耗時(shí)201ms ————此時(shí)郵件發(fā)送還沒(méi)有結(jié)束,和郵件發(fā)送異步了
正在向12345678911發(fā)送短信…。。 ————–短信發(fā)送和郵件發(fā)送和主業(yè)務(wù)處理程序都異步了!
處理其他業(yè)務(wù)邏輯開(kāi)始。。
處理其他業(yè)務(wù)邏輯結(jié)束。。
處理所有業(yè)務(wù)耗時(shí)701ms
向客戶端發(fā)送注冊(cè)成功響應(yīng) ——客戶端耗時(shí)701ms就收到響應(yīng)了。
郵件發(fā)送成功! —-這個(gè)時(shí)候郵箱才發(fā)完
短信發(fā)送成功!
從以上的測(cè)試結(jié)果我們,我們的郵箱發(fā)送和短信發(fā)送都 分別單獨(dú)地異步完成了,大大縮短了我們主業(yè)務(wù)處理事件,也提高了用戶體驗(yàn)
小結(jié)
從本例可以看出,不同業(yè)務(wù)功能的生硬組合,會(huì)出現(xiàn)邏輯處理混亂的嚴(yán)重耦合現(xiàn)象,比如userService類既處理自己的用戶邏輯,還要處理郵箱等發(fā)送的邏輯,這是不是也意味著,如果以后我們拓展更多的功能,我們的userService類還要出現(xiàn)更多的邏輯處理,來(lái)個(gè)大雜燴?,這同時(shí)還可能會(huì)為我們主要業(yè)務(wù)處理帶來(lái)不必要的阻塞。當(dāng)然,為了防止阻塞,我們還可以創(chuàng)建新的線程來(lái)異步,但這樣原來(lái)的類就顯得更加雜亂臃腫了。使用spring事件機(jī)制能很好地幫助我們消除不同業(yè)務(wù)間的深耦合關(guān)系。它強(qiáng)大的任務(wù)調(diào)度還能幫助我們簡(jiǎn)潔地實(shí)現(xiàn)事件異步。關(guān)于事件的一些其他用法可參考我的下一篇博文《趣談spring 事件機(jī)制[2]:多監(jiān)聽(tīng)器流水線式順序處理》 關(guān)于任務(wù)調(diào)度的相關(guān)框架和使用可參考我的專欄《深入淺出Quartz任務(wù)調(diào)度》。
非常好我支持^.^
(0) 0%
不好我反對(duì)
(0) 0%
下載地址
淺談spring事件業(yè)務(wù)解耦與異步調(diào)用下載
相關(guān)電子資料下載
- SpringBoot物理線程、虛擬線程、Webflux性能比較 37
- Spring Cloud :打造可擴(kuò)展的微服務(wù)網(wǎng)關(guān) 60
- BeanFactory 和 FactoryBean的區(qū)別 54
- SpringBoot AOP + Redis 延時(shí)雙刪功能實(shí)戰(zhàn) 69
- Spring Boot 的設(shè)計(jì)目標(biāo) 104
- Spring Boot的啟動(dòng)原理 125
- SpringBootApplication是什么 200
- Spring Boot怎么通過(guò)注解來(lái)實(shí)現(xiàn)全局異常處理的 92
- Spring 的線程池應(yīng)用 108
- SpringBoot分布式驗(yàn)證碼登錄方案 145