Google-Guava-EventBus源码解读

Guava是Google开源的一个Java基础类库,它在Google内部被广泛使用。Guava提供了很多功能模块比如:集合、并发库、缓存等,EventBus是其中的一个module,本篇结合EventBus源码来谈谈它的设计与实现。

概要

首先,我们先来预览一下EventBus模块的全部类图:


类并不是多而且几乎没有太多继承关系。

下面,我们来看一下各个类的职责:

  • EventBus:核心类,代表了一个事件总线。Publish事件也由它发起。
  • AsyncEventBus:在分发事件的时候,将其压入一个全局队列的异步分发模式。
  • Subscriber:对某个事件的处理器抽象,封装了事件的订阅者以及处理器,并负责事件处理(该类的类名及其语义有些不明确,后续会谈到)。
  • SubscriberRegistry:订阅注册表,它用于存储Subscriber跟Event的对应关系,以便于EventBus在publish一个事件时,可以找到它对应的Subscriber。
  • Dispatcher:事件分发器,它定义了事件的分发策略。
  • @Subscribe:用于标识事件处理器的注解,当EventBus publish一个事件后,相应的Subscriber将会得到通知并执行事件处理器。
  • @AllowConcurrentEvents:该注解跟@Subscribe一同使用,标识该订阅者的处理方法为线程安全的,该注解还用于标识该方法将可能会被EventBus在多线程环境下执行。
  • DeadEvent:死信(没有订阅者关注的事件)对象。
  • SubscribeExceptionHandler:订阅者抛出异常的处理器。
  • SubscribeExceptionContext:订阅者抛出异常的上下文对象。
在对每个类进行分解之前,我们再来看一下各个类之间的关联关系:

分“类”解读

EventBus

它有这么几个字段:
  • identifier:事件总线的标识,这说明在一个应用里是可以有多个EventBus的。如果不指明它的值,它将以“default”作为其默认名称。
  • executor:它是Executor接口的实例,用于对订阅者处理事件方法的执行。这里需要注意的是,该字段的实例化是在EventBus内部构造器中,并不是从外部注入进来的,另外真正的执行订阅者方法的时机也不由EventBus负责,而是由Subscriber负责,因此该字段会被公开给外部访问。
  • exceptionHandler:它是SubscribeExceptionHandler的实例,用于处理订阅者在执行事件处理方法时抛出的异常。EventBus可以接收一个外部定义的异常处理器,也可以采用内部缺省的日志记录处理器。
  • subscribers:订阅者注册表,用于存储所有的事件以及事件处理器、订阅对象的对应关系。
  • dispatcher:事件分发器,用于分发事件给订阅对象的事件处理器,该对象在EventBus构造方法内部初始化,默认的实现是PerThreadQueuedDispatcher,该分发器将事件存入队列,并保证在同一个线程上发送的事件能够按照他们发布的顺序被分发给所有的订阅者。
EventBus提供了几个核心方法:
  • register:注册subscriber;
  • unregister:移除注册过的subscriber;
  • post:发布事件;

你可以将EventBus看做是一个代理,这些方法真正的实现者都是上面的这些对象。

AsyncEventBus

一个支持异步发布模式的EventBus,它覆盖了EventBus的默认构造方法,指定了一个异步的分发器:LegacyAsyncDispatcher,这个分发器基于一个全局的队列来暂存未发布的事件。

Subscriber

之前也提到Subscriber的名称是比较容易混淆的。这个类的名称看似表示一个订阅者对象,但其实是用来封装“一个订阅者的一个事件处理器”对象。因为当一个订阅者存在多个处理方法被标注为@Subscribe的时候,那么每个处理方法都对应于一个独立的Subscriber对象的实例。我个人觉得这个名称与其具体的实现语义有些混淆。当然也许实现者认为:一个对象以及一个事件处理器就是一个Subscriber的话,那是没有问题的。因此这里为了理解方便,你可以将其看做是一个封装了订阅者对象以及一个订阅者处理器方法的实体类。
Subscriber的访问级别是package的,它还承担了执行事件处理的责任。通过一个create静态工厂方法创建它:
static Subscriber create(EventBus bus, Object listener, Method method) {return isDeclaredThreadSafe(method)? new Subscriber(bus, listener, method): new SynchronizedSubscriber(bus, listener, method);}

它接收三个参数:
  • bus:EventBus的实例,通过它来获取事件的执行器(executor)
  • listener:真实的订阅者对象
  • method:订阅对象的事件处理方法的Method实例
在实现中,它会先判断该处理器方法上是否被标注有@AllowConcurrentEvents注解,如果有,则实例化Subscriber类的一个实例;如果没有,则不允许eventbus在多线程的环境中调用处理器方法,所以这里专门为此提供了一个同步的订阅者对象:SynchronizedSubscriber来保证线程安全。
该类的两个关键方法之一:
dispatchEvent:
final void dispatchEvent(final Object event) {executor.execute(new Runnable() {@Overridepublic void run() {try {invokeSubscriberMethod(event);} catch (InvocationTargetException e) {bus.handleSubscriberException(e.getCause(), context(event));}}});}

它调用一个多线程执行器来执行事件处理器方法。
另一个方法:invokeSubscriberMethod以反射的方式调用事件处理器方法。
另外,该类对Object的equals方法进行了override并标识为final。主要是为了避免同一个对象对某个事件进行重复订阅,在SubscriberRegistry中会有相应的判等操作。当然这里Subscriber也override并final了hashCode方法。这是最佳实践,不必多谈,如果不了解的可以去看看《Effective Java》。
该类还有个内部类,就是我们上面谈到的SynchronizedSubscriber,它继承了Subscriber,与Subscriber唯一的不同就是在invokeSubscriberMethod的执行上做了同步。

SubscriberRegistry

针对单个EventBus的订阅与事件的关系维护。在内部用来存储订阅者关系的对象是java并发包下的并发Map:ConcurrentMap,该map以Class对象为键,值的类型是CopyOnWriteArraySet<Subscriber>集合类型。
SubscriberRegistry直接依赖EventBus对象,所以在构造器中需要注入EventBus的实例。
SubscriberRegistry里有两个关键的实例方法:register/unregister。

register

接收订阅者对象作为参数并建立Event跟Subscriber的关联关系。
我们来看看它的实现:
void register(Object listener) {Multimap<Class<?>, Subscriber> listenerMethods = findAllSubscribers(listener);for (Map.Entry<Class<?>, Collection<Subscriber>> entry : listenerMethods.asMap().entrySet()) {Class<?> eventType = entry.getKey();Collection<Subscriber> eventMethodsInListener = entry.getValue();CopyOnWriteArraySet<Subscriber> eventSubscribers = subscribers.get(eventType);if (eventSubscribers == null) {CopyOnWriteArraySet<Subscriber> newSet = new CopyOnWriteArraySet<Subscriber>();eventSubscribers = MoreObjects.firstNonNull(subscribers.putIfAbsent(eventType, newSet), newSet);}eventSubscribers.addAll(eventMethodsInListener);}}

它首先获得一个Multimap实例(它是Google Guava集合框架提供的一个多值Map类型,也就是说一个key可以对应多个value),该Multimap用于存储事件类型对应的该订阅者内所有关于该事件的处理器方法集合,其key为事件的Class类型。这里在for循环的中通过asMap获取其map视图,即可将Multimap对应的多个值存储到一个Collection中。
也就是说这里for循环的每个entry,表示的是一个事件的Class实例对应的一组Subscriber的集合,即eventMethodsInListener。
然后根据该事件的Class对象从注册表中获取对应的存储Subscriber实例的集合,如果不存在则创建该集合,然后将该订阅者内所有的事件处理器方法都加入到注册表中去。

unregister

unregister的实现跟register有些类似,先查找该订阅者所有的事件类型与处理器的对应关系。然后,遍历所有的事件类型,移除针对当前订阅者的所有Subscriber实例。

findAllSubscribers

register/unregister方法都调用了findAllSubscribers方法,它有一些特别之处,这里需要单独拎出来提一下。
findAllSubscribers用于查找事件类型以及事件处理器的对应关系。查找注解需要涉及到反射,通过反射来获取标注在方法上的注解。因为Guava针对EventBus的注册采取的是“隐式契约”而非接口这种“显式契约”。而类与接口是存在继承关系的,所有很有可能某个订阅者其父类(或者父类实现的某个接口)也订阅了某个事件。因此这里的查找需要顺着继承链向上查找父类的方法是否也被注解标注,代码实现:
  private Multimap<Class<?>, Subscriber> findAllSubscribers(Object listener) {Multimap<Class<?>, Subscriber> methodsInListener = HashMultimap.create();Class<?> clazz = listener.getClass();for (Method method : getAnnotatedMethods(clazz)) {Class<?>[] parameterTypes = method.getParameterTypes();Class<?> eventType = parameterTypes[0];methodsInListener.put(eventType, Subscriber.create(bus, listener, method));}return methodsInListener;}

同样涉及这个问题的,还有根据事件类型获取Subscriber实例的方法:getSubscribers。

getSubscribers

  Iterator<Subscriber> getSubscribers(Object event) {ImmutableSet<Class<?>> eventTypes = flattenHierarchy(event.getClass());List<Iterator<Subscriber>> subscriberIterators =Lists.newArrayListWithCapacity(eventTypes.size());for (Class<?> eventType : eventTypes) {CopyOnWriteArraySet<Subscriber> eventSubscribers = subscribers.get(eventType);if (eventSubscribers != null) {// eager no-copy snapshotsubscriberIterators.add(eventSubscribers.iterator());}}return Iterators.concat(subscriberIterators.iterator());}

Dispatcher

dispatcher用于分发事件给Subscriber。它内部实现了多个分发器用于提供在不同场景下不同的事件顺序性。Dispatcher是一个抽象类,定义了一个核心抽象方法:

abstract void dispatch(Object event, Iterator<Subscriber> subscribers);

该方法用于将一个指定的事件分发给所有的订阅者。

另外在Dispatcher提供了三个不同的分发器实现:

PerThreadQueuedDispatcher

它比较常用,针对每个线程构建一个队列用于暂存事件对象。保证所有的事件都按照他们publish的顺序从单一的线程上发出。保证从单一线程上发出,没什么特别的地方,主要是在内部定义了一个队列,将其放在ThreadLocal中,用以跟特定的线程关联。

LegacyAsyncDispatcher

另一个异步分发器的实现:LegacyAsyncDispatcher,之前在介绍AsyncEventBus的时候提到,它就是用这种实现来分发事件。
它在内部通过一个ConcurrentLinkedQueue<EventWithSubscriber>的全局队列来存储事件。从关键方法:dispatch的实现来看,它跟PerThreadQueuedDispatcher的区别主要是两个循环上的差异(这里基于队列的缓存事件的方式,肯定会存在两个循环:循环取队列里的事件以及循环发送给Subscriber)。
PerThreadQueuedDispatcher:是两层嵌套循环,外层是遍历队列取事件,内存是遍历事件的订阅处理器。
LegacyAsyncDispatcher:是一前一后两个循环。前面一个是遍历事件订阅处理器,并构建一个事件实体对象存入队列。后一个循环是遍历该事件实体对象队列,取出事件实体对象中的事件进行分发。

ImmediateDispatcher

其实以上两个基于中间队列的分发实现都可以看做是异步模式,而ImmediateDispatcher则是同步模式:只要有事件发生就会立即分发并被立即得到处理。ImmediateDispatcher从感官上看类似于线性并顺序执行,而采用队列的方式有多线程汇聚到一个公共队列的由发散到聚合的模型。因此,ImmediateDispatcher的分发方式是一种深度优先的方式,而使用队列是一种广度优先的方式。

DeadEvent

它是一个实体对象,封装了没有订阅者的事件。DeadEvent由两个属性组成:
  • source:事件源(通常指发布事件的EventBus对象)
  • event:事件对象
DeadEvent对象的产生:当通过某个EventBus的实例发布一个事件的时候,没有找到事件订阅者并且它本身又不是一个DeadEvent的实例时,将由EventBus构建一个DeadEvent类的实例。

总结

Guava的EventBus源码还是比较简单、清晰的。从源码来看,它一番常用的Observer的设计方式,放弃采用统一的接口、统一的事件对象类型。转而采用基于注解扫描的绑定方式。
其实无论是强制实现统一的接口,还是基于注解的实现方式都是在构建一种关联关系(或者说满足某种契约)。很明显接口的方式是编译层面上强制的显式契约,而注解的方式则是运行时动态绑定的隐式契约关系。接口的方式是传统的方式,编译时确定观察者关系,清晰明了,但通常要求有一致的事件类型、方法签名。而基于注解实现的机制,刚好相反,编译时因为没有接口的语法层面上的依赖关系,显得不那么清晰,至少静态分析工具很难展示观察者关系,但无需一致的方法签名、事件参数,至于多个订阅者类之间的继承关系,可以继承接收事件的通知,可以看作既是其优点也是其缺点。


原文发布时间为:2015-06-01

本文作者:vinoYang

本文来自云栖社区合作伙伴CSDN博客,了解相关信息可以关注CSDN博客。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/392609.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python之numpy

numpy是一个多维的数组对象&#xff0c;类似python的列表&#xff0c;但是数组对象的每个元素之间由空格隔开。 一、数组的创建 1.通过numpy的array(参数)&#xff0c;参数可以是列表、元组、数组、生成器等 由arr2和arr3看出&#xff0c;对于多维数组来说&#xff0c;如果最里…

git 上传

转载于:https://www.cnblogs.com/benbentu/p/6543154.html

Liferay 部署war包时候的deployDirectory 细节分析

引入&#xff1a; 在上文中&#xff0c;我们从宏观上讲解了Liferay部署war包的动作是如何触发监听器并且完成部署过程的&#xff0c;但是其中最核心的一块deployDirectory我们没讲&#xff0c;它的作用是当有了临时目录并且已经把war包的内容展开到该目录之后&#xff0c;是如何…

使用brew安装软件

brew 又叫Homebrew&#xff0c;是Mac OSX上的软件包管理工具&#xff0c;能在Mac中方便的安装软件或者卸载软件&#xff0c; 只需要一个命令&#xff0c; 非常方便 brew类似ubuntu系统下的apt-get的功能 阅读目录 安装brew 使用brew安装软件 使用brew卸载软件 使用brew查询软…

mysql 绕过select报错_MySQL注射绕过技巧(三)

在测试一次注入的时候发现过滤了逗号 所以找到这个思路第一次遇到的时候是看key哥挖洞 遇到后就想记录下来正文过滤了逗号 利用join来逐步查询select*from(select 1)a join (select 2)b join (select 3)c;例如下图逐步查询user()user() basediruser() basedir version()也可以…

Citrix、Microsoft、VMware虚拟桌面之网页接口登录对比

软件环境 Citrix Xendesktop 5.6 Microsoft Windows Server 2008 R2 Hyper-v VMware View client 4.6 首先看citrix的&#xff0c;很早之前Citrix就推出了网页的虚拟桌面和应用程序&#xff0c;默认是单点登录获取桌面 下面是微软的&#xff0c;和citrix很类似&#xff0c; 据我…

recyclerview 加载fragment_恢复 RecyclerView 的滚动位置

您可能在开发过程中遇到过这种情况&#xff0c;在 Activity/Fragment 被重新创建后&#xff0c;RecyclerView 丢失了它之前保有的滚动位置信息。通常这种情况发生的原因是由于异步加载 Adapter 数据&#xff0c;且数据在 RecyclerView 需要进行布局的时候尚未加载完成&#xff…

4.6.2 软件测试的步骤

系统测试是可有可无的。因为系统测试是和环境结合在一起。系统测试应该是在系统设计或者是需求分析阶段的前一步来完成的。 单元测试它的测试计划是在详细设计阶段完成。所以说单元测试的计划是在详细设计阶段来完成的。 模块接口的测试它保证了测试模块的数据流可以正确地流入…

栈,递归

栈的基本操作注意&#xff1a;是从后往前连接的 1 #include <stdio.h>2 #include <Windows.h>3 typedef struct sStack4 {5 int num;6 struct sStack* pnext;7 }Stack;8 void push(Stack **pStack,int num);9 int pop(Stack **pStack); 10 BOOL isEmpty(St…

mysql集群多管理节点_项目进阶 之 集群环境搭建(三)多管理节点MySQL集群

多管理节点MySQL的配置很easy&#xff0c;仅须要改动之前的博文中提高的三种节点的三个地方。1)改动管理节点配置打开管理节点C:\mysql\bin下的config.ini文件&#xff0c;将当中ndb_mgmd的相关配置改动为例如以下内容&#xff1a;[ndb_mgmd]# Management process options:# Ho…

APK伪加密

一、伪加密技术原理 我们知道android apk本质上是zip格式的压缩包&#xff0c;我们将android应用程序的后缀.apk改为.zip就可以用解压软件轻松的将android应用程序解压缩。在日常生活或者工作中&#xff0c;我们通常为了保护我们自己的文件在进行压缩式都会进行加密处理。这样的…

乱花渐欲迷人眼-杜绝设计的视噪

视噪&#xff0c;又称视觉噪音。我们每天接受来自外界的大量信息&#xff0c;这些信息有将近70&#xff05;是通过视觉感知获得的。视噪会干扰我们对信息的判断&#xff0c;影响到产品的易用性和可用性&#xff0c;与用户体验的好坏息息相关。(克劳德香农图演示了噪音如何影响信…

超详细windows安装mongo数据库、注册为服务并添加环境变量

1.官网下载zip安装包 官网地址https://www.mongodb.com/download-center/community?jmpnav&#xff0c;现在windows系统一般都是64位的&#xff0c;选好版本、系统和包类型之后点击download&#xff0c;mongodb-win32-x86_64-2008plus-ssl-4.0.10.zip。 2.解压zip包&#xff0…

.netcore mysql_.netcore基于mysql的codefirst

.netcore基于mysql的codefirst此文仅是对于netcore基于mysql的简单的codefirst实现的简单记录。示例为客服系统消息模板的增删改查实现第一步、创建实体项目&#xff0c;并在其中建立对应的实体类&#xff0c;以及数据库访问类须引入Pomelo.EntityFrameworkCore.MySql和Microso…

android 涨潮动画加载_Android附带涨潮动画效果的曲线报表绘制

写在前面本文属于部分原创&#xff0c;实现安卓平台正弦曲线类报表绘制功能介绍&#xff0c;基于网络已有的曲线报表绘制类(LineGraphicView)自己添加了涨潮的渐变动画算法最终效果图废话少说&#xff0c;直接上源码一、自定义View LineGraphicView&#xff0c;本类注释不算多&…

Oracle Study之--Oracle等待事件(5)

Db file single write这个等待事件通常只发生在一种情况下&#xff0c;就是Oracle 更新数据文件头信息时&#xff08;比如发生Checkpoint&#xff09;。当这个等待事件很明显时&#xff0c;需要考虑是不是数据库中的数据文件数量太大&#xff0c;导致Oracle 需要花较长的时间来…

Java多线程-工具篇-BlockingQueue

Java多线程-工具篇-BlockingQueue 转载 http://www.cnblogs.com/jackyuj/archive/2010/11/24/1886553.html 这也是我们在多线程环境下&#xff0c;为什么需要BlockingQueue的原因。作为BlockingQueue的使用者&#xff0c;我们再也不需要关心什么时候需要阻塞线程&#xff0c;什…

怎么连接 mysql_怎样连接连接数据库

这个博客是为了说明怎么连接数据库第一步&#xff1a;肯定是要下载数据库&#xff0c;本人用的SqlServer2008&#xff0c;是从别人的U盘中拷来的。第二步&#xff1a;数据库的登录方式设置为混合登录&#xff0c;步骤如下&#xff1a;1.打开数据库这是数据库界面&#xff0c;要…

webstorm环境安装配置(less+autoprefixer)

node安装&#xff1a; 参考地址&#xff1a;http://www.runoob.com/nodejs/nodejs-install-setup.html 1.下载node安装包并完成安装 2.在开始菜单打开node 3.查看是否安装完成&#xff08;npm是node自带安装的&#xff09; 命令&#xff1a;node -v npm -v less安装&#xff1a…

如何解决ajax跨域问题(转)

由 于此前很少写前端的代码(哈哈&#xff0c;不合格的程序员啊)&#xff0c;最近项目中用到json作为系统间交互的手段&#xff0c;自然就伴随着众多ajax请求&#xff0c;随之而来的就是要解决 ajax的跨域问题。本篇将讲述一个小白从遇到跨域不知道是跨域问题&#xff0c;到知道…