EventBus
EventBus
允许发布-订阅模式在组件之间通信,无需显性注册另外一个组件(从而相互意识到)。它专门设计使用显性注册的替换传统的Java进程内的事件分布。它并不是通用的发布订阅系统,也不是进程间通信的系统。
避免EnventBus
我们建议反对使用EventBus
。它在很多年以前设计,并且更新的类型提供更好的方式来将组件解耦和对事件作出响应。
要将组件解耦,我们推荐依赖注入框架。对于Android代码,大多数app使用Dagger。对于服务器代码,常用的选择包括Guice和Spring。框架通常提供一种独立注册多个监听器方法,并且作为集合一起请求他们(Dagger,Guice,Spring)。
要对事件作出响应,我们推荐响应式流框架,像RxJava(如果你对于Android进行构建,使用RxAndroid扩展补充)或者Project Reactor。(有关将代码从事件总线到使用响应式流框架的基础,请参阅这两个指南:1,2)。一些EventBus的使用使用Kotlin协同程序可能更好写,包括Flow和Channels。其他用法通过单独的类库提供了更好的服务,这些类库对特定的用例提供了特殊化的支持。
EventBus的缺点包括:
- 它会产生生产者和订阅者跨依赖难以发现。这使得调试复杂化,导致无意识地重入调用,并且在启动时,强制应用尽早初始化所有可能的订阅者。
- 当代码被R8和Proguard优化器或者最小化器处理时,它使用的反射会被打断。
- 它没有提供在采取行动之前提供等待多个时间的方法。例如,它没有提供等待一种等待多个生产者都报告他们“准备好了”的方法,也没有提供一种来自单个生产者的多个事件批量处理在一起的方法。
- 它不支持反压和其他弹性所需的功能。
- 它没有提供线程控制
- 它没有提供监控
- 它没有传播异常,所有应用程序没有一种方式来响应他们
- 它无法与RxJava,协程和其他更常用的替代方案进行交互。
- 它对他的订阅者生命周期强制要求。例如,如果一个事件发生在一个订阅者被移除和下一个订阅者被添加时之间,事件被删除。
- 他的性能未能达到最佳标准,特别是在Android下。
- 它不支持参数化类型
- 随着Java8的lambda的引进,
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);
}
一分钟指南
将已存在的基于EventListener
系统转换为使用EventBus
是容易的。
对于监听者
要监听事件的特定的特点(也就是说,CustomerChangeEvent
)
- 使用传统的Java事件:实现通过事件定义的接口 – 例如
CustomerChangeEventListener
。 - 使用
EventBus
:创建一个接收CustomerChangeEvent
方法作为他的独有参数,并且使用@Subscribe
注解标记它。
要使用事件生产者注册你的监听器方法。
- 使用传统的Java事件:将你的对象传入到每一个生产者的
registerCustomerChangeEventListener
方法。这些方法很少在公共接口中定义,所以除了知道每一个可能的生产者之外,你也必须知道他的类型。 - 使用
EventBus
:将你的对象传入到在EventBus
上的EventBus.register(Object)
方法。你需要确保你的对象与事件生产者共享EventBus
实例。
要监听通用的事件超级类型(例如EventObject
或者Object
)…
- 使用传统的Java事件:不容易。
- 使用
EventBus
:事件自动化分配到超级类型的监听器,允许接口类型的监听器或者Object
的“通配符监听器”。
监听和检测在没有监听器的情况下被分发的事件…
- 使用传统的Java事件:将代码添加到每一个分发事件方法(例如使用AOP)。
- 使用
EventBus
:订阅DeadEvent
。EventBus
将提醒你任何已发布 但未交付的事件(方便调试)。
对于生产者
要保留你的事件的监听器的踪迹…
- 使用传统的Java事件:编写代码来管理对象的监听器列表,包括同步,或者使用使用工具类,就像
EventListenerList
。 - 使用
EventBus
:EventBus
为你做了这些。
要分发一个事件到监听器…
- 使用传统的Java事件:写一个分发事件的方法到每一个事件监听器,包括错误隔离和异步性(如果需要的话)。
- 使用
EventBus
:将事件对象传入到EventBus
的EventBus.post(Object)
方法。
术语表
EventBus
系统和代码使用以下术语来讨论事件分布:
事件 | 任何可以被发布到总线(bus)的对象 |
---|---|
Subscribing | 使用EventBus 注册监听器的行为,以便它的处理器方法将响应事件 |
Listener | 希望响应事件的对象,通过处理器方法暴露 |
Handler 方法 | EventBus 应该用于投递发布事件的公共方法。Handler方法通过@Subscribe 注解进行标记 |
发布一个事件 | 通过EventBus 使事件对任何监听器有效 |
FAQ
为什么我必须创建自己的Event Bus,而不是使用单例?
EventBus
没有指定你如何使用它;没有任何东西阻止你的应用程序为每一个组件拥有单独隔离的EventBus
实例,或者使用单独隔离的实例根据上下文或者主题来隔离事件。这也使在你的测试中设置和删除EventBus
对象非常的简单。
当然,如果你想要有一个全面的EventBus
单例,没有任何东西能够阻止你以该方式这样做。简单地让你的容器(例如Guice)在全局作用域创建EventBus
为一个单例(或者如果你有兴趣的话,在静态字段存储它)。
总之,EventBus
不是单例,因为我们不想为你做这个决定。你喜欢怎么用就怎么用。
我可以从Event Bus注销一个监听器吗?
可以,使用EventBus.unregister
,但是只有极少被需要:
- 大多数监听器在启动或者延迟初始化注册,并应用程序生命周期内持久化。
- 特定的作用域
EventBus
实例可以处理临时的事件分布(例如,在请求作用域对象之间的分布式事件) - 对于测试,
EventBus
实例可以非常容易创建和丢弃,从而取消了对显性地注销的需要。
为什么使用注解来标记处理器方法,而不是要求监听器实现一个接口?
我们感觉Event Bus的@Subscribe
注解和实现一个接口一样地显性地传到出你的意图(或者更加明显),同时可以让你自由地将事件处理器方法放置你希望的任意位置并且给出他们显示意图的名称。
传统的Java事件使用监听器接口,通常指支持少量方法 – 通常只有一个。这有多个弊端:
- 任意一个类只能实现对给定事件的单个响应。
- 监听器接口方法可能有冲突
- 方法必须以事件命名,而不是使用其目的命名(例如,
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());
}
}
第二种情况的意图更加清晰:更少的干扰代码,并且事件处理器有一个清晰和有意义的名字。
通用的Handler<T>
接口怎么样?
一些人提出了一个通用的EventBus
监听器通用的Handler<T>
接口的建议。这在Java使用类型消除遇到了问题,更不用说可用性的问题了。
让我们讲一下看起来像下面的接口:
interface Handler<T> {
void handleEvent(T event);
}
由于消除,没有单独的类可以使用不同的类型参数多次实现一个泛型接口。这是传统Java事件的一大倒退,尽管actionPerformed
和keyPressed
不是非常有意义的名字,至少你可以实现这两个方法。
EventBus
不是破坏了静态类型和消除了对自动重构的支持?
一些人已经了解到EventBus
的register(Object)
和post(Object)
方法使用了Object
类型。
Object
在这里被使用的原因:Event Bus类库对事件监听器(正如在register(Object)
)或者事件本身(post(Object)
)的类型没有限制。
换句话说,事件处理器方法必须显性地声明他们的参数类型 – 期望的事件类型(或者一个他们的超类型)。这样,搜索对事件类的引用将立即找到所有该事件的处理器方法,并且重命名类型将影响所有在你的IDE视图内的处理器方法(或者创建事件的任何代码)。
的确,你可以随时重命名你的@Subscribed
事件处理器方法;Event Bus不会停止此操作或者执行任何操作来传播重命名,因为对于Event Bus你的处理器的名称是不相关的。当然,直接调用方法的测试代码,将会受到你的重命名的影响 – 但是,这就是重命名工具的作用。我们将这个视为一种特性,而不是bug:可以随意重命名你的处理器名称,让你使他们的含义更清晰。
如果我register
一个监听器没有任何处理器方法,会发生什么?
什么也不会。
Event Bus被设计为与容器和多模块系统集成,以Guice为原型。在这些情况下,让容器/工厂/环境将每一个创建的对象传入到EventBus
的register(Object)
方法是非常方便的。
这样,通过容器/工厂/环境创建的对象可以通过暴露处理器方法挂载到系统事件模型中。
在编译时可以检测Event Bus什么问题?
通过Java的类型系统任何问题可以被明明白白的检测出来。例如,声明一个不存在的事件类的处理器方法。
在注册时可以立即检测Event Bus什么问题?
在调用register(Object)
方法之后,将立即检查正在注册的监听器的处理方法的结构完整性。特别是,使用@Subscribe
标记的任何方法必须只有一个参数。
任何规则的违反将会抛出IllegalArgumentException
异常。(这个检查可以使用APT转移到编译时,这个是我们在研究的一个解决方案)
EventBus
什么问题只能在运行时检测?
如果没有注册的监听器的情况下,组件发出事件,它可能识别一个错误(通常你缺失一个@Subscribe
注解,或者监听器组件没有加载)。
注意:这不一定表明存在问题。在很多情况下,应用程序将故意忽略发出的事件,特别是事件来自你不受控制的代码。
要处理这样的事件,注册一个DeadEvent
类的处理方法。无论什么时候EventBus
接收一个没有注册处理器的事件,它将事件传入到DeadEvent
并使用你自己的方式转换它 – 允许你记录它或者其他方式恢复。
我如何测试事件监听器和他们的处理器方法?
因为你的监听器类的处理器方法是普通方法,你可以从你的测试代码调用他们来模拟EventBus
。
为什么我不可以使用EventBus做“神奇的事情”?
EventBus
被设计成能够很好的处理大量的用例。我们更喜欢在大多数用例中击中钉子,而不是所有用例中处理的很好。
除此之外,使EventBus
可扩展的–使他扩展时变得有用和高效,同时仍允许我们自己对核心EventBus
API进行添加,而不会与任何你的扩展相冲突–是一个相当困难的问题。
如果你确实,确实需要魔法X,EventBus
当前不能提供,你应该提交一个问题,然后设计你自己的替代方案。