您好,歡迎來電子發(fā)燒友網(wǎng)! ,新用戶?[免費(fèi)注冊(cè)]

您的位置:電子發(fā)燒友網(wǎng)>源碼下載>數(shù)值算法/人工智能>

淺談spring事件業(yè)務(wù)解耦與異步調(diào)用

大?。?/span>0.5 MB 人氣: 2017-10-11 需要積分:1

  使用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ù)耦合在了一起。耦合造成的常見缺點(diǎn)是,我(甚至假設(shè)很頻繁的)修改了郵件、短信發(fā)送的API,我就可能需要在UserService層修改相應(yīng)的調(diào)用方法,但這樣做人家UserService就會(huì)很無辜并吐槽: 你改郵件、短信發(fā)送的業(yè)務(wù),又不關(guān)我的事,干嘛老改到我身上來了?這就是你的不對(duì)了。

  對(duì)呀!根據(jù)職責(zé)分明的設(shè)計(jì)原則,人家UserService就只該管用戶管理部分的業(yè)務(wù)邏輯,你老讓它干別人干的事,它當(dāng)然不高興了!

  那該怎么拌?涼拌?不不不。。。我們可以通過spring的事件機(jī)制來實(shí)現(xiàn)解耦呀。利用觀察者設(shè)計(jì)模式,設(shè)置監(jiān)聽器來監(jiān)聽userService的注冊(cè)事件(同時(shí),我們可以很自然地將userService理解成了 事件發(fā)布者),一旦userService注冊(cè)了,監(jiān)聽器就完成相應(yīng)的郵箱、短信發(fā)送工作(同時(shí),我們也可以很自然地將 發(fā)送郵件、 發(fā)送短信理解成我們的 事件源)。這樣userService就不用管別人的事了,只需要在完成注冊(cè)功能時(shí)候,當(dāng)下老大,號(hào)令手下(監(jiān)聽器),讓它完成短信、郵箱的發(fā)送工作。

  spring的事件通信常按下列流程進(jìn)行

  Created with Rapha?l 2.1.0事件發(fā)布者廣播事件(源)監(jiān)聽器收到廣播,獲取事件源監(jiān)聽器根據(jù)事件源采取相應(yīng)的處理措施

  事件實(shí)例分析

  在這里面,我們涉及到三個(gè)主要對(duì)象:事件發(fā)布者、事件源、事件監(jiān)聽器。根據(jù)這三個(gè)對(duì)象,我們來配置我們的注冊(cè)事件實(shí)例:

  1. 定義事件源

  利用事件通信的第一步往往便是定義我們的事件。在spring中,所有事件都必須擴(kuò)展抽象類ApplicationEvent,同時(shí)將事件源作為構(gòu)造函數(shù)參數(shù),在這里,我們定義了發(fā)郵件、發(fā)短信兩個(gè)事件如下所示

  /*****************郵件發(fā)送事件源*************/publicclassSendEmailEventextendsApplicationEvent{//定義事件的核心成員:發(fā)送目的地,共監(jiān)聽器調(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)聽器

  事件監(jiān)聽類需要實(shí)現(xiàn)我們的ApplicationListener接口,除了可以實(shí)現(xiàn)ApplicationListener定義事件監(jiān)聽器外,我們還可以讓事件監(jiān)聽類實(shí)現(xiàn)SmartApplicationListener(智能監(jiān)聽器)接口,。關(guān)于它的具體用法和實(shí)現(xiàn)可參考我的下一篇文章《spring學(xué)習(xí)筆記(14)趣談spring 事件機(jī)制[2]:多監(jiān)聽器流水線式順序處理 》。而此外,如果我們事件監(jiān)聽器監(jiān)聽的事件類型唯一的話,我們可以通過泛型來簡化配置。

  現(xiàn)在我們先來看看本例定義:

  publicclassRegisterListenerimplementsApplicationListener{/* *當(dāng)我們的發(fā)布者發(fā)布時(shí)間時(shí),我們的監(jiān)聽器收到信號(hào),就會(huì)調(diào)用這個(gè)方法 *我們對(duì)其進(jìn)行重寫來適應(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ā)送短信過程} catch(InterruptedException e) { e.printStackTrace(); } System.out.println(“短信發(fā)送成功!”); } } } /******************通過泛型配置實(shí)例如下******************/publicclassRegisterListenerimplementsApplicationListener《SendEmailEvent》 {//這里使用泛型@Override//因?yàn)槭褂昧朔盒?,我們的重寫方法入?yún)⑹录臀ㄒ涣恕ublicvoidonApplicationEvent(SendEmailEvent event) { 。。.。。 } 。。.。 }

  3. 定義事件發(fā)布者

  事件發(fā)送的代表類是ApplicationEventPublisher我們的事件發(fā)布類常實(shí)現(xiàn)ApplicationEventPublisherAware接口,同時(shí)需要定義成員屬性ApplicationEventPublisher來發(fā)布我們的事件。

  除了通過實(shí)現(xiàn)ApplicationEventPublisherAware外,我們還可以實(shí)現(xiàn)ApplicationContextAware接口來完成定義,ApplicationContext接口繼承了ApplicationEventPublisher。ApplicationContext是我們的事件容器上層,我們發(fā)布事件,也可以通過此容器完成發(fā)布。下面使用兩種方法來定義我們的發(fā)布者

  在本例中,我們的時(shí)間發(fā)布者自然就是我們的吐槽者,userService:

  /**********方法一:實(shí)現(xiàn)除了通過實(shí)現(xiàn)ApplicationEventPublisherAware接口************/publicclassUserServiceimplementsApplicationEventPublisherAware{privateApplicationEventPublisher applicationEventPublisher;//底層事件發(fā)布者@OverridepublicvoidsetApplicationEventPublisher(//通過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)除了通過實(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)聽器

  《!-- 在spring容器中注冊(cè)事件監(jiān)聽器, 應(yīng)用上下文將會(huì)識(shí)別實(shí)現(xiàn)了ApplicationListener接口的Bean, 并在特定時(shí)刻將所有的事件通知它們 --》《beanid=“RegisterListener”class=“test.event.RegisterListener”/》《!-- 注冊(cè)我們的發(fā)布者,后面測試用到 --》《beanid=“userService”class=“test.event.UserService”/》

  5. 測試方法

  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. 測試結(jié)果及分析

  調(diào)用上面測試方法,控制臺(tái)打印信息

  注冊(cè)成功!

  正在向zenghao@google.com發(fā)送郵件……

  郵件發(fā)送成功!

  正在向12345678911發(fā)送短信……

  發(fā)送成功!

  處理注冊(cè)相關(guān)業(yè)務(wù)耗時(shí)2201ms

  處理其他業(yè)務(wù)邏輯開始。。

  處理其他業(yè)務(wù)邏輯結(jié)束。。

  處理所有業(yè)務(wù)耗時(shí)2701ms

  向客戶端發(fā)送注冊(cè)成功響應(yīng)

  在本例中,我們通過事件機(jī)制完成了userService和郵件、短信發(fā)送業(yè)務(wù)的解耦。但觀察我們的測試結(jié)果,我們會(huì)發(fā)現(xiàn),這樣的用戶體驗(yàn)真是糟糕透了:天吶,我去你那注冊(cè)個(gè)用戶,要我等近3秒鐘!這太久了!

  為什么會(huì)這么久?我們根據(jù)方法分析:

  1. 注冊(cè)查詢數(shù)據(jù)庫用了200ms(查詢用戶名、郵箱、手機(jī)號(hào)有沒被使用,插入用戶信息到數(shù)據(jù)庫等操作)

  2. 發(fā)送郵件用了1000ms

  3. 發(fā)送短信用了1000ms

  4. 處理其他業(yè)務(wù)邏輯(保存用戶信息到session,其他信息數(shù)據(jù)處理等)

  第1,4步的時(shí)間耗損我們很難優(yōu)化,但2,3步是主要耗時(shí)的地方,我們能不能想辦法把它縮減掉了,它把我們的正常的業(yè)務(wù)處理堵塞了。什么?堵塞,想到堵塞,我們會(huì)很自然地想到非堵塞,那就通過異步來完成2,3唄!

  7. 異步拓展。

  在spring3以上,拓展了自己獨(dú)立的時(shí)間機(jī)制,我們可以使用@Async來完成異步配置。

  首先我們需要在我們的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 --》《!-- 我們的異步事件配置,非常簡單 --》《!--開啟注解調(diào)度支持 @Async @Scheduled--》《task:annotation-driven/》

  然后在我們的事件監(jiān)聽器中添加@Async注解

  /***************我們可以在類名上添加****************/@AsyncpublicclassRegisterListenerimplementsApplicationListener{。。.。。. } /****************也可以在方法體上添加************/@AsyncpublicclassRegisterListenerimplementsApplicationListener{@OverridepublicvoidonApplicationEvent(ApplicationEvent event) { 。。.。。 } }

  然后,再調(diào)用我們的同樣的測試方法,這次我們的結(jié)果變成:

  注冊(cè)成功!

  正在向zenghao@google.com發(fā)送郵件……

  處理注冊(cè)相關(guān)業(yè)務(wù)耗時(shí)201ms ————此時(shí)郵件發(fā)送還沒有結(jié)束,和郵件發(fā)送異步了

  正在向12345678911發(fā)送短信…。。 ————–短信發(fā)送和郵件發(fā)送和主業(yè)務(wù)處理程序都異步了!

  處理其他業(yè)務(wù)邏輯開始。。

  處理其他業(yè)務(wù)邏輯結(jié)束。。

  處理所有業(yè)務(wù)耗時(shí)701ms

  向客戶端發(fā)送注冊(cè)成功響應(yīng) ——客戶端耗時(shí)701ms就收到響應(yīng)了。

  郵件發(fā)送成功! —-這個(gè)時(shí)候郵箱才發(fā)完

  短信發(fā)送成功!

  從以上的測試結(jié)果我們,我們的郵箱發(fā)送和短信發(fā)送都 分別單獨(dú)地異步完成了,大大縮短了我們主業(yè)務(wù)處理事件,也提高了用戶體驗(yàn)

  小結(jié)

  從本例可以看出,不同業(yè)務(wù)功能的生硬組合,會(huì)出現(xiàn)邏輯處理混亂的嚴(yán)重耦合現(xiàn)象,比如userService類既處理自己的用戶邏輯,還要處理郵箱等發(fā)送的邏輯,這是不是也意味著,如果以后我們拓展更多的功能,我們的userService類還要出現(xiàn)更多的邏輯處理,來個(gè)大雜燴?,這同時(shí)還可能會(huì)為我們主要業(yè)務(wù)處理帶來不必要的阻塞。當(dāng)然,為了防止阻塞,我們還可以創(chuàng)建新的線程來異步,但這樣原來的類就顯得更加雜亂臃腫了。使用spring事件機(jī)制能很好地幫助我們消除不同業(yè)務(wù)間的深耦合關(guān)系。它強(qiáng)大的任務(wù)調(diào)度還能幫助我們簡潔地實(shí)現(xiàn)事件異步。關(guān)于事件的一些其他用法可參考我的下一篇博文《趣談spring 事件機(jī)制[2]:多監(jiān)聽器流水線式順序處理》 關(guān)于任務(wù)調(diào)度的相關(guān)框架和使用可參考我的專欄《深入淺出Quartz任務(wù)調(diào)度》。

非常好我支持^.^

(0) 0%

不好我反對(duì)

(0) 0%

      發(fā)表評(píng)論

      用戶評(píng)論
      評(píng)價(jià):好評(píng)中評(píng)差評(píng)

      發(fā)表評(píng)論,獲取積分! 請(qǐng)遵守相關(guān)規(guī)定!

      ?