事件总线:组件之间的发布-订阅式通信,而无需组件之间进行显式注册。
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
当前不能提供,你可以提出问题,然后设计自己的替代方案。