鍍金池/ 教程/ Java/ 事件總線
不可變集合
排序: Guava 強(qiáng)大的”流暢風(fēng)格比較器”
強(qiáng)大的集合工具類(lèi):java.util.Collections 中未包含的集合工具
新集合類(lèi)型
常見(jiàn) Object 方法
I/O
前置條件
字符串處理:分割,連接,填充
散列
原生類(lèi)型
數(shù)學(xué)運(yùn)算
使用和避免 null
Throwables:簡(jiǎn)化異常和錯(cuò)誤的傳播與檢查
google Guava 包的 ListenableFuture 解析
事件總線
緩存
函數(shù)式編程
區(qū)間
集合擴(kuò)展工具類(lèi)
Google-Guava Concurrent 包里的 Service 框架淺析
google Guava 包的 reflection 解析

事件總線

傳統(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)者[Listeners]

監(jiān)聽(tīng)特定事件(如,CustomerChangeEvent):

  • 傳統(tǒng)實(shí)現(xiàn):定義相應(yīng)的事件監(jiān)聽(tīng)者類(lèi),如 CustomerChangeEventListener;
  • EventBus 實(shí)現(xiàn):以 CustomerChangeEvent 為唯一參數(shù)創(chuàng)建方法,并用 Subscribe 注解標(biāo)記。

把事件監(jiān)聽(tīng)者注冊(cè)到事件生產(chǎn)者:

  • 傳統(tǒng)實(shí)現(xiàn):調(diào)用事件生產(chǎn)者的 registerCustomerChangeEventListener 方法;這些方法很少定義在公共接口中,因此開(kāi)發(fā)者必須知道所有事件生產(chǎn)者的類(lèi)型,才能正確地注冊(cè)監(jiān)聽(tīng)者;
  • EventBus 實(shí)現(xiàn):在 EventBus 實(shí)例上調(diào)用 EventBus.register(Object)方法;請(qǐng)保證事件生產(chǎn)者和監(jiān)聽(tīng)者共享相同的 EventBus 實(shí)例。

按事件超類(lèi)監(jiān)聽(tīng)(如,EventObject 甚至 Object):

  • 傳統(tǒng)實(shí)現(xiàn):很困難,需要開(kāi)發(fā)者自己去實(shí)現(xiàn)匹配邏輯;
  • EventBus 實(shí)現(xiàn):EventBus 自動(dòng)把事件分發(fā)給事件超類(lèi)的監(jiān)聽(tīng)者,并且允許監(jiān)聽(tīng)者聲明監(jiān)聽(tīng)接口類(lèi)型和泛型的通配符類(lèi)型(wildcard,如 ? super XXX)。

檢測(cè)沒(méi)有監(jiān)聽(tīng)者的事件:

  • 傳統(tǒng)實(shí)現(xiàn):在每個(gè)事件分發(fā)方法中添加邏輯代碼(也可能適用 AOP);
  • EventBus 實(shí)現(xiàn):監(jiān)聽(tīng) DeadEvent;EventBus 會(huì)把所有發(fā)布后沒(méi)有監(jiān)聽(tīng)者處理的事件包裝為 DeadEvent(對(duì)調(diào)試很便利)。

事件生產(chǎn)者[Producers]

管理和追蹤監(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í)現(xiàn):開(kāi)發(fā)者自己寫(xiě)代碼,包括事件類(lèi)型匹配、異常處理、異步分發(fā);
  • EventBus 實(shí)現(xiàn):把事件傳遞給 EventBus.post(Object)方法。異步分發(fā)可以直接用 EventBus 的子類(lèi) AsyncEventBus。

術(shù)語(yǔ)表

事件總線系統(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àn)問(wèn)題解答[FAQ]

為什么一定要?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)這種需求很少:

  • 大多數(shù)監(jiān)聽(tīng)者都是在啟動(dòng)或者模塊懶加載時(shí)注冊(cè)的,并且在應(yīng)用程序的整個(gè)生命周期都存在;
  • 可以使用特定作用域的事件總線來(lái)處理臨時(shí)事件,而不是注冊(cè)/注銷(xiāo)監(jiān)聽(tīng)者;比如在請(qǐng)求作用域[request-scoped]的對(duì)象間分發(fā)消息,就可以同樣適用請(qǐng)求作用域的事件總線;
  • 銷(xiāo)毀和重建事件總線的成本很低,有時(shí)候可以通過(guò)銷(xiāo)毀和重建事件總線來(lái)更改分發(fā)規(guī)則。

為什么使用注解標(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):

  • 監(jiān)聽(tīng)者類(lèi)對(duì)給定事件類(lèi)型,只能有單一處理邏輯;
  • 監(jiān)聽(tīng)者接口方法可能沖突;
  • 方法命名只和事件相關(guān)(handleChangeEvent),不能表達(dá)意圖(recordChangeInJournal);
  • 事件通常有自己的接口,而沒(méi)有按類(lèi)型定義的公共父接口(如所有的UI事件接口)。

接口實(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。這有點(diǎn)牽扯到 Java 類(lèi)型擦除的問(wèn)題,假設(shè)我們有如下這個(gè)接口:


    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, Handler)。這比起傳統(tǒng)的 Java 事件機(jī)制也是巨大的退步,至少傳統(tǒng)的 Java Swing 監(jiān)聽(tīng)者接口使用了不同的方法把不同的事件區(qū)分開(kāi)。

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ì)自己的替代方案。