Guava 源码分析 | EventBus | 解耦代码的利器:开箱即用的发布-订阅实现

3,056 阅读12分钟

前言

Gauva 工具包是 Google 开放的一个开源工具包,就像一个瑞士军刀,小巧强大,有不少文章对其使用做个分析。平时在项目中对 Guava 的部分代码做过修改,阅读过代码,这里花一点时间,总结 Guava EventBus 的源码。文章主要包含以下内容:

  • EventBus 的使用方法
  • 简单的类图
  • 讲解一部分源码
  • EventBus 设计模式原则的分析
  • 感悟

预备知识

源码截图

EventBus 模块的源码不多,是比较容易读的。

使用

public class EventBusMain {

    @ToString
    @AllArgsConstructor
    static class MyEvent {
        public int value;
    }

    @ToString
    static class MyListener {
        public int value;

        @Subscribe
        public void listen(MyEvent e) {
            // 监听事件
            System.out.println("This is " + toString() + ", 收到事件:" + e.toString());
            value += e.value;
        }
    }

    public static void main(String[] args) {
        EventBus bus = new EventBus("myEventBus"); // 定义总线
        MyListener myListener = new MyListener();  
        bus.register(myListener);   // 注册监听者
        System.out.println("Before: " + myListener.toString());
        bus.post(new MyEvent(1));   // 向总线发布时间
        bus.post(new MyEvent(2));
        System.out.println("After: " + myListener.toString());
    }
}
输出:
Before: EventBusMain.MyListener(value=0)
This is EventBusMain.MyListener(value=0), 收到事件:EventBusMain.MyEvent(value=1)
This is EventBusMain.MyListener(value=1), 收到事件:EventBusMain.MyEvent(value=2)
After: EventBusMain.MyListener(value=3)

如代码所示,EventBus 是一个基于发布-订阅模式构建的一个工具,其 API 非常好用易用,主要分定义和调用两部分:

  • 定义事件,设计一个类MyEvent.java
  • 定义监听方法@Subscribe public void listen(MyEvent e)
  • 调用注册接口 bus.register(myListener);
  • 调用发布接口 bus.post(new MyEvent(1));

发布-订阅者模式是解耦类间调用的一种最常用的方案,EventBus 就是这种方案的实现利器。

顶层类图

一个总线类会包含以下四个模块

  • Executor 执行器,用的是 JDK 接口,Guava 自己在工具包中也定义了自己的一组执行器,默认使用直接执行器(MoreExecutors.directExecutor())
  • SubscriberExceptionHandler,异常处理器,用于处理在执行事件过程中,处理执行异常,默认只会打印异常记录,不会把异常传递到更高上层。
  • SubscriberRegistry,注册器,通过反射注册监听者,重点分析。
  • Dispatcher ,转发器,收到事件,通过注册器获得监听者并转发,重点分析。

源码分析

特点:在正式分析 EventBus 源码前,先整体说说作为一个利器工具的 EventBus 代码设计的特点。

  • 1、极其漂亮的封装。从源码截图中可以发现,所有的相关类都是处于同一个 package,其目的就是通过package权限把大部分的实现细节都封装起来,不用客户端知道,客户端仅仅需要使用 EventBus 类和其构造方法配置一下就可以了。
  • 2、单一职责特点。EventBus 这个类连上注释只有250行,核心代码100行不到,原因就是在设计过程中,把很多职责都分解开,分成四个模块,分别让不同的类自行实现。单一职责的好处还在于,每个职责本身也有自己的体系。
  • 3、在及其稳定的封装和细节屏蔽环境下,继承会比组合简单。EventBus 的特点就是细节屏蔽,在这个前提下,继承表示的is-a关系就表现出优势了。
  • 4、安全优先。工具不考虑用户是否会使用多线程,但会先保证工具的线程安全,这样,用户最多只是限制了工具的性能,而不会出现并发错误。

注册器(SubscriberRegistry)

注册器的代码只有300行,核心 API (这个API是用户不可见的,仅仅是给 其他模块调用)包括以下几个:

  • register(Object listener) 注册监听者
  • void unregister(Object listener) 撤销监听者(不详细写)
  • Iterator<Subscriber> getSubscribers(Object event) 根据事件获得所有的监听者(不详细写)

register(Object listener) // 向注册器注册监听者中所有的监听方法

  /**
   * Registers all subscriber methods on the given listener object.
   */
  void register(Object listener) {
    // 拆解 listener 类,返回 事件 -> List<监听者 (Object, Method)>
    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) {
        // 第一次创建这个事件,则构建一个 COW List,也就是这个方法是并发安全的。
        CopyOnWriteArraySet<Subscriber> newSet = new CopyOnWriteArraySet<Subscriber>();
        eventSubscribers =
            MoreObjects.firstNonNull(subscribers.putIfAbsent(eventType, newSet), newSet);
      }
      // 把拆解发现的监听者,注册到对应的 事件->List<监听者> 去
      eventSubscribers.addAll(eventMethodsInListener);
    }
  }
// 由Object 拆解发现所有监听者的细节
  /**
   * Returns all subscribers for the given listener grouped by the type of event they subscribe to.
   */
  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;
  }

  private static ImmutableList<Method> getAnnotatedMethods(Class<?> clazz) {
    // 使用了LoadingCache,重复拆解同一个类时,会直接返回拆解结果
    return subscriberMethodsCache.getUnchecked(clazz);
  }
  
  // 利用反射拆解的细节,获得某个类所有的方法,包括父类、接口等
  private static ImmutableList<Method> getAnnotatedMethodsNotCached(Class<?> clazz) {
    // Returns the set of interfaces and classes that this type is or is a subtype of. 获得这个类的所有父类及接口
    Set<? extends Class<?>> supertypes = TypeToken.of(clazz).getTypes().rawTypes();
    Map<MethodIdentifier, Method> identifiers = Maps.newHashMap();
    // 遍历每一个类
    for (Class<?> supertype : supertypes) {
      // 遍历每一个方法
      for (Method method : supertype.getDeclaredMethods()) {
        if (method.isAnnotationPresent(Subscribe.class) && !method.isSynthetic()) {
          // TODO(cgdecker): Should check for a generic parameter type and error out
          Class<?>[] parameterTypes = method.getParameterTypes();
          checkArgument(
              parameterTypes.length == 1,
              "Method %s has @Subscribe annotation but has %s parameters."
                  + "Subscriber methods must have exactly 1 parameter.",
              method,
              parameterTypes.length);

          MethodIdentifier ident = new MethodIdentifier(method);
          if (!identifiers.containsKey(ident)) {
            identifiers.put(ident, method);
          }
        }
      }
    }
    return ImmutableList.copyOf(identifiers.values());
  }

转发器(Dispatcher)

转发器 Dispatcher 是一个抽象类,其实现类有三个,细节会在下文说

  • PerThreadQueuedDispatcher
  • LegacyAsyncDispatcher
  • ImmediateDispatcher

转发器的核心方法

  /**
   * Dispatches the given {@code event} to the given {@code subscribers}.
   */
  abstract void dispatch(Object event, Iterator<Subscriber> subscribers);

PerThreadQueuedDispatcher

这是 EventBus 默认的转发器,可以翻译作:"每个线程单独设置一个队列"转发器。

private static final class PerThreadQueuedDispatcher extends Dispatcher {

    // This dispatcher matches the original dispatch behavior of EventBus.

    /**
     * Per-thread queue of events to dispatch.
     * 定义一个 ThreadLocal 线程私有对象,每次获取的时候都能够获得一个队列
     */
    private final ThreadLocal<Queue<Event>> queue =
        new ThreadLocal<Queue<Event>>() {
          @Override
          protected Queue<Event> initialValue() {
            return Queues.newArrayDeque();
          }
        };

    /**
     * Per-thread dispatch state, used to avoid reentrant event dispatching.
     * 线程私有对象,用于保存每个线程的转发状态,防止事件被重复转发
     */
    private final ThreadLocal<Boolean> dispatching =
        new ThreadLocal<Boolean>() {
          @Override
          protected Boolean initialValue() {
            return false;
          }
        };

    @Override
    void dispatch(Object event, Iterator<Subscriber> subscribers) {
      checkNotNull(event);
      checkNotNull(subscribers);
      // 获取线程私有的队列
      Queue<Event> queueForThread = queue.get();
      // 往队列写入需要被转发的 Event(事件本身+监听者们)
      queueForThread.offer(new Event(event, subscribers));

      if (!dispatching.get()) {
        dispatching.set(true);
        try {
          Event nextEvent;
          while ((nextEvent = queueForThread.poll()) != null) {
            // 使用迭代器 遍历执行队列中的事件
            while (nextEvent.subscribers.hasNext()) {
              // 监听者通过反射执行方法: Subscribers.java:line 70
              nextEvent.subscribers.next().dispatchEvent(nextEvent.event);
            }
          }
        } finally {
          // 主动释放 线程私有 对象!
          dispatching.remove();
          queue.remove();
        }
      }
    }

PerThreadQueuedDispatcher 转发器,具备以下两个特点:

  • 线程安全的。EventBus 是一个总线,意味着它大概率是会被不同的线程投递事件的。PerThreadQueuedDispatcher 通过 ThreadLocal 将不同线程的数据隔离开,保证线程安全。
  • 这个转发器是基于广度优先转发的。想象一下,假如监听的事件处理中继续往总线中post事件,那就面对着深度优先广度优先两种选择,这个实现是广度优先的,另外一个Dispatcher是深度优先的,等会解释。
    • 广度优先,意味着在转发过程中,新入的事件会被写到这个队列尾部,而不会立刻执行。

ImmediateDispatcher

与 PerThreadQueuedDispatcher 一样服务于本线程的转发器,其执行的是深度优先执行方法。

  /**
   * Implementation of {@link #immediate()}.
   */
  private static final class ImmediateDispatcher extends Dispatcher {
    private static final ImmediateDispatcher INSTANCE = new ImmediateDispatcher();

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

LegacyAsyncDispatcher

传统的异步分发器,这个是专门服务于多线程的。其内部会设置一个全局的队列,post 时间进去后,由多线程的 Excutor 执行器对其进行消费。使用异步就意味着,事件被监听都是无序的了,这也和我们常用的消息队列特性是一致的。

  /**
   * Implementation of a {@link #legacyAsync()} dispatcher.
   */
  private static final class LegacyAsyncDispatcher extends Dispatcher {
  
    /**
     * Global event queue.
     */
    private final ConcurrentLinkedQueue<EventWithSubscriber> queue =
        Queues.newConcurrentLinkedQueue();

    @Override
    void dispatch(Object event, Iterator<Subscriber> subscribers) {
      checkNotNull(event);
      while (subscribers.hasNext()) {
        // Event - 1:1 -> 监听者,作为一个事件写入队列
        queue.add(new EventWithSubscriber(event, subscribers.next()));
      }

      EventWithSubscriber e;
      while ((e = queue.poll()) != null) {
        // 把事件出队,写入执行器中。
        // 分开编写的目的
        e.subscriber.dispatchEvent(e.event);
      }
    }
  }

PS: 这里有个没明白的地方,为什么一定要先入队,然后再出队呢?是因为 e.subscriber 存在不一样的执行器么?

执行器(Executor)

执行器的接口是 JDK 的,意味着可以使用 JDK 以下的众多执行器去用在 EventBus,Guava 也实现了几个自己的执行器,写在 MoreExecutors.java 。这里只介绍最典型的直接执行器。

DirectExecutor

直接在提交任务的线程中执行任务,属于一种同步的执行方法,也是 EventBus 默认的执行方法。

  private enum DirectExecutor implements Executor {
    INSTANCE;   // 通过枚举,使用 加载器 保证代码的单例

    @Override
    public void execute(Runnable command) {
      command.run();
    }

    @Override
    public String toString() {
      return "MoreExecutors.directExecutor()";
    }
  }

异常处理器(SubscriberExceptionHandler)

这是处理 EventBus 在传播事件中的发生错误的处理方法。EventBus 默认对错误进行日志输出,然后屏蔽事件,防止一个监听者异常导致其他监听者失效。属于一种异常隔离的方法。

  /**
   * Simple logging handler for subscriber exceptions.
   */
  static final class LoggingHandler implements SubscriberExceptionHandler {
    static final LoggingHandler INSTANCE = new LoggingHandler();

    @Override
    public void handleException(Throwable exception, SubscriberExceptionContext context) {
      Logger logger = logger(context);
      if (logger.isLoggable(Level.SEVERE)) {
        logger.log(Level.SEVERE, message(context), exception);
      }
    }

    private static Logger logger(SubscriberExceptionContext context) {
      return Logger.getLogger(EventBus.class.getName() + "." + context.getEventBus().identifier());
    }

    private static String message(SubscriberExceptionContext context) {
      Method method = context.getSubscriberMethod();
      return "Exception thrown by subscriber method "
          + method.getName()
          + '('
          + method.getParameterTypes()[0].getName()
          + ')'
          + " on subscriber "
          + context.getSubscriber()
          + " when dispatching event: "
          + context.getEvent();
    }
  }

监听者(Subscriber)

一个 Object 注册到注册器(SubscriberRegistry),会拆分生成多个 监听者。一个Object假如注册了n个监听Method,则会产生多个Subscriber

以下是一个监听者的组成

监听者在默认的情况下是线程安全的,即使用其内部子类SynchronizedSubscriber,也就是每次都只能有一个线程执行该监听者,只有在方法上增加注解AllowConcurrentEvents,表示监听者方法是线程安全的。

        使用:
        @Subscribe
        @AllowConcurrentEvents  // 允许并发执行
        public void listen(MyEvent e) {
            System.out.println("This is " + toString() + ", 收到事件:" + e.toString());
            value += e.value;
            bus.post(new MyEvent(e.value + 1));
        }

实现:
  static final class SynchronizedSubscriber extends Subscriber {

    private SynchronizedSubscriber(EventBus bus, Object target, Method method) {
      super(bus, target, method);
    }

    @Override
    void invokeSubscriberMethod(Object event) throws InvocationTargetException {
      synchronized (this) {
        super.invokeSubscriberMethod(event);
      }
    }
  }

总结EventBus的工作过程

  • 1、定义并编写事件类 Event
  • 2、定义监听者类 Object,并以 Event 类为参数
  • 3、将 Object 对象 注册到 EventBus中。
    • 3.1 EventBus 首先会通过反射拆解 Object 的类型,并根据注解取出被@Subscribe修饰的监听方法,组成监听者(Subscriber),形成 Event -> Subscriber 的映射对
    • 3.2 把 3.1 生成的监听者保存在注册器中。
  • 4、EventBus 被提交时间 Event。
    • Bus 从注册器中取出 Event 映射的所有监听者
    • 通过事件+监听者,组成执行任务,提交给转发器
  • 5、转发器 根据自己定义的规则(深度优先、广度优先或多线程),把事件交付到监听者处。
  • 6、监听者通过反射执行监听方法。

EventBus的多线程模式

以上分析的都是同步版本的 EventBus,即Executor是马上执行的,即提交事件的线程会去执行所有的监听方法,执行完成才返回。

在 Guava 包中,还有一个异步的版本,提交事件后,主线程就返回了,由自行定义的 Executor 去执行监听方法。相比之下,我觉得同步版本会更难一点,因为发布-订阅模式有一种天生就容易被异步执行的感觉,故不重复记录。

设计模式思想对比

对比原则来自: 极客时间 设计模式之美 15讲-22讲 作者:王争

  • 1、符合单一职责的设计,把模式中的各个角色都分配到不同类中;
  • 2、符合扩展开放、修改关闭的设计,客户端是无法扩展和修改的,这里的对象主要是面向需要修改 EventBus 源代码的人员,其扩展开放、修改关闭表现在: EventBus 很多不同的模块(转发器、执行器)是通过扩展去实现的。
  • 3、符合里式替换,解释如2;
  • 4、符合依赖反转原则,即 高层 的 EventBus 即 4 个组件是可以基于抽象类和接口去调用的,真正的功能和策略其实是子类在实现。;
  • 5、符合KISS,API 非常简单;
  • 6、符合迪米特法则,最小知识原则,属于高内聚、低耦合的设计。从 EventBus 划分的 4 个组件来看,每个组件之间的调用其实是非常简单的,一般只有一两个接口;
  • 7、符合 DRY 功能非常确定,所以没什么重复代码;
  • 8、符合 接口隔离原则,客户端调用 EventBus 使用的API非常有限且独立,这也主要得益于 Bus 的功能非常明确;

8种设计思想的对比其实有不少重叠的地方,也反证了面向对象的设计,目的还是代码重用 + 高内聚、低耦合。

后记

这是一个 Guava EventBus 的源码分析文章,前面一直在用,则是第一次总结其源码的特点。基本上可以做到完全读懂执行过程,这里在记录一下自己对这段代码的印象。

  • 一个好的工具或框架,总是不会要求用户做太多事情。EventBus 的内部其实不算复杂,但提供的API是极其简洁的。
  • 好的工具自行封装屏蔽,限制用户对其修改。为了防止误导用户的使用,EventBus 只有 EventBus 类,@Subscribe注解,@AllowConcurrentEvents注解 和 SubscriberExceptionHandler 接口是开放给用户的,其他的组件一律通过 final包权限 限制用户访问、修改、继承。