傳統(tǒng)上,Java 的進(jìn)程內(nèi)事件分發(fā)都是通過(guò)發(fā)布者和訂閱者之間的顯式注冊(cè)實(shí)現(xiàn)的。設(shè)計(jì) EventBus 就是為了取代這種顯示注冊(cè)方式,使組件間有了更好的解耦。EventBus 不是通用型的發(fā)布-訂閱實(shí)現(xiàn),不適用于進(jìn)程間通信。
// 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);
}
把已有的進(jìn)程內(nèi)事件分發(fā)系統(tǒng)遷移到 EventBus 非常簡(jiǎn)單。
監(jiān)聽(tīng)特定事件(如,CustomerChangeEvent):
把事件監(jiān)聽(tīng)者注冊(cè)到事件生產(chǎn)者:
按事件超類(lèi)監(jiān)聽(tīng)(如,EventObject 甚至 Object):
檢測(cè)沒(méi)有監(jiān)聽(tīng)者的事件:
管理和追蹤監(jiān)聽(tīng)者:
傳統(tǒng)實(shí)現(xiàn):用列表管理監(jiān)聽(tīng)者,還要考慮線程同步;或者使用工具類(lèi),如 EventListenerList; EventBus實(shí)現(xiàn):EventBus 內(nèi)部已經(jīng)實(shí)現(xiàn)了監(jiān)聽(tīng)者管理。
向監(jiān)聽(tīng)者分發(fā)事件:
事件總線系統(tǒng)使用以下術(shù)語(yǔ)描述事件分發(fā):
事件 | 可以向事件總線發(fā)布的對(duì)象 |
訂閱 | 向事件總線注冊(cè)監(jiān)聽(tīng)者以接受事件的行為 |
監(jiān)聽(tīng)者 | 提供一個(gè)處理方法,希望接受和處理事件的對(duì)象 |
處理方法 | 監(jiān)聽(tīng)者提供的公共方法,事件總線使用該方法向監(jiān)聽(tīng)者發(fā)送事件;該方法應(yīng)該用 Subscribe 注解 |
發(fā)布消息 | 通過(guò)事件總線向所有匹配的監(jiān)聽(tīng)者提供事件 |
為什么一定要?jiǎng)?chuàng)建 EventBus 實(shí)例,而不是使用單例模式?
EventBus 不想給定開(kāi)發(fā)者怎么使用;你可以在應(yīng)用程序中按照不同的組件、上下文或業(yè)務(wù)主題分別使用不同的事件總線。這樣的話,在測(cè)試過(guò)程中開(kāi)啟和關(guān)閉某個(gè)部分的事件總線,也會(huì)變得更簡(jiǎn)單,影響范圍更小。
當(dāng)然,如果你想在進(jìn)程范圍內(nèi)使用唯一的事件總線,你也可以自己這么做。比如在容器中聲明 EventBus 為全局單例,或者用一個(gè)靜態(tài)字段存放 EventBus,如果你喜歡的話。
簡(jiǎn)而言之,EventBus 不是單例模式,是因?yàn)槲覀儾幌霝槟阕鲞@個(gè)決定。你喜歡怎么用就怎么用吧。
可以從事件總線中注銷(xiāo)監(jiān)聽(tīng)者嗎?
當(dāng)然可以,使用 EventBus.unregister(Object)方法,但我們發(fā)現(xiàn)這種需求很少:
為什么使用注解標(biāo)記處理方法,而不是要求監(jiān)聽(tīng)者實(shí)現(xiàn)接口?
我們覺(jué)得注解和實(shí)現(xiàn)接口一樣傳達(dá)了明確的語(yǔ)義,甚至可能更好。同時(shí),使用注解也允許你把處理方法放到任何地方,和使用業(yè)務(wù)意圖清晰的方法命名。
傳統(tǒng)的 Java 實(shí)現(xiàn)中,監(jiān)聽(tīng)者使用方法很少的接口——通常只有一個(gè)方法。這樣做有一些缺點(diǎn):
接口實(shí)現(xiàn)監(jiān)聽(tīng)者的方式很難做到簡(jiǎn)潔,這甚至引出了一個(gè)模式,尤其是在 Swing 應(yīng)用中,那就是用匿名類(lèi)實(shí)現(xiàn)事件監(jiān)聽(tīng)者的接口。比較以下兩種實(shí)現(xiàn):
class ChangeRecorder {
void setCustomer(Customer cust) {
cust.addChangeListener(new ChangeListener() {
public void customerChanged(ChangeEvent e) {
recordChange(e.getChange());
}
};
}
}
//這個(gè)監(jiān)聽(tīng)者類(lèi)通常由容器注冊(cè)給事件總線
class EventBusChangeRecorder {
@Subscribe public void recordCustomerChange(ChangeEvent e) {
recordChange(e.getChange());
}
}
第二種實(shí)現(xiàn)的業(yè)務(wù)意圖明顯更加清晰:沒(méi)有多余的代碼,并且處理方法的名字是清晰和有意義的。
通用的監(jiān)聽(tīng)者接口 Handler
有些人已經(jīng)建議過(guò)用泛型定義一個(gè)通用的監(jiān)聽(tīng)者接口 Handler
interface Handler<T> {
void handleEvent(T event);
}
因?yàn)轭?lèi)型擦除,Java 禁止一個(gè)類(lèi)使用不同的類(lèi)型參數(shù)多次實(shí)現(xiàn)同一個(gè)泛型接口(即不可能出現(xiàn) MultiHandler implements Handler
EventBus 不是破壞了靜態(tài)類(lèi)型,排斥了自動(dòng)重構(gòu)支持嗎?
有些人被 EventBus 的 register(Object) 和 post(Object) 方法直接使用 Object 做參數(shù)嚇壞了。
這里使用 Object 參數(shù)有一個(gè)很好的理由:EventBus 對(duì)事件監(jiān)聽(tīng)者類(lèi)型和事件本身的類(lèi)型都不作任何限制。
另一方面,處理方法必須要明確地聲明參數(shù)類(lèi)型——期望的事件類(lèi)型(或事件的父類(lèi)型)。因此,搜索一個(gè)事件的類(lèi)型引用,可以馬上找到針對(duì)該事件的處理方法,對(duì)事件類(lèi)型的重命名也會(huì)在 IDE 中自動(dòng)更新所有的處理方法。
在 EventBus 的架構(gòu)下,你可以任意重命名@Subscribe 注解的處理方法,并且這類(lèi)重命名不會(huì)被傳播(即不會(huì)引起其他類(lèi)的修改),因?yàn)閷?duì) EventBus 來(lái)說(shuō),處理方法的名字是無(wú)關(guān)緊要的。如果測(cè)試代碼中直接調(diào)用了處理方法,那么當(dāng)然,重命名處理方法會(huì)引起測(cè)試代碼的變動(dòng),但使用 EventBus 觸發(fā)處理方法的代碼就不會(huì)發(fā)生變更。我們認(rèn)為這是 EventBus 的特性,而不是漏洞:能夠任意重命名處理方法,可以讓你的處理方法命名更清晰。
如果我注冊(cè)了一個(gè)沒(méi)有任何處理方法的監(jiān)聽(tīng)者,會(huì)發(fā)生什么?
什么也不會(huì)發(fā)生。
EventBus 旨在與容器和模塊系統(tǒng)整合,Guice 就是個(gè)典型的例子。在這種情況下,可以方便地讓容器/工廠/運(yùn)行環(huán)境傳遞任意創(chuàng)建好的對(duì)象給 EventBus 的 register(Object)方法。
這樣,任何容器/工廠/運(yùn)行環(huán)境創(chuàng)建的對(duì)象都可以簡(jiǎn)便地通過(guò)暴露處理方法掛載到系統(tǒng)的事件模塊。
編譯時(shí)能檢測(cè)到 EventBus 的哪些問(wèn)題?
Java 類(lèi)型系統(tǒng)可以明白地檢測(cè)到的任何問(wèn)題。比如,為一個(gè)不存在的事件類(lèi)型定義處理方法。
運(yùn)行時(shí)往 EventBus 注冊(cè)監(jiān)聽(tīng)者,可以立即檢測(cè)到哪些問(wèn)題?
一旦調(diào)用了 register(Object) 方法,EventBus 就會(huì)檢查監(jiān)聽(tīng)者中的處理方法是否結(jié)構(gòu)正確的[well-formedness]。具體來(lái)說(shuō),就是每個(gè)用@Subscribe 注解的方法都只能有一個(gè)參數(shù)。
違反這條規(guī)則將引起 IllegalArgumentException(這條規(guī)則檢測(cè)也可以用 APT 在編譯時(shí)完成,不過(guò)我們還在研究中)。
哪些問(wèn)題只能在之后事件傳播的運(yùn)行時(shí)才會(huì)被檢測(cè)到?
如果組件傳播了一個(gè)事件,但找不到相應(yīng)的處理方法,EventBus 可能會(huì)指出一個(gè)錯(cuò)誤(通常是指出@Subscribe 注解的缺失,或沒(méi)有加載監(jiān)聽(tīng)者組件)。
請(qǐng)注意這個(gè)指示并不一定表示應(yīng)用有問(wèn)題。一個(gè)應(yīng)用中可能有好多場(chǎng)景會(huì)故意忽略某個(gè)事件,尤其當(dāng)事件來(lái)源于不可控代碼時(shí)
你可以注冊(cè)一個(gè)處理方法專(zhuān)門(mén)處理 DeadEvent 類(lèi)型的事件。每當(dāng) EventBus 收到?jīng)]有對(duì)應(yīng)處理方法的事件,它都會(huì)將其轉(zhuǎn)化為 DeadEvent,并且傳遞給你注冊(cè)的 DeadEvent 處理方法——你可以選擇記錄或修復(fù)該事件。
怎么測(cè)試監(jiān)聽(tīng)者和它們的處理方法?
因?yàn)楸O(jiān)聽(tīng)者的處理方法都是普通方法,你可以簡(jiǎn)便地在測(cè)試代碼中模擬 EventBus 調(diào)用這些方法。
為什么我不能在 EventBus 上使用<泛型魔法>?
EventBus 旨在很好地處理一大類(lèi)用例。我們更喜歡針對(duì)大多數(shù)用例直擊要害,而不是在所有用例上都保持體面。
此外,泛型也讓 EventBus 的可擴(kuò)展性——讓它有益、高效地?cái)U(kuò)展,同時(shí)我們對(duì) EventBus 的增補(bǔ)不會(huì)和你們的擴(kuò)展相沖突——成為一個(gè)非常棘手的問(wèn)題。
如果你真的很想用泛型,EventBus 目前還不能提供,你可以提交一個(gè)問(wèn)題并且設(shè)計(jì)自己的替代方案。