事件總線:組件之間的發佈-訂閱式通信,而無需組件之間進行顯式註冊。
EventBus
允許組件之間的發佈-訂閱式通信,而無需組件之間進行顯式註冊(因此彼此瞭解)。它是專門爲使用顯式註冊替代傳統的Java進程內事件分發而設計的。它不是一個通用的發佈-訂閱系統,也不打算用於進程間通信。
1.示例
// 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);
}
2.一分鐘指南
將現有的基於EventListener
的系統轉換爲使用EventBus
很容易。
2.1對於監聽器
要監聽事件的特定類型(例如,CustomerChangeEvent
)…
- …在傳統的Java事件中:實現用事件定義的接口,例如
CustomerChangeEventListener
。 - …使用
EventBus
:創建一個接受CustomerChangeEvent
作爲其唯一參數的方法,並使用@Subscribe
註解對其進行標記。
要向事件生產者註冊你的監聽器方法…
- …在傳統的Java事件中:將對象傳遞給每個生產者的
registerCustomerChangeEventListener
方法。這些方法很少在公共接口中定義,因此,除了知道每個可能的生產者之外,還必須知道其類型。 - …使用
EventBus
:將對象傳遞給EventBus
上的EventBus.register(Object)
方法。你需要確保你的對象與事件生產者共享一個EventBus
實例。
要監聽常見事件超類型(例如EventObject
或Object
)…
- …在傳統的Java事件中:不容易。
- …使用
EventBus
:事件會自動調度給任何超類型的監聽器,允許接口類型的監聽器或Object
的“通配符監聽器”。
要監聽和檢測沒有監聽器的情況下調度的事件…
- …在傳統的Java事件中:將代碼添加到每個事件調度方法中(可能使用AOP)。
- …使用
EventBus
:訂閱DeadEvent
。EventBus
將通知你任何已發佈但尚未傳遞的事件。(方便調試)
2.2對於生產者
爲了跟蹤事件的監聽器…
- …在傳統的Java事件中:編寫代碼以管理對象的監聽器列表,包括同步,或使用諸如
EventListenerList
之類的工具類。 - …使用
EventBus
:EventBus
爲你做了這些。
要將事件調度給監聽器…
- …在傳統的Java事件中:編寫一個將事件調度給每個事件監聽器的方法,包括錯誤隔離和(如果需要)異步性。
- …使用
EventBus
:將事件對象傳遞到EventBus
的EventBus.post(Object)
方法。
3.術語表
EventBus
系統和代碼使用以下術語來討論事件分發:
事件 | 可能發佈到總線的任何對象。 |
---|---|
訂閱 | 向EventBus 註冊監聽器的行爲,以便它的處理方法將接收事件。 |
監聽器 | 通過暴露處理方法接收事件的對象。 |
處理方法 | EventBus 用於傳遞已發佈事件的公共方法。處理方法由@Subscribe 註解標記。 |
發佈事件 | 通過EventBus 使事件對給任何監聽器可用。 |
4.常見問題
4.1爲什麼我必須創建自己的事件總線,而不是使用單例?
EventBus
沒有指定你如何使用它;沒有什麼可以阻止你的應用程序爲每個組件使用單獨的EventBus
實例,或者使用單獨的實例按上下文或主題來分隔事件。這也使得在測試中設置和銷燬EventBus
對象變得很簡單。
當然,如果你希望擁有一個進程範圍內的EventBus
單例,那麼沒有什麼可以阻止你這樣做。只需讓你的容器(例如Guice)在全局範圍內創建EventBus
作爲一個單例(或將其存儲在靜態字段中,如果你喜歡這樣操作的話)。
簡而言之,EventBus
不是單例的,因爲我們不想爲你做出這樣的決定。你喜歡怎麼用就怎麼用。
4.2我可以從事件總線上註銷一個監聽器嗎?
可以,使用EventBus.unregister
,但是我們發現很少需要它:
- 大多數監聽器是在啓動或延遲初始化時註冊的,並在應用程序的生命週期內都存在。
- 特定作用域的
EventBus
實例可以處理臨時事件分發(例如,在請求作用域內的對象之間分發事件) - 爲了進行測試,可以輕鬆創建和銷燬
EventBus
實例,從而無需顯式地註銷。
4.3爲什麼使用註解來標記處理方法,而不是要求監聽器實現接口?
我們認爲,事件總線的@Subscribe
註解傳達的意圖與實現接口一樣明確(或者可能更明確),同時讓你可以隨意在任意位置放置事件處理程序方法,併爲它們提供意圖公開的名稱。
傳統的Java事件使用一個監聽器接口,該接口通常只使用幾種方法——通常是一種。這有許多缺點:
- 任何一個類只能實現對給定事件單個響應。
- 監聽器接口方法可能會衝突。
- 該方法必須以事件(例如
handleChangeEvent
)命名,而不是以其用途(例如recordChangeInJournal
)命名。 - 每個事件通常都有其自己的接口,而沒有用於一系列事件(例如,所有UI事件)的公共父接口。
整潔地實現這一點上的困難引出了一種模式,該模式在Swing應用程序中尤其常見,即使用微小的匿名類來實現事件監聽器接口。
比較這兩種情況:
class ChangeRecorder {
void setCustomer(Customer cust) {
cust.addChangeListener(new ChangeListener() {
public void customerChanged(ChangeEvent e) {
recordChange(e.getChange());
}
};
}
}
與
// Class is typically registered by the container.
class EventBusChangeRecorder {
@Subscribe public void recordCustomerChange(ChangeEvent e) {
recordChange(e.getChange());
}
}
在第二種情況下,意圖實際上更加清晰:干擾代碼更少,事件處理具有清晰且有意義的名稱。
4.4通用Handler<T>
接口怎麼樣呢?
有些人爲EventBus
監聽器提出了一個通用的Handler<T>
接口。Java使用類型擦除會遇到問題,更不用說可用性方面的問題了。
假設接口看起來像下面這樣:
interface Handler<T> {
void handleEvent(T event);
}
由於擦除的原因,沒有單個類可以使用不同的類型參數多次實現通用接口。這是對傳統Java事件的巨大倒退,在傳統Java事件中,即使actionPerformed
和keyPressed
不是很有意義的名稱,至少你可以實現這兩種方法!
4.5EventBus
不會破壞靜態類型並消除自動重構支持嗎?
有些人對EventBus
的register(Object)
和post(Object)
方法對Object
類型的使用感到抓狂。
這裏使用Object
對象有一個很好的理由:事件總線庫對事件監聽器(如在register(Object)
中)或事件本身(在post(Object)
中)的類型沒有任何限制。
另一方面,事件處理方法必須顯式聲明它們的參數類型——所需的事件類型(或其超類型之一)。因此,搜索對事件類的引用將立即找到該事件的所有處理方法,而重命名該類型將影響IDE視圖(以及創建該事件的任何代碼)中的所有處理程序方法。
的確,你可以隨意重命名@Subscribed
事件處理方法。事件總線不會停止此操作,也不會做任何傳播重命名的操作,因爲對於事件總線,處理方法的名稱無關緊要。當然,直接調用這些方法的測試代碼將受到重命名的影響——但這正是重構工具的作用所在。我們將其視爲特性,而不是錯誤bug:能夠隨意重命名處理方法,可以使它們的含義更清晰。
4.6如果我註冊了沒有任何處理方法的監聽器會發生什麼?
什麼也不會發生。
事件總線被設計爲與容器和模塊系統集成,Guice是一個典型的例子。在這些情況下,讓容器/工廠/環境將每個創建的對象傳遞給EventBus
的register(Object)
方法很方便。
這樣,由容器/工廠/環境創建的任何對象都可以通過簡單地暴露處理方法而掛接到系統的事件模型中。
4.7在編譯時可以檢測到哪些事件總線問題?
Java的類型系統可以明確地檢測到任何問題。例如,爲不存在的事件類型定義處理方法。
4.8在註冊時可以立即檢測到哪些事件總線問題?
調用register(Object)
時,將立即檢查正在註冊的監聽器的處理方法的格式是否正確。特別地,任何標有@Subscribe
的方法都只能接受一個參數。
任何違反此規則的行爲都將導致拋出IllegalArgumentException
。
(我們正在研究的解決方案是使用APT,它可以將這種檢查移至編譯時。)
4.9哪些EventBus
問題可能只在以後運行時檢測到?
如果組件在沒有註冊監聽器的情況下發布事件,則可能顯示錯誤(通常表示你錯過了@Subscribe
註解或未加載監聽組件)。
(注意,這不一定表示有問題。在許多情況下,應用程序會故意忽略已發佈的事件,尤其是如果該事件來自你無法控制的代碼時。)
要處理此類事件,請爲DeadEvent
類註冊一個處理方法。每當EventBus
收到沒有註冊處理的事件時,它將把它變成DeadEvent
並以你的方式傳遞給它——允許你記錄它或以其他方式恢復。
4.10如何測試事件監聽器及其處理方法?
由於監聽器類上的處理方法是常規方法,因此你可以簡單地從測試代碼中調用它們以模擬EventBus
。
4.11爲什麼我不能用EventBus
做<泛型魔法>?
EventBus
被設計用來很好很好地處理大量用例。對於大多數用例,我們更喜歡一針見血,而不是在所有用例上都做得得體。
此外,使EventBus
可擴展——使其擴展有用和高效,同時仍然允許我們自己添加與你的任何擴展都不衝突的核心EventBus
API——是一個非常困難的問題。
如果你真的,真的需要泛型魔法X,EventBus
當前不能提供,你可以提出問題,然後設計自己的替代方案。