java guava eventbus 怎么区分事件

让天下没有难学的技术
[Google Guava] 11-事件总线
[Google Guava] 11-事件总线
译者:沈义扬
传统上,Java的进程内事件分发都是通过发布者和订阅者之间的显式注册实现的。设计就是为了取代这种显示注册方式,使组件间有了更好的解耦。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);
一分钟指南
把已有的进程内事件分发系统迁移到EventBus非常简单。
事件监听者[Listeners]
监听特定事件(如,CustomerChangeEvent):
传统实现:定义相应的事件监听者类,如CustomerChangeEventListener;
EventBus实现:以CustomerChangeEvent为唯一参数创建方法,并用注解标记。
把事件监听者注册到事件生产者:
传统实现:调用事件生产者的registerCustomerChangeEventListener方法;这些方法很少定义在公共接口中,因此开发者必须知道所有事件生产者的类型,才能正确地注册监听者;
EventBus实现:在EventBus实例上调用方法;请保证事件生产者和监听者共享相同的EventBus实例。
按事件超类监听(如,EventObject甚至Object):
传统实现:很困难,需要开发者自己去实现匹配逻辑;
EventBus实现:EventBus自动把事件分发给事件超类的监听者,并且允许监听者声明监听接口类型和泛型的通配符类型(wildcard,如 ? super XXX)。
检测没有监听者的事件:
传统实现:在每个事件分发方法中添加逻辑代码(也可能适用AOP);
EventBus实现:监听;EventBus会把所有发布后没有监听者处理的事件包装为DeadEvent(对调试很便利)。
事件生产者[Producers]
管理和追踪监听者:
传统实现:用列表管理监听者,还要考虑线程同步;或者使用工具类,如EventListenerList;
EventBus实现:EventBus内部已经实现了监听者管理。
向监听者分发事件:
传统实现:开发者自己写代码,包括事件类型匹配、异常处理、异步分发;
EventBus实现:把事件传递给 方法。异步分发可以直接用EventBus的子类。
事件总线系统使用以下术语描述事件分发:
可以向事件总线发布的对象
向事件总线注册监听者以接受事件的行为
提供一个处理方法,希望接受和处理事件的对象
监听者提供的公共方法,事件总线使用该方法向监听者发送事件;该方法应该用Subscribe注解
通过事件总线向所有匹配的监听者提供事件
常见问题解答[FAQ]
为什么一定要创建EventBus实例,而不是使用单例模式?
EventBus不想给定开发者怎么使用;你可以在应用程序中按照不同的组件、上下文或业务主题分别使用不同的事件总线。这样的话,在测试过程中开启和关闭某个部分的事件总线,也会变得更简单,影响范围更小。
当然,如果你想在进程范围内使用唯一的事件总线,你也可以自己这么做。比如在容器中声明EventBus为全局单例,或者用一个静态字段存放EventBus,如果你喜欢的话。
简而言之,EventBus不是单例模式,是因为我们不想为你做这个决定。你喜欢怎么用就怎么用吧。
可以从事件总线中注销监听者吗?
当然可以,使用EventBus.unregister(Object)方法,但我们发现这种需求很少:
大多数监听者都是在启动或者模块懒加载时注册的,并且在应用程序的整个生命周期都存在;
可以使用特定作用域的事件总线来处理临时事件,而不是注册/注销监听者;比如在请求作用域[request-scoped]的对象间分发消息,就可以同样适用请求作用域的事件总线;
销毁和重建事件总线的成本很低,有时候可以通过销毁和重建事件总线来更改分发规则。
为什么使用注解标记处理方法,而不是要求监听者实现接口?
我们觉得注解和实现接口一样传达了明确的语义,甚至可能更好。同时,使用注解也允许你把处理方法放到任何地方,和使用业务意图清晰的方法命名。
传统的Java实现中,监听者使用方法很少的接口——通常只有一个方法。这样做有一些缺点:
监听者类对给定事件类型,只能有单一处理逻辑;
监听者接口方法可能冲突;
方法命名只和事件相关(handleChangeEvent),不能表达意图(recordChangeInJournal);
事件通常有自己的接口,而没有按类型定义的公共父接口(如所有的UI事件接口)。
接口实现监听者的方式很难做到简洁,这甚至引出了一个模式,尤其是在Swing应用中,那就是用匿名类实现事件监听者的接口。比较以下两种实现:
class ChangeRecorder {
void setCustomer(Customer cust) {
cust.addChangeListener(new ChangeListener() {
public void customerChanged(ChangeEvent e) {
recordChange(e.getChange());
//这个监听者类通常由容器注册给事件总线
class EventBusChangeRecorder {
@Subscribe public void recordCustomerChange(ChangeEvent e) {
recordChange(e.getChange());
第二种实现的业务意图明显更加清晰:没有多余的代码,并且处理方法的名字是清晰和有意义的。
通用的监听者接口Handler&T&怎么样?
有些人已经建议过用泛型定义一个通用的监听者接口Handler&T&。这有点牵扯到Java类型擦除的问题,假设我们有如下这个接口:
interface Handler&T& {
void handleEvent(T event);
因为类型擦除,Java禁止一个类使用不同的类型参数多次实现同一个泛型接口(即不可能出现MultiHandler implements Handler&Type1&, Handler&Type2&)。这比起传统的Java事件机制也是巨大的退步,至少传统的Java Swing监听者接口使用了不同的方法把不同的事件区分开。
EventBus不是破坏了静态类型,排斥了自动重构支持吗?
有些人被EventBus的register(Object) 和post(Object)方法直接使用Object做参数吓坏了。
这里使用Object参数有一个很好的理由:EventBus对事件监听者类型和事件本身的类型都不作任何限制。
另一方面,处理方法必须要明确地声明参数类型——期望的事件类型(或事件的父类型)。因此,搜索一个事件的类型引用,可以马上找到针对该事件的处理方法,对事件类型的重命名也会在IDE中自动更新所有的处理方法。
在EventBus的架构下,你可以任意重命名@Subscribe注解的处理方法,并且这类重命名不会被传播(即不会引起其他类的修改),因为对EventBus来说,处理方法的名字是无关紧要的。如果测试代码中直接调用了处理方法,那么当然,重命名处理方法会引起测试代码的变动,但使用EventBus触发处理方法的代码就不会发生变更。我们认为这是EventBus的特性,而不是漏洞:能够任意重命名处理方法,可以让你的处理方法命名更清晰。
如果我注册了一个没有任何处理方法的监听者,会发生什么?
什么也不会发生。
EventBus旨在与容器和模块系统整合,Guice就是个典型的例子。在这种情况下,可以方便地让容器/工厂/运行环境传递任意创建好的对象给EventBus的register(Object)方法。
这样,任何容器/工厂/运行环境创建的对象都可以简便地通过暴露处理方法挂载到系统的事件模块。
编译时能检测到EventBus的哪些问题?
Java类型系统可以明白地检测到的任何问题。比如,为一个不存在的事件类型定义处理方法。
运行时往EventBus注册监听者,可以立即检测到哪些问题?
一旦调用了register(Object) 方法,EventBus就会检查监听者中的处理方法是否结构正确的[well-formedness]。具体来说,就是每个用@Subscribe注解的方法都只能有一个参数。
违反这条规则将引起IllegalArgumentException(这条规则检测也可以用APT在编译时完成,不过我们还在研究中)。
哪些问题只能在之后事件传播的运行时才会被检测到?
如果组件传播了一个事件,但找不到相应的处理方法,EventBus可能会指出一个错误(通常是指出@Subscribe注解的缺失,或没有加载监听者组件)。
请注意这个指示并不一定表示应用有问题。一个应用中可能有好多场景会故意忽略某个事件,尤其当事件来源于不可控代码时
你可以注册一个处理方法专门处理DeadEvent类型的事件。每当EventBus收到没有对应处理方法的事件,它都会将其转化为DeadEvent,并且传递给你注册的DeadEvent处理方法——你可以选择记录或修复该事件。
怎么测试监听者和它们的处理方法?
因为监听者的处理方法都是普通方法,你可以简便地在测试代码中模拟EventBus调用这些方法。
为什么我不能在EventBus上使用&泛型魔法&?
EventBus旨在很好地处理一大类用例。我们更喜欢针对大多数用例直击要害,而不是在所有用例上都保持体面。
此外,泛型也让EventBus的可扩展性——让它有益、高效地扩展,同时我们对EventBus的增补不会和你们的扩展相冲突——成为一个非常棘手的问题。
如果你真的很想用泛型,EventBus目前还不能提供,你可以提交一个问题并且设计自己的替代方案。
原创文章,转载请注明: 转载自本文链接地址:
本站的翻译主编。关注并发编程,面向对象设计,分布式系统。
Latest posts by 沈义扬 ()
Related posts:
(8 votes, average: 4.38 out of 5)
Loading...Guava - EventBus(事件总线)
Guava在guava-libraries中为我们提供了事件总线EventBus库,它是事件发布订阅模式的实现,让我们能在领域驱动设计(DDD)中以事件的弱引用本质对我们的模块和领域边界很好的解耦设计。
不再多的废话,直奔Guava EventBus主题。首先Guava为我们提供了同步事件EventBus和异步实现AsyncEventBus两个事件总线,他们都不是单例的,官方理由是并不想我们我们的使用方式。当然如果我们想其为单例,我们可以很容易封装它,一个单例模式保证只创建一个实例就对了。
下面将以EventBus为例,AsyncEventBus使用方式与其一致的。
首先EventBus为我们提供了register方法来订阅事件,Guava在这里的实现很友好,我们不需要实现任何的额外接口或者base类,只需要在订阅方法上标注上@Subscribe和保证只有一个输入参数的方法就可以搞定。这样对于简单的某些事件,我们甚至可以直接
new Object() {
& & @Subscribe
& & public void lister(Integer integer) {
& & & & System.out.printf(&%d from int%n&, integer);
Guava发布的事件默认不会处理线程安全的,但我们可以标注@AllowConcurrentEvents来保证其线程安全
对于事件源,则可以通过post方法发布事件。 正在这里对于Guava对于事件的发布,是依据上例中订阅方法的方法参数类型决定的,换而言之就是post传入的类型和其基类类型可以收到此事件。例如下例:
final EventBus eventBus = new EventBus();
eventBus.register(new Object() {
& & @Subscribe
& & public void lister(Integer integer) {
& & & & System.out.printf(&%s from int%n&, integer);
& & @Subscribe
& & public void lister(Number integer) {
& & & & System.out.printf(&%s from Number%n&, integer);
& & @Subscribe
& & public void lister(Long integer) {
& & & & System.out.printf(&%s from long%n&, integer);
eventBus.post(1);
eventBus.post(1L);
在这里有 Integer,Long,与它们基类Number。我们发送一个整数数据的时候,或者Integer和Number的方法接收,而Long类型则Long类型和Number类型接受。
所以博主建议对于每类时间封装一个特定的事件类型是必要的。
DeadEvent暂时不清楚怎么翻译更合意,它描述的是死亡事件,即没有没任何订阅者关心,没有被处理,以DeadEvent类型参数的方法表示.例如在上例中我们post一个Object类型,如下:
final EventBus eventBus = new EventBus();
eventBus.register(new Object() {
& & @Subscribe
& & public void lister(DeadEvent event) {
& & & & System.out.printf(&%s=%s from dead events%n&, event.getSource().getClass(), event.getEvent());
eventBus.post(new Object());> 博客详情
在软件开发过程中,对象信息的分享以及相互直接的协作是必须的,困难在于确保对象之间的沟通是有效完成的,而不是拥有成本高度耦合的组件。当对象对其他组件的责任有太多的细节时,它被认为是高度耦合的。当一个应用程序有高度的耦合,维护将变得非常具有挑战,任何变化都将带来涟漪效应。为了解决这一类的软件设计问题,我们就需要基于事件的编程。本篇,我们就来学习Guava 基于事件的编程,Guava EventBus(一)EventBus。
& &在软件开发过程中,对象信息的分享以及相互直接的协作是必须的,困难在于确保对象之间的沟通是有效完成的,而不是拥有成本高度耦合的组件。当对象对其他组件的责任有太多的细节时,它被认为是高度耦合的。当一个应用程序有高度的耦合,维护将变得非常具有挑战,任何变化都将带来涟漪效应。为了解决这一类的软件设计问题,我们就需要基于事件的编程。本篇,我们就来学习Guava 基于事件的编程,Guava EventBus(一)EventBus。
& & 在基于事件的编程中,对象可以订阅/监听特定事件,或发布事件。在Java中,我们已经对事件的监听有了初步的认识,一个事件侦听器是一个对象,其目的是当特定事件发生时得到通知。中,我们已经提到了,本篇起,我们将要学习Guava EventBus类,以及它如何作用于发布和订阅的事件。EventBus类将能够提高相互的协作水平,而且几乎没有对象之间的耦合。值得注意的是,EventBus是一个轻量级、进程内发布/订阅的沟通风格,并不用于进程间通信。
& & 本系列的学习中,我们将学习和讨论以下内容:
EventBus 以及 AsyncEventBus类
使用EventBus注册订阅事件和事件通知
使用EventBus发布订阅
根据我们的需要,编写事件处理程序,选择粗粒度或细粒度的事件处理机制
结合EventBus,使用依赖注入框架
& & EventBus
& & EventBus类位与mon.eventbus包下,它是Guava基于事件编程,发布/订阅编程范式的重点和基础,在一个非常高的层面,用户将注册EventBus特定事件的通知,发布者将发送事件通过EventBus分发给感兴趣的用户。连续的通知所有用户,更重要的是在任何的代码中都可以很迅速的执行事件处理方法。
& &&创建 EventBus 实例
& & 通过调用EventBus构造函数,我们就可以创建一个EventBus实例:
& & & & EventBus eventBus = new EventBus();
& & 我们还可以提供一个可选的字符串参数作为标识符来创建一个EventBus(用于日志记录):
& & & & EventBus eventBus1 = new EventBus(TradeAccountEvent.class.getName()) ;
& &&订阅事件
& & 通过以下三个必须的步骤,可以从EventBus接收通知对象:
对象需要定义一个公共方法,只接受一个参数,这个参数标识事件类型的对象有兴趣接收通知。
事件通知暴露的方法必须使用@Subscribe注解。
最后,一个对象注册的EventBus实例,register注册方法,本身作为一个参数传递给EventBus。
& &&发布事件
& & 想要发布事件,我们需要向EventBus.post方法传递一个event对象,EventBus将调用订阅者注册处理程序方法,分配带有事件对象类型的参数。这是非常强大的设计概念,包含了接口、超类、实现超类的接口等,这意味着我们可以很容易的,使我们的事件处理程序,变成我们想要的细粒度,只需要通过改变类型接受的事件处理方法。
& &&定义处理方法
& & 用于事件处理程序的方法,必须只接受一个事件对象参数,如前所述,EventBus将连续调用事件处理方法,所以保证这些方法快速完成显得尤为重要。如果需要做任何扩展处理的接收事件,最好是在一个单独的线程运行该代码。
& &&并发性
& & EventBus不会从多个线程调用处理程序方法,除非使用@AllowConcurrentEvent注释处理程序方法。通过@AllowConcurrentEvent注解注释处理方法,我们断言处理程序方法是线程安全的。使用@AllowConcurrentEvent注释的处理程序方法,本身不会在EventBus中注册。
& & 至此,我们已经简单的学习了如何使用EventBus类,下一篇起,我们让通过一些示例进行学习,欢迎持续关注。
人打赏支持
开源项目作者
领取时间:
作为一个开源项目作者,是时候站出来拯救世界了!
领取条件:开源项目被开源中国收录的开发者可领取
码字总数 144659
支付宝支付
微信扫码支付
打赏金额: ¥
已支付成功
打赏金额: ¥不一样的事件总线 - NextEvents - 博客频道 - CSDN.NET
一枚代码手艺人 - /yoojia
分类:开源项目
与Guava.EventBus/Otto相同,但又大不同的事件总线。
Why NextEvents
总会有人说Guava.EventBus和Otto,还有Greenbot.EventBus已经非常优秀,你再造轮子有什么鬼用?
因为他们是圆轮子,我想要方的。他们没有,我就自己造啦!
Greenbot.EventBus、Guava.EventBus和Otto类似,对@Subscribe注解的方法进行回调,并根据方法参数类型来过滤。NextEvents 基于它们的设计思想,并且在回调调度粒度、过滤方式等做了改进,适应项目的需要。
在此前Android项目实践中使用到Otto,它非常好地解耦一些模块之间的弱关联。但过程中,我发现Otto未能解决遇到的问题:
我需要在@Subscribe方法中自行决定回调内部的执行线程,而不是由调用者或总线本身决定。可以在@Subscriber注解中声明调度方式是最好的。我在其它相似的库中也看到同样的实现,看来大家的思想都是一致的。
项目中多个不同逻辑的回调方法,其参数是相同的。Otto基于类型过滤,使得我只能在回调之后再过滤一次。我需要在定义方法时就决定它只接受哪一类型的事件。使用消息名来标识和过滤,与传统Web后端的消息总线相同。
触发执行一个方法,前提是多个任务的处理结果。(这是未来版本多事件触发的特性,现在还没实现)
已支持特性
支持使用EventHandler接口回调;
支持使用@Subscribe注解方法回调;
支持多种回调方式: CallerThread / Threads / MainThread(On Android only)
支持自定义回调目标的调度处理 Schedule
支持提交事件组
未来支持特性
多事件触发
自定义事件匹配处理
在注解模式下,即@Subscribe注解的回调方法的匹配规则如下:
回调方法定义的事件名必须与发送事件名相同;
回调方法无参数时,符合条件1即触发回调;
回调方法有参数时,参数数量与发送的事件(组)数量必须相同,顺序可以随意;
回调方法的参数中,在有且仅有一个参数是,可使用Object来接收任意类型事件;
@Subscribe(on = "test-event")
void handleWithArgEvents(String evt) {
@Subscribe(on = "text-event", run = Runs.ON_THREADS)
void handleNoArgEvent( ) {
@Subscribe(on = "text-event", run = Runs.ON_MAIN_THREAD)
void handleAnyTypeEvent(Object evt) {
nextEvents.emit("test-event", new String("this is an event payload"))
注意1:在回调方法的参数列表,参数类型强烈建议互不相同!相同类型的参数将按发送顺序依次填充!
注意2:在回调方法参数列表中,如果参数超过 1 个,则列表中的参数不允许使用Object类型
在发送事件时,将多个事件以对象组的方式提交,回调方法中定义对应类型的参数即可。参数顺序可以随意,但类型最好是互不相同。
如果相同类型的参数,回调方法中数值填充的顺序将与emit(…)的参数顺序一致。
事件组示例代码
@Subscribe(on = "users")
void handleEvent(String name, int age, float weight) {
nextEvents.emit("users", "yoojia", 18, 1024.0f)
Dead event
EventBus的DeadEvent设计实在在太好了!NextEvent直接借(cao)鉴(xi)这个设计思想。在早期版本中,我设计是使用Listener来处理DeadEvent,后来经过一系列实践,我发现这种修改实在太蠢了!我不量需要加入大量代码来处理,而且失去@Subscriber注解方法现有的灵活性和简洁性。而在@Subcriber注解方法中处理是多么顺其自然水到渠成的方式啊!
NextEvent的DeadEvent处理逻辑是这样的:如果发送事件没有回调目标方法或者Handler时,原事件将被包装成DeadEvent重新发送。可以通过订阅PayloadEvent.DEAD_EVENT事件名来处理DeadEvent。
DeadEvent事件与普通事件一样,唯一区别是它的事件名固定为PayloadEvent.DEAD_EVENT的值。
@Subscribe(on = PayloadEvent.DEAD_EVENT)
void onMissedTyped(String evt) {
好吧,测试基准用Guava.EventBus。
Dispatcher内核800W的TPS,在Events中390W,掉了一半,反射搞什么鬼。NextEvents设计目标是Android应用,这性能还可以应付。
NextEvents内核性能
v2.0 在 OS X 10.11.3' / 2.6 GHz Intel Core i5 / 8 GB / Java(TM) SE Runtime Environment (build 1.8.0_60-b27)环境下内核性能表现如下:
总发送时间
发送事件量
Dispatcher
与Guava.EventBus对比情况
跑分结果上看,NextEvents比Guava.EventBus高了一个分段。原因是Guava.EventBus内部大量使用了Guava库的工具类,而NextEvents则尽量精简。
Caller模式与Guava.EventBus是相同的,回调方法与调用者在同一线程上。在空负载(Nop Payload)对比测试中跑分能力优秀。
而MultiThreads和SharedThreads在有效负载时才有并发优势。
v2.0 在 OS X 10.11.3' / 2.6 GHz Intel Core i5 / 8 GB / Java(TM) SE Runtime Environment (build 1.8.0_60-b27)环境下的性能对比情况如下:
总发送时间
发送事件量
MultiThreads (1ms Payload)
SharedThreads (1ms Payload)
CallerThread (1ms Payload)
GuavaEvents (1ms Payload)
MultiThreads (Nop Payload)
SharedThreads (Nop Payload)
CallerThread (Nop Payload)
GuavaEvents (Nop Payload)
v2.0 在 Ubuntu 14.04 LTS / 3.6 GHz AMD A8-5600K / 8 GB / Java(TM) SE Runtime Environment (build 1.8.0_65-b17) 环境下的性能对比情况如下:
总发送时间
发送事件量
MultiThreads (1ms Payload)
SharedThreads (1ms Payload)
CallerThread (1ms Payload)
GuavaEvents (1ms Payload)
MultiThreads (Nop Payload)
SharedThreads (Nop Payload)
CallerThread (Nop Payload)
GuavaEvents (Nop Payload)
v2.0 在 Windows 10 64x / 3.2 GHz Intel i5-4460 / 8 GB / Java(TM) SE Runtime Environment (build 1.8.0_74-b02) 环境下的性能对比情况如下:
总发送时间
发送事件量
MultiThreads (1ms Payload)
SharedThreads (1ms Payload)
CallerThread (1ms Payload)
GuavaEvents (1ms Payload)
MultiThreads (Nop Payload)
SharedThreads (Nop Payload)
CallerThread (Nop Payload)
GuavaEvents (Nop Payload)
排名:千里之外
(3)(2)(1)

我要回帖

更多关于 guava eventbus maven 的文章

 

随机推荐