事件驅(qū)動模型簡介

事件驅(qū)動模型也就是我們常說的觀察者,或者發(fā)布-訂閱模型;理解它的幾個關(guān)鍵點:

  1. 首先是一種對象間的一對多的關(guān)系;最簡單的如交通信號燈,信號燈是目標(biāo)(一方),行人注視著信號燈(多方);
  2. 當(dāng)目標(biāo)發(fā)送改變(發(fā)布),觀察者(訂閱者)就可以接收到改變;
  3. 觀察者如何處理(如行人如何走,是快走/慢走/不走,目標(biāo)不會管的),目標(biāo)無需干涉;所以就松散耦合了它們之間的關(guān)系。

 

 

接下來先看一個用戶注冊的例子:



用戶注冊成功后,需要做這么多事:

1、加積分

2、發(fā)確認(rèn)郵件

3、如果是游戲帳戶,可能贈送游戲大禮包

4、索引用戶數(shù)據(jù)

…………

問題:

  1. UserService和其他Service耦合嚴(yán)重,增刪功能比較麻煩;
  2. 有些功能可能需要調(diào)用第三方系統(tǒng),如增加積分/索引用戶,速度可能比較慢,此時需要異步支持;這個如果使用Spring,可以輕松解決,后邊再介紹;

 

從如上例子可以看出,應(yīng)該使用一個觀察者來解耦這些Service之間的依賴關(guān)系,如圖:


 

增加了一個Listener來解耦UserService和其他服務(wù),即注冊成功后,只需要通知相關(guān)的監(jiān)聽器,不需要關(guān)系它們?nèi)绾翁幚怼T鰟h功能非常容易。

 

這就是一個典型的事件處理模型/觀察者,解耦目標(biāo)對象和它的依賴對象,目標(biāo)只需要通知它的依賴對象,具體怎么處理,依賴對象自己決定。比如是異步還是同步,延遲還是非延遲等。

 

上邊其實也使用了DIP(依賴倒置原則),依賴于抽象,而不是具體。

 

還是就是使用了IoC思想,即以前主動去創(chuàng)建它依賴的Service,現(xiàn)在只是被動等待別人注冊進(jìn)來。

 

 

其他的例子還有如GUI中的按鈕和動作的關(guān)系,按鈕和動作本身都是一種抽象,每個不同的按鈕的動作可能不一樣;如“文件-->新建”打開新建窗口;點擊“關(guān)閉”按鈕關(guān)閉窗口等等。

 

主要目的是:松散耦合對象間的一對多的依賴關(guān)系,如按鈕和動作的關(guān)系;

 

如何實現(xiàn)呢?面向接口編程(即面向抽象編程),而非面向?qū)崿F(xiàn)。即按鈕和動作可以定義為接口,這樣它倆的依賴是最小的(如在Java中,沒有比接口更抽象的了)。

 

有朋友會問,我剛開始學(xué)的時候也是這樣:抽象類不也行嗎?記住一個原則:接口目的是抽象,抽象類目的是復(fù)用;所以如果接觸過servlet/struts2/spring等框架,大家都應(yīng)該知道:

  • Servlet<-----GenericServlet<-----HttpServlet<------我們自己的
  • Action<------ActionSupport<------我們自己的
  • DaoInterface<------××DaoSupport<-----我們自己的

從上邊大家應(yīng)該能體會出接口、抽象類的主要目的了。現(xiàn)在想想其實很簡單。

 

在Java中接口還一個非常重要的好處:接口是可以多實現(xiàn)的,類/抽象類只能單繼承,所以使用接口可以非常容易擴(kuò)展新功能(還可以實現(xiàn)所謂的mixin),類/抽象類辦不到

 

Java GUI事件驅(qū)動模型/觀察者 

扯遠(yuǎn)了,再來看看Java GUI世界里的事件驅(qū)動模型吧:

 

如果寫過AWT/Swing程序,應(yīng)該知道其所有組件都繼承自java.awt.Component抽象類,其內(nèi)部提供了addXXXListener(XXXListener l) 注冊監(jiān)聽器的方法,即Component與實際動作之間依賴于XXXListener抽象。

 

比如獲取焦點事件,很多組件都可以有這個事件,是我們知道組件獲取到焦點后需要一個處理,雖然每個組件如何處理是特定的(具體的),但我們可以抽象一個FocusListener,讓所有具體實現(xiàn)它然后提供具體動作,這樣組件只需依賴于FocusListener抽象,而不是具體。

 

還有如java.awt.Button,提供了一個addActionListener(ActionListener l),用于注冊點擊后觸發(fā)的ActionListener實現(xiàn)。

 

組件是一個抽象類,其好處主要是復(fù)用,比如復(fù)用這些監(jiān)聽器的觸發(fā)及管理等。

 

JavaBean規(guī)范的事件驅(qū)動模型/觀察者

JavaBean規(guī)范提供了JavaBean的PropertyEditorSupport及PropertyChangeListener支持。

 

PropertyEditorSupport就是目標(biāo),而PropertyChangeListener就是監(jiān)聽器,大家可以google搜索下,具體網(wǎng)上有很多例子。

 

Java提供的事件驅(qū)動模型/觀察者抽象

JDK內(nèi)部直接提供了觀察者模式的抽象:

目標(biāo):java.util.Observable,提供了目標(biāo)需要的關(guān)鍵抽象:addObserver/deleteObserver/notifyObservers()等,具體請參考javadoc。

觀察者:java.util.Observer,提供了觀察者需要的主要抽象:update(Observable o, Object arg),此處還提供了一種推模型(目標(biāo)主動把數(shù)據(jù)通過arg推到觀察者)/拉模型(目標(biāo)需要根據(jù)o自己去拉數(shù)據(jù),arg為null)。

 

因為網(wǎng)上介紹的非常多了,請google搜索了解如何使用這個抽象及推/拉模型的優(yōu)缺點。

 

接下來是我們的重點:spring提供的事件驅(qū)動模型。

 

Spring提供的事件驅(qū)動模型/觀察者抽象

首先看一下Spring提供的事件驅(qū)動模型體系圖: 

事件

具體代表者是:ApplicationEvent:

1、其繼承自JDK的EventObject,JDK要求所有事件將繼承它,并通過source得到事件源,比如我們的AWT事件體系也是繼承自它;

2、系統(tǒng)默認(rèn)提供了如下ApplicationEvent事件實現(xiàn):


只有一個ApplicationContextEvent,表示ApplicationContext容器事件,且其又有如下實現(xiàn):

  • ContextStartedEvent:ApplicationContext啟動后觸發(fā)的事件;(目前版本沒有任何作用)
  • ContextStoppedEvent:ApplicationContext停止后觸發(fā)的事件;(目前版本沒有任何作用)
  • ContextRefreshedEvent:ApplicationContext初始化或刷新完成后觸發(fā)的事件;(容器初始化完成后調(diào)用)
  • ContextClosedEvent:ApplicationContext關(guān)閉后觸發(fā)的事件;(如web容器關(guān)閉時自動會觸發(fā)spring容器的關(guān)閉,如果是普通java應(yīng)用,需要調(diào)用ctx.registerShutdownHook();注冊虛擬機(jī)關(guān)閉時的鉤子才行)

注:org.springframework.context.support.AbstractApplicationContext抽象類實現(xiàn)了LifeCycle的start和stop回調(diào)并發(fā)布ContextStartedEvent和ContextStoppedEvent事件;但是無任何實現(xiàn)調(diào)用它,所以目前無任何作用。

 

目標(biāo)(發(fā)布事件者)

具體代表者是:ApplicationEventPublisher及ApplicationEventMulticaster,系統(tǒng)默認(rèn)提供了如下實現(xiàn):

1、ApplicationContext接口繼承了ApplicationEventPublisher,并在AbstractApplicationContext實現(xiàn)了具體代碼,實際執(zhí)行是委托給ApplicationEventMulticaster(可以認(rèn)為是多播):

Java代碼  收藏代碼
  1. public void publishEvent(ApplicationEvent event) {  
  2.     //省略部分代碼  
  3.     }  
  4.     getApplicationEventMulticaster().multicastEvent(event);  
  5.     if (this.parent != null) {  
  6.         this.parent.publishEvent(event);  
  7.     }  
  8. }  

我們常用的ApplicationContext都繼承自AbstractApplicationContext,如ClassPathXmlApplicationContext、XmlWebApplicationContext等。所以自動擁有這個功能。

 

2、ApplicationContext自動到本地容器里找一個名字為”“的ApplicationEventMulticaster實現(xiàn),如果沒有自己new一個SimpleApplicationEventMulticaster。其中SimpleApplicationEventMulticaster發(fā)布事件的代碼如下:

Java代碼  收藏代碼
  1. public void multicastEvent(final ApplicationEvent event) {  
  2.     for (final ApplicationListener listener : getApplicationListeners(event)) {  
  3.         Executor executor = getTaskExecutor();  
  4.         if (executor != null) {  
  5.             executor.execute(new Runnable() {  
  6.                 public void run() {  
  7.                     listener.onApplicationEvent(event);  
  8.                 }  
  9.             });  
  10.         }  
  11.         else {  
  12.             listener.onApplicationEvent(event);  
  13.         }  
  14.     }  
  15. }  

 大家可以看到如果給它一個executor(java.util.concurrent.Executor),它就可以異步支持發(fā)布事件了。佛則就是通過發(fā)送。

 

所以我們發(fā)送事件只需要通過ApplicationContext.publishEvent即可,沒必要再創(chuàng)建自己的實現(xiàn)了。除非有必要。 

 

監(jiān)聽器

具體代表者是:ApplicationListener

1、其繼承自JDK的EventListener,JDK要求所有監(jiān)聽器將繼承它,比如我們的AWT事件體系也是繼承自它;

2、ApplicationListener接口:

Java代碼  收藏代碼
  1. public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {  
  2.     void onApplicationEvent(E event);  
  3. }  

其只提供了onApplicationEvent方法,我們需要在該方法實現(xiàn)內(nèi)部判斷事件類型來處理,也沒有提供按順序觸發(fā)監(jiān)聽器的語義,所以Spring提供了另一個接口,SmartApplicationListener:

Java代碼  收藏代碼
  1. public interface SmartApplicationListener extends ApplicationListener<ApplicationEvent>, Ordered {  
  2.         //如果實現(xiàn)支持該事件類型 那么返回true  
  3.     boolean supportsEventType(Class<? extends ApplicationEvent> eventType);  
  4.     
  5.         //如果實現(xiàn)支持“目標(biāo)”類型,那么返回true  
  6.     boolean supportsSourceType(Class<?> sourceType);  
  7.          
  8.         //順序,即監(jiān)聽器執(zhí)行的順序,值越小優(yōu)先級越高  
  9.         int getOrder();  
  10. }  

該接口可方便實現(xiàn)去判斷支持的事件類型、目標(biāo)類型,及執(zhí)行順序。 

 

Spring事件機(jī)制的簡單例子

本例子模擬一個給多個人發(fā)送內(nèi)容(類似于報紙新聞)的例子。

1、定義事件

Java代碼  收藏代碼
  1. package com.sishuok.hello;  
  2. import org.springframework.context.ApplicationEvent;  
  3. public class ContentEvent extends ApplicationEvent {  
  4.     public ContentEvent(final String content) {  
  5.         super(content);  
  6.     }  
  7. }  

非常簡單,如果用戶發(fā)送內(nèi)容,只需要通過構(gòu)造器傳入內(nèi)容,然后通過getSource即可獲取。

 

2、定義無序監(jiān)聽器

之所以說無序,類似于AOP機(jī)制,順序是無法確定的。

Java代碼  收藏代碼
  1. package com.sishuok.hello;  
  2. import org.springframework.context.ApplicationEvent;  
  3. import org.springframework.context.ApplicationListener;  
  4. import org.springframework.stereotype.Component;  
  5. @Component  
  6. public class LisiListener implements ApplicationListener<ApplicationEvent> {  
  7.     @Override  
  8.     public void onApplicationEvent(final ApplicationEvent event) {  
  9.         if(event instanceof ContentEvent) {  
  10.             System.out.println("李四收到了新的內(nèi)容:" + event.getSource());  
  11.         }  
  12.     }  
  13. }  

1、使用@Compoent注冊Bean即可;

2、在實現(xiàn)中需要判斷event類型是ContentEvent才可以處理;

 

更簡單的辦法是通過泛型指定類型,如下所示

Java代碼  收藏代碼
  1. package com.sishuok.hello;  
  2. import org.springframework.context.ApplicationListener;  
  3. import org.springframework.stereotype.Component;  
  4. @Component  
  5. public class ZhangsanListener implements ApplicationListener<ContentEvent> {  
  6.     @Override  
  7.     public void onApplicationEvent(final ContentEvent event) {  
  8.         System.out.println("張三收到了新的內(nèi)容:" + event.getSource());  
  9.     }  
  10. }  

 

3、定義有序監(jiān)聽器 

實現(xiàn)SmartApplicationListener接口即可。

Java代碼  收藏代碼
  1. package com.sishuok.hello;  
  2. import org.springframework.context.ApplicationEvent;  
  3. import org.springframework.context.event.SmartApplicationListener;  
  4. import org.springframework.stereotype.Component;  
  5.   
  6. @Component  
  7. public class WangwuListener implements SmartApplicationListener {  
  8.   
  9.     @Override  
  10.     public boolean supportsEventType(final Class<? extends ApplicationEvent> eventType) {  
  11.         return eventType == ContentEvent.class;  
  12.     }  
  13.     @Override  
  14.     public boolean supportsSourceType(final Class<?> sourceType) {  
  15.         return sourceType == String.class;  
  16.     }  
  17.     @Override  
  18.     public void onApplicationEvent(final ApplicationEvent event) {  
  19.         System.out.println("王五在孫六之前收到新的內(nèi)容:" + event.getSource());  
  20.     }  
  21.     @Override  
  22.     public int getOrder() {  
  23.         return 1;  
  24.     }  
  25. }  
Java代碼  收藏代碼
  1. package com.sishuok.hello;  
  2. import org.springframework.context.ApplicationEvent;  
  3. import org.springframework.context.event.SmartApplicationListener;  
  4. import org.springframework.stereotype.Component;  
  5.   
  6. @Component  
  7. public class SunliuListener implements SmartApplicationListener {  
  8.   
  9.     @Override  
  10.     public boolean supportsEventType(final Class<? extends ApplicationEvent> eventType) {  
  11.         return eventType == ContentEvent.class;  
  12.     }  
  13.   
  14.     @Override  
  15.     public boolean supportsSourceType(final Class<?> sourceType) {  
  16.         return sourceType == String.class;  
  17.     }  
  18.   
  19.     @Override  
  20.     public void onApplicationEvent(final ApplicationEvent event) {  
  21.         System.out.println("孫六在王五之后收到新的內(nèi)容:" + event.getSource());  
  22.     }  
  23.   
  24.     @Override  
  25.     public int getOrder() {  
  26.         return 2;  
  27.     }  
  28. }  
  1. supportsEventType:用于指定支持的事件類型,只有支持的才調(diào)用onApplicationEvent;
  2. supportsSourceType:支持的目標(biāo)類型,只有支持的才調(diào)用onApplicationEvent;
  3. getOrder:即順序,越小優(yōu)先級越高

 

4、測試 

4.1、配置文件

Java代碼  收藏代碼
  1. <context:component-scan base-package="com.sishuok"/>  

 就一句話,自動掃描注解Bean。

 

4.2、測試類

Java代碼  收藏代碼
  1. package com.sishuok;  
  2. import com.sishuok.hello.ContentEvent;  
  3. import org.junit.Test;  
  4. import org.junit.runner.RunWith;  
  5. import org.springframework.beans.factory.annotation.Autowired;  
  6. import org.springframework.context.ApplicationContext;  
  7. import org.springframework.test.context.ContextConfiguration;  
  8. import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;  
  9.   
  10. @RunWith(SpringJUnit4ClassRunner.class)  
  11. @ContextConfiguration(locations={"classpath:spring-config-hello.xml"})  
  12. public class HelloIT {  
  13.   
  14.     @Autowired  
  15.     private ApplicationContext applicationContext;  
  16.     @Test  
  17.     public void testPublishEvent() {  
  18.         applicationContext.publishEvent(new ContentEvent("今年是龍年的博客更新了"));  
  19.     }  
  20.   
  21. }  

接著會輸出:

Java代碼  收藏代碼
  1. 王五在孫六之前收到新的內(nèi)容:今年是龍年的博客更新了  
  2. 孫六在王五之后收到新的內(nèi)容:今年是龍年的博客更新了  
  3. 李四收到了新的內(nèi)容:今年是龍年的博客更新了  
  4. 張三收到了新的內(nèi)容:今年是龍年的博客更新了  

   

一個簡單的測試?yán)泳脱菔就戤叄椅覀兪褂胹pring的事件機(jī)制去寫相關(guān)代碼會非常簡單。

 

Spring事件機(jī)制實現(xiàn)之前提到的注冊流程

具體請下載源代碼參考com.sishuok.register包里的代碼。此處貼一下源碼結(jié)構(gòu):


  

這里講解一下Spring對異步事件機(jī)制的支持,實現(xiàn)方式有兩種:

 

1、全局異步

即只要是觸發(fā)事件都是以異步執(zhí)行,具體配置(spring-config-register.xml)如下:

 

Java代碼  收藏代碼
  1. <task:executor id="executor" pool-size="10" />  
  2. <!-- 名字必須是applicationEventMulticaster和messageSource是一樣的,默認(rèn)找這個名字的對象 -->  
  3. <!-- 名字必須是applicationEventMulticaster,因為AbstractApplicationContext默認(rèn)找個 -->  
  4. <!-- 如果找不到就new一個,但不是異步調(diào)用而是同步調(diào)用 -->  
  5. <bean id="applicationEventMulticaster" class="org.springframework.context.event.SimpleApplicationEventMulticaster">  
  6.     <!-- 注入任務(wù)執(zhí)行器 這樣就實現(xiàn)了異步調(diào)用(缺點是全局的,要么全部異步,要么全部同步(刪除這個屬性即是同步))  -->  
  7.     <property name="taskExecutor" ref="executor"/>  
  8. </bean>  
通過注入taskExecutor來完成異步調(diào)用。具體實現(xiàn)可參考之前的代碼介紹。這種方式的缺點很明顯:要么大家都是異步,要么大家都不是。所以不推薦使用這種方式。
 

2、更靈活的異步支持

spring3提供了@Aync注解來完成異步調(diào)用。此時我們可以使用這個新特性來完成異步調(diào)用。不僅支持異步調(diào)用,還支持簡單的任務(wù)調(diào)度,比如我的項目就去掉Quartz依賴,直接使用spring3這個新特性,具體可參考spring-config.xml

 

2.1、開啟異步調(diào)用支持

 

Java代碼  收藏代碼
  1. <!-- 開啟@AspectJ AOP代理 -->  
  2. <aop:aspectj-autoproxy proxy-target-class="true"/>  
  3.   
  4. <!-- 任務(wù)調(diào)度器 -->  
  5. <task:scheduler id="scheduler" pool-size="10"/>  
  6.   
  7. <!-- 任務(wù)執(zhí)行器 -->  
  8. <task:executor id="executor" pool-size="10"/>  
  9.   
  10. <!--開啟注解調(diào)度支持 @Async @Scheduled-->  
  11. <task:annotation-driven executor="executor" scheduler="scheduler" proxy-target-class="true"/>  

 

 

2.2、配置監(jiān)聽器讓其支持異步調(diào)用

Java代碼  收藏代碼
  1. @Component  
  2. public class EmailRegisterListener implements ApplicationListener<RegisterEvent> {  
  3.     @Async  
  4.     @Override  
  5.     public void onApplicationEvent(final RegisterEvent event) {  
  6.         System.out.println("注冊成功,發(fā)送確認(rèn)郵件給:" + ((User)event.getSource()).getUsername());  
  7.     }  
  8. }  

使用@Async注解即可,非常簡單。 

 

這樣不僅可以支持通過調(diào)用,也支持異步調(diào)用,非常的靈活,實際應(yīng)用推薦大家使用這種方式。

 

 

通過如上,大體了解了Spring的事件機(jī)制,可以使用該機(jī)制非常簡單的完成如注冊流程,而且對于比較耗時的調(diào)用,可以直接使用Spring自身的異步支持來優(yōu)化。