最近看Elastic-Job源码,看到它里面实现的任务运行轨迹的持久化,使用的是Guava的AsyncEventBus,一个内存级别的异步事件总线服务,实现了简单的生产-消费者模式,从而在不影响任务执行效率的基础上,将任务执行和任务轨迹记录解耦,大大提高了EJ的性能。

EventBus在Elastic-Job中的使用

EventBus的使用方法不难,具体可以参考EJ里面几个相关的类:JobEventListener、JobEventBus和LiteJobFacade。主要的流程如下:

  • JobEventListener主要是消费者。定义需要监听的方法,目前主要定义了两个listen方法,注意想监听到的话,需要在方法前加上注解:@Subscribe和@AllowConcurrentEvents。看字面意思就是订阅和允许并发事件。如果不加上后面那个注解,则会导致效率问题,这个咱们后续分析。目前这个接口只有一个实现类JobEventRdbListener,实现了日志写入DB的操作。
  • JobEventBus参考的EventBus源码,提供了register和post方法,去掉了unregister方法。主要的功能就是注册监听器和生产消息。他的构造方法中,默认使用的是Guava的AsyncEventBus,初始化中同时包含了注册动作。
  • LiteJobFacade主要是JobEventBus的使用者。主要调用的是JobEventBus的post方法。
    @Override
public void postJobExecutionEvent(final JobExecutionEvent jobExecutionEvent) {
jobEventBus.post(jobExecutionEvent);
} @Override
public void postJobStatusTraceEvent(final String taskId, final State state, final String message) {
TaskContext taskContext = TaskContext.from(taskId);
jobEventBus.post(new JobStatusTraceEvent(taskContext.getMetaInfo().getJobName(), taskContext.getId(),
taskContext.getSlaveId(), Source.LITE_EXECUTOR, taskContext.getType(), taskContext.getMetaInfo().getShardingItems().toString(), state, message));
if (!Strings.isNullOrEmpty(message)) {
log.trace(message);
}
}

EventBus源码分析

言归正传,我们来看看EventBus到底是如何实现观察者模式的。他的主要实现类都在com.google.common.eventbus这个包下面。

主要类概念分析

我们首先来看一下里面比较重要的几个类,同时理解一些概念。

  • EventBus:这个类的作用有两个,一个是作为一个总线通道,另一个作用是消息的广播。
  • AsyncEventBus:异步的EventBus,功能与EventBus类似,只不过实现方式有所差异。
  • Subscriber:可以按照字面理解是订阅者,也可以说是监听器。
  • SubscriberRegistry:订阅注册表。主要存储的是Subcriber和Event之间的关系,用于消息分发时可以迅速根据Event的类型找到Subscriber。
  • Dispatcher:事件分发器,定义了一些分发的策略,里面包含三种分发器。
  • 两个重要的注解@Subscribe和@AllowConcurrentEvents。第一个是标识监听器的方法,第二个与第一个配合使用,标识允许多线程执行。
  • DeadEvent:死信对象,标识没有订阅者关注的事件。
  • SubscribeExceptionHandler:订阅者抛出异常的处理器。SubscribeExceptionContext:订阅者抛出异常的上下文对象。

EventBus

这个类有几个属性:

  private final String identifier;//唯一标识,默认为default
private final Executor executor;//多线程处理器,默认MoreExecutors.directExecutor()
private final SubscriberExceptionHandler exceptionHandler;//异常处理器 private final SubscriberRegistry subscribers = new SubscriberRegistry(this);//订阅注册表
private final Dispatcher dispatcher;//消息分发器,默认为Dispatcher.perThreadDispatchQueue(),单线程消息分发队列

其中,identifier表示,同一个应用中,可以根据identifier来区分不同的事件总线,只不过默认为default而已。

EventBus主要定义了几个方法:

注册

public void register(Object object) {
subscribers.register(object);
}

注册的是自己定义的监听器,也就是listener。

取消注册

public void unregister(Object object) {
subscribers.unregister(object);
}

类似于注册。

消息广播

public void post(Object event) {
Iterator<Subscriber> eventSubscribers = subscribers.getSubscribers(event);
if (eventSubscribers.hasNext()) {
dispatcher.dispatch(event, eventSubscribers);
} else if (!(event instanceof DeadEvent)) {
// the event had no subscribers and was not itself a DeadEvent
post(new DeadEvent(this, event));
}
}

这块主要是根据event事件类型,来获取事件的订阅者,然后进行事件消息的分发。当然,如果没有订阅者,也就是event的类型是DeadEvent,也会进行对应的处理。

AsyncEventBus

继承自EventBus,主要区别在于分发器,使用的是Dispatcher.legacyAsync()。这个后续咱们再分析。

Subscriber

乍看这个类,就是订阅者,其实我们看源码就能理解,当一个订阅类的多个方法用@Subscribe注解时,每个被注解的方法对应的是一个订阅者。

构造

这个类只是package内可见,没有定义为public,可以通过静态方法create来创建它。

static Subscriber create(EventBus bus, Object listener, Method method) {
return isDeclaredThreadSafe(method)
? new Subscriber(bus, listener, method)
: new SynchronizedSubscriber(bus, listener, method);
}

这里传入的method就是使用了@Subscribe注解的方法,这块会先判断这个方法是否线程安全,即是否使用@AllowConcurrentEvent来进行注解,来创建不同的Subscriber。唯一的差别是SynchronizedSubscriber中一个方法使用了synchronized来修饰。

dispatchEvent

  final void dispatchEvent(final Object event) {
executor.execute(
new Runnable() {
@Override
public void run() {
try {
invokeSubscriberMethod(event);
} catch (InvocationTargetException e) {
bus.handleSubscriberException(e.getCause(), context(event));
}
}
});
}

调用多线程来处理event。

invokeSubscriberMethod

@VisibleForTesting
void invokeSubscriberMethod(Object event) throws InvocationTargetException {
try {
method.invoke(target, checkNotNull(event));
} catch (IllegalArgumentException e) {
throw new Error("Method rejected target/argument: " + event, e);
} catch (IllegalAccessException e) {
throw new Error("Method became inaccessible: " + event, e);
} catch (InvocationTargetException e) {
if (e.getCause() instanceof Error) {
throw (Error) e.getCause();
}
throw e;
}
}

调用订阅者的方法。

SubscriberRegistry

我们之前在讲到EventBus时,里面有两个方法register和unregister,调用的就是这个类的方法。这个类的作用也讲到,是存储event和对应的订阅者的关系的。我们来看一下这个类的设计。

属性

private final ConcurrentMap<Class<?>, CopyOnWriteArraySet<Subscriber>> subscribers =
Maps.newConcurrentMap(); @Weak private final EventBus bus;

这个类有两个属性。

  • 第一个是ConcurrentMap,他的键是Class类,也就是Event的类型,值是CopyOnWriteArraySet,也就是订阅者。这个ConcurrentMap是Guava定义的并发Map,这个后续咱们有机会再分析。
  • 第二个属性就是EventBus。

register

注册监听器。

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);
}
}

主要的逻辑是:

  • 获取这个类中所有用@Subscribe注解的方法,存储到Multimap中。
  • 遍历Multimap,键为eventType,然后根据这个键,从缓存中获取这个事件对应的订阅者集合。
  • 获取到之后,判断集合是否为空,如果为空,新建一个集合来存储。

unregister

实现与register类似,先根据listener找到subscriber,找到需要监听的方法,然后根据事件类型去移除subscriber。

findAllSubscribers

获取监听器中所有的监听方法。

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;
}

findAllSubscribers用于查找事件类型以及事件处理器的对应关系。查找注解需要涉及到反射,通过反射来获取标注在方法上的注解。因为Guava针对EventBus的注册采取的是“隐式契约”而非接口这种“显式契约”。而类与接口是存在继承关系的,所有很有可能某个订阅者其父类(或者父类实现的某个接口)也订阅了某个事件。因此这里的查找需要顺着继承链向上查找父类的方法是否也被注解标注。

getSubscribes

获取event的订阅者。

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 snapshot
subscriberIterators.add(eventSubscribers.iterator());
}
} return Iterators.concat(subscriberIterators.iterator());
}

Dispatcher

分发器,用于将event分发给subscriber。它内部实现了三种不同类型的分发器,用于不同的情况下事件的顺序性。它的核心方法是:

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

它的三种实现:

PerThreadQueuedDispatcher

EventBus默认使用的分发器。它的实现是通过ThreadLocal来实现一个事件队列,每个线程包含一个这样的内部队列。

它的分发代码如下:

void dispatch(Object event, Iterator<Subscriber> subscribers) {
checkNotNull(event);
checkNotNull(subscribers);
Queue<Event> queueForThread = queue.get();
queueForThread.offer(new Event(event, subscribers)); if (!dispatching.get()) {
dispatching.set(true);
try {
Event nextEvent;
while ((nextEvent = queueForThread.poll()) != null) {
while (nextEvent.subscribers.hasNext()) {
nextEvent.subscribers.next().dispatchEvent(nextEvent.event);
}
}
} finally {
dispatching.remove();
queue.remove();
}
}
}

嵌套两层循环,第一层事件不为空,第二层该事件下的订阅者不为空,则分发事件下去。

LegacyAsyncDispatcher

AsyncEventBus使用的分发器。它在内部通过一个ConcurrentLinkedQueue的全局队列来存储事件。他和PerThreadQueuedDispatcher的主要区别在于分发循环这块。

void dispatch(Object event, Iterator<Subscriber> subscribers) {
checkNotNull(event);
while (subscribers.hasNext()) {
queue.add(new EventWithSubscriber(event, subscribers.next()));
} EventWithSubscriber e;
while ((e = queue.poll()) != null) {
e.subscriber.dispatchEvent(e.event);
}
}

是一前一后两个循环。前面一个是遍历事件订阅处理器,并构建一个事件实体对象存入队列。后一个循环是遍历该事件实体对象队列,取出事件实体对象中的事件进行分发。

ImmediateDispatcher

同步分发器。

void dispatch(Object event, Iterator<Subscriber> subscribers) {
checkNotNull(event);
while (subscribers.hasNext()) {
subscribers.next().dispatchEvent(event);
}
}

总结

Elastic-Job使用的EventBus,可以说很好的对任务的运行和轨迹记录进行了解耦,借鉴了Guava的思想,将代码优雅发挥到了新的境界。当然,Guava对EventBus的设计思想是我们需要进行学习和使用的。

Guava源码解析之EventBus的更多相关文章

  1. andorid jar/库源码解析之EventBus

    目录:andorid jar/库源码解析 EventBus: 作用: 用于不同Activity,Service等之间传递消息(数据). 栗子: A页面:onCreate定义   EventBus.ge ...

  2. [EventBus源码解析] 初探EventBus

    本期blog作为EventBus(以下简称EB)学习的始动篇,主要记载了EB的功能.优点.使用方法,内容基于github上的README.md与HOWTO.md. 何为EventBus EB实现了An ...

  3. Guava源码学习(五)EventBus

    基于版本:Guava 22.0 Wiki:EventBus 0. EventBus简介 提供了发布-订阅模型,可以方便的在EventBus上注册订阅者,发布者可以简单的将事件传递给EventBus,E ...

  4. EventBus源码解析 源码阅读记录

    EventBus源码阅读记录 repo地址: greenrobot/EventBus EventBus的构造 双重加锁的单例. static volatile EventBus defaultInst ...

  5. 【Android】EventBus 源码解析

    EventBus 源码解析 本文为 Android 开源项目实现原理解析 中 EventBus 部分项目地址:EventBus,分析的版本:ccc2771,Demo 地址:EventBus Demo分 ...

  6. EventBus (三) 源码解析 带你深入理解EventBus

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40920453,本文出自:[张鸿洋的博客] 上一篇带大家初步了解了EventBus ...

  7. Android EventBus源码解析 带你深入理解EventBus

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40920453,本文出自:[张鸿洋的博客] 上一篇带大家初步了解了EventBus ...

  8. Google guava cache源码解析1--构建缓存器(1)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 1.guava cache 当下最常用最简单的本地缓存 线程安全的本地缓存 类似于ConcurrentHas ...

  9. 源码解析-EventBus

    示例使用 时序图 源码解读 EventBus 使用 官网定义:EventBus 是一个使用 Java 写的观察者模式,解耦的 Android 开源库.EventBus 只需要几行代码即可解耦简化代码, ...

随机推荐

  1. 如何打开Tango的ADF文件?

    3ds max? opengl? ... Excel? vs? UltraEdit OpenGL Android API ADF文件数据结构:链接

  2. 自定义 Asp.Net SessionID 获取方式

    新建类 CustomSessionIDManager public class CustomSessionIDManager : SessionIDManager, ISessionIDManager ...

  3. 基于python 3.5 所做的找出来一个字符串中最长不重复子串算法

    功能:找出来一个字符串中最长不重复子串 def find_longest_no_repeat_substr(one_str): #定义一个列表用于存储非重复字符子串 res_list=[] #获得字符 ...

  4. C#Doc写入 XML文件

    HTML是XML的先驱,XML延续了HTML的简单性的优点.XML不是用来替代HTML的, XML和HTML为不同的目的而设计: XML被设计用来描述数据,其焦点是数据的内容.HTML被设计用来显示数 ...

  5. MD5签名

    /// <summary>        /// 校验签名        /// </summary>        /// <param name="mode ...

  6. docker-runc not installed on system 问题

    问题描述: docker运行镜像的时候,报错如下: 1 2 3 [root@etcd1 k8s]# docker run -it registry.helloworld.com/test/atsd:l ...

  7. Python 错误和异常小结

    1.Python异常类 Python是面向对象语言,所以程序抛出的异常也是类.常见的Python异常有以下几个,大家只要大致扫一眼,有个映像,等到编程的时候,相信大家肯定会不只一次跟他们照面(除非你不 ...

  8. ROS(机器人URDF模型优化)

    URDF模型 xacro优化后的URDF模型 1.精简模型代码(创建宏定义,文件包含) 2.提供可编程接口(常量,变量,数学计算,条件语句) 常量定义: name:base_length的值value ...

  9. java后台简单从腾讯云下载文件通知前端以附件的形式保存

    腾讯云对象存储和阿里云差不多 这是我的配置 /** * 腾讯云client * @return COSClient */ public static COSClient getCOSClient() ...

  10. python基础知识梳理----5dict 字典的应用

    内容简介: 1:字典简介 2:字典的增删该查 3:字典嵌套 1: 字典(dict)是python中唯一的一个映射类型.他是以{ }括起来的键值对组成. 在dict中key是唯一的. 在保存的时候, 根 ...