事件驅動模型簡介

事件驅動模型也就是我們常說的觀察者,或者釋出-訂閱模型;理解它的幾個關鍵點:

  1. 首先是一種物件間的一對多的關係;最簡單的如交通訊號燈,訊號燈是目標(一方),行人注視著訊號燈(多方);
  2. 當目標傳送改變(釋出),觀察者(訂閱者)就可以接收到改變;
  3. 觀察者如何處理(如行人如何走,是快走/慢走/不走,目標不會管的),目標無需干涉;所以就鬆散耦合了它們之間的關係。

接下來先看一個使用者註冊的例子:



使用者註冊成功後,需要做這麼多事:

1、加積分

2、發確認郵件

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

4、索引使用者資料

…………

問題:

  1. UserService和其他Service耦合嚴重,增刪功能比較麻煩;
  2. 有些功能可能需要呼叫第三方系統,如增加積分/索引使用者,速度可能比較慢,此時需要非同步支援;這個如果使用Spring,可以輕鬆解決,後邊再介紹;

從如上例子可以看出,應該使用一個觀察者來解耦這些Service之間的依賴關係,如圖:


增加了一個Listener來解耦UserService和其他服務,即註冊成功後,只需要通知相關的監聽器,不需要關係它們如何處理。增刪功能非常容易。

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

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

還是就是使用了IoC思想,即以前主動去建立它依賴的Service,現在只是被動等待別人註冊進來。

其他的例子還有如GUI中的按鈕和動作的關係,按鈕和動作本身都是一種抽象,每個不同的按鈕的動作可能不一樣;如“檔案-->新建”開啟新建視窗;點選“關閉”按鈕關閉視窗等等。

主要目的是:鬆散耦合物件間的一對多的依賴關係,如按鈕和動作的關係;

如何實現呢?面向介面程式設計(即面向抽象程式設計),而非面向實現。即按鈕和動作可以定義為介面,這樣它倆的依賴是最小的(如在Java中,沒有比介面更抽象的了)。

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

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

從上邊大家應該能體會出介面、抽象類的主要目的了。現在想想其實很簡單。

在Java中介面還一個非常重要的好處:介面是可以多實現的,類/抽象類只能單繼承,所以使用介面可以非常容易擴充套件新功能(還可以實現所謂的mixin),類/抽象類辦不到。

Java GUI事件驅動模型/觀察者 

扯遠了,再來看看Java GUI世界裡的事件驅動模型吧:

如果寫過AWT/Swing程式,應該知道其所有元件都繼承自java.awt.Component抽象類,其內部提供了addXXXListener(XXXListener l) 註冊監聽器的方法,即Component與實際動作之間依賴於XXXListener抽象。

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

還有如java.awt.Button,提供了一個addActionListener(ActionListener l),用於註冊點選後觸發的ActionListener實現。

元件是一個抽象類,其好處主要是複用,比如複用這些監聽器的觸發及管理等。

JavaBean規範的事件驅動模型/觀察者

JavaBean規範提供了JavaBean的PropertyEditorSupport及PropertyChangeListener支援。

PropertyEditorSupport就是目標,而PropertyChangeListener就是監聽器,大家可以google搜尋下,具體網上有很多例子。

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

JDK內部直接提供了觀察者模式的抽象:

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

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

因為網上介紹的非常多了,請google搜尋瞭解如何使用這個抽象及推/拉模型的優缺點。

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

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

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

事件

具體代表者是:ApplicationEvent:

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

2、系統預設提供瞭如下ApplicationEvent事件實現:


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

  • ContextStartedEvent:ApplicationContext啟動後觸發的事件;(目前版本沒有任何作用)
  • ContextStoppedEvent:ApplicationContext停止後觸發的事件;(目前版本沒有任何作用)
  • ContextRefreshedEvent:ApplicationContext初始化或重新整理完成後觸發的事件;(容器初始化完成後呼叫)
  • ContextClosedEvent:ApplicationContext關閉後觸發的事件;(如web容器關閉時自動會觸發spring容器的關閉,如果是普通java應用,需要呼叫ctx.registerShutdownHook();註冊虛擬機器關閉時的鉤子才行)

注:org.springframework.context.support.AbstractApplicationContext抽象類實現了LifeCycle的start和stop回撥併發布ContextStartedEvent和ContextStoppedEvent事件;但是無任何實現呼叫它,所以目前無任何作用。

目標(釋出事件者)

具體代表者是:ApplicationEventPublisher及ApplicationEventMulticaster,系統預設提供瞭如下實現:

1、ApplicationContext介面繼承了ApplicationEventPublisher,並在AbstractApplicationContext實現了具體程式碼,實際執行是委託給ApplicationEventMulticaster(可以認為是多播):

Java程式碼

	public void publishEvent(ApplicationEvent event) {
		//省略部分程式碼
		}
		getApplicationEventMulticaster().multicastEvent(event);
		if (this.parent != null) {
			this.parent.publishEvent(event);
		}
	}

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

2、ApplicationContext自動到本地容器裡找一個名字為”“的ApplicationEventMulticaster實現,如果沒有自己new一個SimpleApplicationEventMulticaster。其中SimpleApplicationEventMulticaster釋出事件的程式碼如下:

Java程式碼

	public void multicastEvent(final ApplicationEvent event) {
		for (final ApplicationListener listener : getApplicationListeners(event)) {
			Executor executor = getTaskExecutor();
			if (executor != null) {
				executor.execute(new Runnable() {
					public void run() {
						listener.onApplicationEvent(event);
					}
				});
			}
			else {
				listener.onApplicationEvent(event);
			}
		}
	}

 大家可以看到如果給它一個executor(java.util.concurrent.Executor),它就可以非同步支援釋出事件了。佛則就是通過傳送。

所以我們傳送事件只需要通過ApplicationContext.publishEvent即可,沒必要再建立自己的實現了。除非有必要。 

監聽器

具體代表者是:ApplicationListener

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

2、ApplicationListener介面:

Java程式碼

public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
	void onApplicationEvent(E event);
}

其只提供了onApplicationEvent方法,我們需要在該方法實現內部判斷事件型別來處理,也沒有提供按順序觸發監聽器的語義,所以Spring提供了另一個介面,SmartApplicationListener:

Java程式碼

public interface SmartApplicationListener extends ApplicationListener<ApplicationEvent>, Ordered {
        //如果實現支援該事件型別 那麼返回true
	boolean supportsEventType(Class<? extends ApplicationEvent> eventType);
  
        //如果實現支援“目標”型別,那麼返回true
	boolean supportsSourceType(Class<?> sourceType);
       
        //順序,即監聽器執行的順序,值越小優先順序越高
        int getOrder();
}

該介面可方便實現去判斷支援的事件型別、目標型別,及執行順序。 

Spring事件機制的簡單例子

本例子模擬一個給多個人傳送內容(類似於報紙新聞)的例子。

1、定義事件

Java程式碼

package com.zuidaima.hello;
import org.springframework.context.ApplicationEvent;
public class ContentEvent extends ApplicationEvent {
    public ContentEvent(final String content) {
        super(content);
    }
}

非常簡單,如果使用者傳送內容,只需要通過構造器傳入內容,然後通過getSource即可獲取。

2、定義無序監聽器

之所以說無序,類似於AOP機制,順序是無法確定的。

Java程式碼

package com.zuidaima.hello;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
public class LisiListener implements ApplicationListener<ApplicationEvent> {
    @Override
    public void onApplicationEvent(final ApplicationEvent event) {
        if(event instanceof ContentEvent) {
            System.out.println("李四收到了新的內容:" + event.getSource());
        }
    }
}

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

2、在實現中需要判斷event型別是ContentEvent才可以處理;

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

Java程式碼

package com.zuidaima.hello;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
public class ZhangsanListener implements ApplicationListener<ContentEvent> {
    @Override
    public void onApplicationEvent(final ContentEvent event) {
        System.out.println("張三收到了新的內容:" + event.getSource());
    }
}

3、定義有序監聽器 

實現SmartApplicationListener介面即可。

Java程式碼

package com.zuidaima.hello;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class WangwuListener implements SmartApplicationListener {

    @Override
    public boolean supportsEventType(final Class<? extends ApplicationEvent> eventType) {
        return eventType == ContentEvent.class;
    }
    @Override
    public boolean supportsSourceType(final Class<?> sourceType) {
        return sourceType == String.class;
    }
    @Override
    public void onApplicationEvent(final ApplicationEvent event) {
        System.out.println("王五在孫六之前收到新的內容:" + event.getSource());
    }
    @Override
    public int getOrder() {
        return 1;
    }
}

Java程式碼

package com.zuidaima.hello;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class SunliuListener implements SmartApplicationListener {

    @Override
    public boolean supportsEventType(final Class<? extends ApplicationEvent> eventType) {
        return eventType == ContentEvent.class;
    }

    @Override
    public boolean supportsSourceType(final Class<?> sourceType) {
        return sourceType == String.class;
    }

    @Override
    public void onApplicationEvent(final ApplicationEvent event) {
        System.out.println("孫六在王五之後收到新的內容:" + event.getSource());
    }

    @Override
    public int getOrder() {
        return 2;
    }
}
  1. supportsEventType:用於指定支援的事件型別,只有支援的才呼叫onApplicationEvent;
  2. supportsSourceType:支援的目標型別,只有支援的才呼叫onApplicationEvent;
  3. getOrder:即順序,越小優先順序越高

4、測試 

4.1、配置檔案

Java程式碼

<context:component-scan base-package="com.zuidaima"/>

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

4.2、測試類

Java程式碼

package com.zuidaima;
import com.zuidaima.hello.ContentEvent;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:spring-config-hello.xml"})
public class HelloIT {

    @Autowired
    private ApplicationContext applicationContext;
    @Test
    public void testPublishEvent() {
        applicationContext.publishEvent(new ContentEvent("今年是龍年的部落格更新了"));
    }

}

接著會輸出:

Java程式碼

王五在孫六之前收到新的內容:今年是龍年的部落格更新了
孫六在王五之後收到新的內容:今年是龍年的部落格更新了
李四收到了新的內容:今年是龍年的部落格更新了
張三收到了新的內容:今年是龍年的部落格更新了

一個簡單的測試例子就演示完畢,而且我們使用spring的事件機制去寫相關程式碼會非常簡單。

Spring事件機制實現之前提到的註冊流程

具體請下載原始碼參考com.zuidaima.register包裡的程式碼。此處貼一下原始碼結構:


  

這裡講解一下Spring對非同步事件機制的支援,實現方式有兩種:

1、全域性非同步

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

Java程式碼

    <task:executor id="executor" pool-size="10" />
    <!-- 名字必須是applicationEventMulticaster和messageSource是一樣的,預設找這個名字的物件 -->
    <!-- 名字必須是applicationEventMulticaster,因為AbstractApplicationContext預設找個 -->
    <!-- 如果找不到就new一個,但不是非同步呼叫而是同步呼叫 -->
    <bean id="applicationEventMulticaster" class="org.springframework.context.event.SimpleApplicationEventMulticaster">
        <!-- 注入任務執行器 這樣就實現了非同步呼叫(缺點是全域性的,要麼全部非同步,要麼全部同步(刪除這個屬性即是同步))  -->
        <property name="taskExecutor" ref="executor"/>
    </bean>

通過注入taskExecutor來完成非同步呼叫。具體實現可參考之前的程式碼介紹。這種方式的缺點很明顯:要麼大家都是非同步,要麼大家都不是。所以不推薦使用這種方式。

2、更靈活的非同步支援

spring3提供了@Aync註解來完成非同步呼叫。此時我們可以使用這個新特性來完成非同步呼叫。不僅支援非同步呼叫,還支援簡單的任務排程,比如我的專案就去掉Quartz依賴,直接使用spring3這個新特性,具體可參考spring-config.xml

2.1、開啟非同步呼叫支援

Java程式碼

    <!-- 開啟@AspectJ AOP代理 -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>

    <!-- 任務排程器 -->
    <task:scheduler id="scheduler" pool-size="10"/>

    <!-- 任務執行器 -->
    <task:executor id="executor" pool-size="10"/>

    <!--開啟註解排程支援 @Async @Scheduled-->
    <task:annotation-driven executor="executor" scheduler="scheduler" proxy-target-class="true"/>

2.2、配置監聽器讓其支援非同步呼叫

Java程式碼

@Component
public class EmailRegisterListener implements ApplicationListener<RegisterEvent> {
    @Async
    @Override
    public void onApplicationEvent(final RegisterEvent event) {
        System.out.println("註冊成功,傳送確認郵件給:" + ((User)event.getSource()).getUsername());
    }
}

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

這樣不僅可以支援通過呼叫,也支援非同步呼叫,非常的靈活,實際應用推薦大家使用這種方式。

通過如上,大體瞭解了Spring的事件機制,可以使用該機制非常簡單的完成如註冊流程,而且對於比較耗時的呼叫,可以直接使用Spring自身的非同步支援來優化。

程式碼下載地址:event.rar (10.5 KB)