# 11-事件總線
[原文鏈接](http://code.google.com/p/guava-libraries/wiki/EventBusExplained) [譯文連接](http://ifeve.com/google-guava-eventbus) **譯者:**沈義揚
傳統上,Java的**進程內事件分發**都是通過發布者和訂閱者之間的顯式注冊實現的。設計[EventBus](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/eventbus/EventBus.html)就是為了取代這種顯示注冊方式,使組件間有了更好的解耦。EventBus不是通用型的發布-訂閱實現,不適用于進程間通信。
## 范例
```
// Class is typically registered by the container.
class EventBusChangeRecorder {
@Subscribe public void recordCustomerChange(ChangeEvent e) {
recordChange(e.getChange());
}
}
// somewhere during initialization
eventBus.register(new EventBusChangeRecorder());
// much later
public void changeCustomer() {
ChangeEvent event = getChangeEvent();
eventBus.post(event);
}
```
## 一分鐘指南
把已有的進程內事件分發系統遷移到EventBus非常簡單。
### 事件監聽者[Listeners]
監聽特定事件(如,CustomerChangeEvent):
* 傳統實現:定義相應的事件監聽者類,如CustomerChangeEventListener;
* EventBus實現:以CustomerChangeEvent為唯一參數創建方法,并用[Subscribe](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/eventbus/Subscribe.html)注解標記。
把事件監聽者注冊到事件生產者:
* 傳統實現:調用事件生產者的registerCustomerChangeEventListener方法;這些方法很少定義在公共接口中,因此開發者必須知道所有事件生產者的類型,才能正確地注冊監聽者;
* EventBus實現:在EventBus**實例**上調用[EventBus.register(Object)](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/eventbus/EventBus.html#register%28java.lang.Object%29)方法;請保證事件生產者和監聽者共享相同的EventBus**實例**。
按事件超類監聽(如,EventObject甚至Object):
* 傳統實現:很困難,需要開發者自己去實現匹配邏輯;
* EventBus實現:EventBus自動把事件分發給事件超類的監聽者,并且允許監聽者聲明監聽接口類型和泛型的通配符類型(wildcard,如 ? super XXX)。
檢測沒有監聽者的事件:
* 傳統實現:在每個事件分發方法中添加邏輯代碼(也可能適用AOP);
* EventBus實現:監聽[DeadEvent](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/eventbus/DeadEvent.html);EventBus會把所有發布后沒有監聽者處理的事件包裝為DeadEvent(對調試很便利)。
### 事件生產者[Producers]
管理和追蹤監聽者:
* 傳統實現:用列表管理監聽者,還要考慮線程同步;或者使用工具類,如EventListenerList;
* EventBus實現:EventBus內部已經實現了監聽者管理。
向監聽者分發事件:
* 傳統實現:開發者自己寫代碼,包括事件類型匹配、異常處理、異步分發;
* EventBus實現:把事件傳遞給 [EventBus.post(Object)](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/eventbus/EventBus.html#post%28java.lang.Object%29)方法。異步分發可以直接用EventBus的子類[AsyncEventBus](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/eventbus/AsyncEventBus.html)。
## 術語表
事件總線系統使用以下術語描述事件分發:
| 事件 | 可以向事件總線發布的對象 |
|:--- |:--- |
| 訂閱 | 向事件總線注冊_監聽者_以接受事件的行為 |
| 監聽者 | 提供一個_處理方法_,希望接受和處理事件的對象 |
| 處理方法 | 監聽者提供的公共方法,事件總線使用該方法向監聽者發送事件;該方法應該用Subscribe注解 |
| 發布消息 | 通過事件總線向所有匹配的監聽者提供事件 |
## 常見問題解答[FAQ]
**為什么一定要創建****EventBus****實例,而不是使用單例模式?**
EventBus不想給定開發者怎么使用;你可以在應用程序中按照不同的組件、上下文或業務主題分別使用不同的事件總線。這樣的話,在測試過程中開啟和關閉某個部分的事件總線,也會變得更簡單,影響范圍更小。
當然,如果你想在進程范圍內使用唯一的事件總線,你也可以自己這么做。比如在容器中聲明EventBus為全局單例,或者用一個靜態字段存放EventBus,如果你喜歡的話。
簡而言之,EventBus不是單例模式,是因為我們不想為你做這個決定。你喜歡怎么用就怎么用吧。
**可以從事件總線中注銷監聽者嗎? ?**
當然可以,使用EventBus.unregister(Object)方法,但我們發現這種需求很少:
* 大多數監聽者都是在啟動或者模塊懶加載時注冊的,并且在應用程序的整個生命周期都存在;
* 可以使用特定作用域的事件總線來處理臨時事件,而不是注冊/注銷監聽者;比如在請求作用域[request-scoped]的對象間分發消息,就可以同樣適用請求作用域的事件總線;
* 銷毀和重建事件總線的成本很低,有時候可以通過銷毀和重建事件總線來更改分發規則。
**為什么使用注解標記處理方法,而不是要求監聽者實現接口?**
我們覺得注解和實現接口一樣傳達了明確的語義,甚至可能更好。同時,使用注解也允許你把處理方法放到任何地方,和使用業務意圖清晰的方法命名。
傳統的Java實現中,監聽者使用方法很少的接口——通常只有一個方法。這樣做有一些缺點:
* 監聽者類對給定事件類型,只能有單一處理邏輯;
* 監聽者接口方法可能沖突;
* 方法命名只和事件相關(handleChangeEvent),不能表達意圖(recordChangeInJournal);
* 事件通常有自己的接口,而沒有按類型定義的公共父接口(如所有的UI事件接口)。
接口實現監聽者的方式很難做到簡潔,這甚至引出了一個模式,尤其是在Swing應用中,那就是用匿名類實現事件監聽者的接口。比較以下兩種實現:
```
class ChangeRecorder {
void setCustomer(Customer cust) {
cust.addChangeListener(new ChangeListener() {
public void customerChanged(ChangeEvent e) {
recordChange(e.getChange());
}
};
}
}
```
```
//這個監聽者類通常由容器注冊給事件總線
class EventBusChangeRecorder {
@Subscribe public void recordCustomerChange(ChangeEvent e) {
recordChange(e.getChange());
}
}
```
第二種實現的業務意圖明顯更加清晰:沒有多余的代碼,并且處理方法的名字是清晰和有意義的。
**通用的監聽者接口****Handler<T>****怎么樣?**
有些人已經建議過用泛型定義一個通用的監聽者接口Handler<T>。這有點牽扯到Java類型擦除的問題,假設我們有如下這個接口:
```
interface Handler<T> {
void handleEvent(T event);
}
```
因為類型擦除,Java禁止一個類使用不同的類型參數多次實現同一個泛型接口(即不可能出現MultiHandler implements Handler<Type1>, Handler<Type2>)。這比起傳統的Java事件機制也是巨大的退步,至少傳統的Java Swing監聽者接口使用了不同的方法把不同的事件區分開。
**EventBus****不是破壞了靜態類型,排斥了自動重構支持嗎?**
有些人被EventBus的register(Object) 和post(Object)方法直接使用Object做參數嚇壞了。
這里使用Object參數有一個很好的理由:EventBus對事件監聽者類型和事件本身的類型都不作任何限制。
另一方面,處理方法必須要明確地聲明參數類型——期望的事件類型(或事件的父類型)。因此,搜索一個事件的類型引用,可以馬上找到針對該事件的處理方法,對事件類型的重命名也會在IDE中自動更新所有的處理方法。
在EventBus的架構下,你可以任意重命名@Subscribe注解的處理方法,并且這類重命名不會被傳播(即不會引起其他類的修改),因為對EventBus來說,處理方法的名字是無關緊要的。如果測試代碼中直接調用了處理方法,那么當然,重命名處理方法會引起測試代碼的變動,但使用EventBus觸發處理方法的代碼就不會發生變更。我們認為這是EventBus的特性,而不是漏洞:能夠任意重命名處理方法,可以讓你的處理方法命名更清晰。
**如果我注冊了一個沒有任何處理方法的監聽者,會發生什么?**
什么也不會發生。
EventBus旨在與容器和模塊系統整合,Guice就是個典型的例子。在這種情況下,可以方便地讓容器/工廠/運行環境傳遞任意創建好的對象給EventBus的register(Object)方法。
這樣,任何容器/工廠/運行環境創建的對象都可以簡便地通過暴露處理方法掛載到系統的事件模塊。
**編譯時能檢測到****EventBus****的哪些問題?**
Java類型系統可以明白地檢測到的任何問題。比如,為一個不存在的事件類型定義處理方法。
**運行時往****EventBus****注冊監聽者,可以立即檢測到哪些問題?**
一旦調用了register(Object) 方法,EventBus就會檢查監聽者中的處理方法是否結構正確的[well-formedness]。具體來說,就是每個用@Subscribe注解的方法都只能有一個參數。
違反這條規則將引起IllegalArgumentException(這條規則檢測也可以用APT在編譯時完成,不過我們還在研究中)。
**哪些問題只能在之后事件傳播的運行時才會被檢測到?**
如果組件傳播了一個事件,但找不到相應的處理方法,EventBus_可能_會指出一個錯誤(通常是指出@Subscribe注解的缺失,或沒有加載監聽者組件)。
_請注意這個指示并不一定表示應用有問題。一個應用中可能有好多場景會故意忽略某個事件,尤其當事件來源于不可控代碼時_
你可以注冊一個處理方法專門處理DeadEvent類型的事件。每當EventBus收到沒有對應處理方法的事件,它都會將其轉化為DeadEvent,并且傳遞給你注冊的DeadEvent處理方法——你可以選擇記錄或修復該事件。
**怎么測試監聽者和它們的處理方法?**
因為監聽者的處理方法都是普通方法,你可以簡便地在測試代碼中模擬EventBus調用這些方法。
**為什么我不能在****EventBus****上使用****<**泛型**魔法****>****?**
EventBus旨在很好地處理一大類用例。我們更喜歡針對大多數用例直擊要害,而不是在所有用例上都保持體面。
此外,泛型也讓EventBus的可擴展性——讓它有益、高效地擴展,同時我們對EventBus的增補不會和你們的擴展相沖突——成為一個非常棘手的問題。
如果你真的很想用泛型,EventBus目前還不能提供,你可以提交一個問題并且設計自己的替代方案。
- Google Guava官方教程(中文版)
- 1-基本工具
- 1.1-使用和避免null
- 1.2-前置條件
- 1.3-常見Object方法
- 1.4-排序: Guava強大的”流暢風格比較器”
- 1.5-Throwables:簡化異常和錯誤的傳播與檢查
- 2-集合
- 2.1-不可變集合
- 2.2-新集合類型
- 2.3-強大的集合工具類:java.util.Collections中未包含的集合工具
- 2.4-集合擴展工具類
- 3-緩存
- 4-函數式編程
- 5-并發
- 5.1-google Guava包的ListenableFuture解析
- 5.2-Google-Guava Concurrent包里的Service框架淺析
- 6-字符串處理:分割,連接,填充
- 7-原生類型
- 9-I/O
- 10-散列
- 11-事件總線
- 12-數學運算
- 13-反射