一文搞定EventBus(含示例及源码解析)

684 阅读9分钟

前言

日常开发中经常遇到一个业务发生之后需要触发好几个业务点,比如订单支付完成之后需要发送短信、送会员积分、发送优惠券等。在分布式系统中我们可以通过消息队列实现,各个系统之间订阅支付成功事件,然后实现各自的业务,达到一个系统之间的解耦和异步的目的。

如果在同一个进程中也存在类似的通知需求,通过消息队列显得太笨重而且也没有跨进程或者系统架构中都没引入消息队列。这时候要实现进程内的消息通讯就可以通过Spring自带的Event事件或者google的EventBus。

最近入职了一家新公司,里面用到了EventBus作为消息总线。本人之前都是用Spring Event去做事件解耦,所以现在通过学习,总结本篇作为EventBus的基础知识,实例,以及源码解析。

简介

EventBus是适用于AndroidJava的开源库,使用发布者/订阅者模式进行松散耦合。EventBus使中央通信只需几行代码即可解耦类,从而简化了代码,消除了依赖关系并加快了应用程序的开发。(通俗理解,可以理解为同进程中的消息队列)

image.png EventBus事件三部曲:Subscriber、Event、Publisher。

  • Subscriber —— EventBus的register方法,会接收到一个Object对象。
  • Event —— EventBus的post()方法中传入的事件类型 (可以是任意类型)。
  • Publisher —— EventBus的post()方法。

ps:不好理解的可以看下述源码解析,源码不难,细心看完。

SpringEvent与EventBus的区别

主要区别

  1. 集成程度‌:Spring Event深度集成在Spring框架中,依赖于Spring的环境;而Guava EventBus是一个独立的库,不需要依赖Spring框架即可使用。
  2. 使用场景‌:Spring Event更适合于Spring应用内部的事件通知,可以很好地与Spring的其他特性如AOP、事务管理等结合使用;而Guava EventBus更适合于进程内的消息通讯,特别是在不需要Spring环境的情况下。
  3. 灵活性‌:Spring Event提供了更多的灵活性和配置选项,例如支持异步事件、多线程环境等;而Guava EventBus则更加轻量级和简单。

选型时就是因为SpringEvent强依赖Spring容器,而我们项目不使用Spring作为框架,所以使用EventBus。在使用过程中,我们需要手动注册Eventbus的事件,而SpringEvent可以自动管理。

观察者模式

SpringEvent和EventBus都是使用观察者模式 不管是SpringEvent还是EventBus都是对观察者模式的实现。与传统的观察者模式不同的是,它是观察者模式的非显示实现,说白了就是通过第三方将消息发布者与订阅者解耦。
传统的观察的模式需要在消息发布方维护一个订阅者的队列,耦合性是比较强的。而SpringEvent和EventBus是通过自身来管理发布者与订阅者的关系,发布者不再关心有多少订阅者,达到一个解耦的效果。

示例

import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
 
// 定义事件类
class SomeEvent {
    private String message;
 
    public SomeEvent(String message) {
        this.message = message;
    }
 
    public String getMessage() {
        return message;
    }
}
 
// 定义事件处理者
class SomeEventHandler {

//处理方法必须加上此注解,处理方法第一个参数必须是Event类
    @Subscribe 
    public void handleEvent(SomeEvent event) {
        System.out.println("Event received: " + event.getMessage());
    }
}
 
public class EventBusExample {
    public static void main(String[] args) {
        // 创建事件总线
        EventBus eventBus = new EventBus();
 
        // 注册事件处理者
        SomeEventHandler eventHandler = new SomeEventHandler();
        eventBus.register(eventHandler);
 
        // 发布事件
        eventBus.post(new SomeEvent("Hello, EventBus!"));
    }
}

上述代码是ChatGpt生成的简单demo,非常浅显易懂,大家也可以尝试一下。

源码解析

对于源码感兴趣的同学可以去github地址查看,直接在编译器中查看的话,有的地方反编译过来参数名称,以及集合遍历方式会变,不太方便学习。github地址如下:github.com/google/guav…

订阅注册 register

注册是EventBus中的register方法,其会调用具体实现类的SubscriberRegistry中的register方法,代码如下

  void register(Object listener) {
    
    // 找到当前订阅类的所有事件方法
    Multimap<Class<?>, Subscriber> listenerMethods = findAllSubscribers(listener);

    // 对订阅类进行遍历
    for (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<>();
        eventSubscribers =
            MoreObjects.firstNonNull(subscribers.putIfAbsent(eventType, newSet), newSet);
      }

      // 将事件的订阅方法加到此事件的订阅集合
      eventSubscribers.addAll(eventMethodsInListener);
    }
  }

上述代码中,我们可以看到注册的本质是,将我们的订阅类中的所有事件的回调方法放到一个集合中。当事件触发时,就会调用依次去执行里面的方法。

此代码块中比较核心的是找到注册类的方法,其代码如下

/**
 * Returns all subscribers for the given listener grouped by the type of event they subscribe to.
 */
// 将listener中的订阅的事件进行分类
private Multimap<Class<?>, Subscriber> findAllSubscribers(Object listener) {
  // 创建一个map
  Multimap<Class<?>, Subscriber> methodsInListener = HashMultimap.create();
  Class<?> clazz = listener.getClass();
  
  // 通过反射,遍历当前listener中带注解的方法
  for (Method method : getAnnotatedMethods(clazz)) {
    Class<?>[] parameterTypes = method.getParameterTypes();
    // 当前方法的参数列表第一个就是事件类型
    Class<?> eventType = parameterTypes[0];
    methodsInListener.put(eventType, Subscriber.create(bus, listener, method));
  }
  return methodsInListener;
}

取消订阅注册 unregister

此处也比较好理解,注册是将事件回调方法放到集合中,取消订阅就是将回调方法从订阅集合中移除。

  /** Unregisters all subscribers on the given listener object. */
  void unregister(Object listener) {
    
    // 先找到当前事件的所有订阅方法并进行分组
    Multimap<Class<?>, Subscriber> listenerMethods = findAllSubscribers(listener);

    for (Entry<Class<?>, Collection<Subscriber>> entry : listenerMethods.asMap().entrySet()) {
      Class<?> eventType = entry.getKey();
      Collection<Subscriber> listenerMethodsForType = entry.getValue();

      CopyOnWriteArraySet<Subscriber> currentSubscribers = subscribers.get(eventType);
      
      // 从订阅中移除
      if (currentSubscribers == null || !currentSubscribers.removeAll(listenerMethodsForType)) {
        throw new IllegalArgumentException(
            "missing event subscriber for an annotated method. Is " + listener + " registered?");
      }
    }
  }

执行订阅事件 post

下面是发布事件的具体执行过程,发布事件时,会找到所有订阅此事件的方法,并通过反射进行执行其方法。

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

在上述代码中有一个dispatcher作为分发器来执行回调方法,此类有三个内部类实现

image-20241023120844201.png

PerThreadQuquedDispatcher

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

private static final class PerThreadQueuedDispatcher extends Dispatcher {
        private final ThreadLocal<Queue<Event>> queue;
        private final ThreadLocal<Boolean> dispatching;

        private PerThreadQueuedDispatcher() {
            this.queue = new ThreadLocal<Queue<Event>>(this) {
                protected Queue<Event> initialValue() {
                    return Queues.newArrayDeque();
                }
            };
            this.dispatching = new ThreadLocal<Boolean>(this) {
                protected Boolean initialValue() {
                    return false;
                }
            };
        }

        void dispatch(Object event, Iterator<Subscriber> subscribers) {
            Preconditions.checkNotNull(event);
            Preconditions.checkNotNull(subscribers);
            Queue<Event> queueForThread = (Queue)Objects.requireNonNull((Queue)this.queue.get());
            queueForThread.offer(new Event(event, subscribers));
            if (!(Boolean)this.dispatching.get()) {
                this.dispatching.set(true);

                Event nextEvent;
                try {
                    while((nextEvent = (Event)queueForThread.poll()) != null) {
                        while(nextEvent.subscribers.hasNext()) {
                            ((Subscriber)nextEvent.subscribers.next()).dispatchEvent(nextEvent.event);
                        }
                    }
                } finally {
                    this.dispatching.remove();
                    this.queue.remove();
                }
            }

        }
        

可以看到其中定义的是ThreadLocl对象,即每个线程私有的

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

  • 线程安全的。EventBus 是一个总线,意味着它大概率是会被不同的线程投递事件的。PerThreadQueuedDispatcher 通过 ThreadLocal 将不同线程的数据隔离开,保证线程安全。

  • 这个转发器是基于广度优先转发的。想象一下,假如监听的事件处理中继续往总线中post事件,那就面对着深度优先和广度优先两种选择,这个实现是广度优先的,另外一个Dispatcher是深度优先的,等会解释。

    广度优先,意味着在转发过程中,新入的事件会被写到这个队列尾部,而不会立刻执行。

ImmediateDispatcher

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

private static final class ImmediateDispatcher extends Dispatcher {
        private static final ImmediateDispatcher INSTANCE = new ImmediateDispatcher();

        private ImmediateDispatcher() {
        }

        void dispatch(Object event, Iterator<Subscriber> subscribers) {
            Preconditions.checkNotNull(event);

            while(subscribers.hasNext()) {
                ((Subscriber)subscribers.next()).dispatchEvent(event);
            }

        }
    }

LegacyAsyncDispatcher

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

private static final class LegacyAsyncDispatcher extends Dispatcher {
        private final ConcurrentLinkedQueue<EventWithSubscriber> queue;

        private LegacyAsyncDispatcher() {
            this.queue = Queues.newConcurrentLinkedQueue();
        }

        void dispatch(Object event, Iterator<Subscriber> subscribers) {
            Preconditions.checkNotNull(event);

            while(subscribers.hasNext()) {
                this.queue.add(new EventWithSubscriber(event, (Subscriber)subscribers.next()));
            }

            EventWithSubscriber e;
            while((e = (EventWithSubscriber)this.queue.poll()) != null) {
                e.subscriber.dispatchEvent(e.event);
            }

        }

        private static final class EventWithSubscriber {
            private final Object event;
            private final Subscriber subscriber;

            private EventWithSubscriber(Object event, Subscriber subscriber) {
                this.event = event;
                this.subscriber = subscriber;
            }
        }
    }

源码总结

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

补充

如何解决同步执行下的事务问题

最近和同事讨论发现,在同步的情况,无法保证事务一致性(异步肯定无法保证事务一致性)。究其原因,是因为,订阅的事件在执行过程中,遇到异常会被捕获,导致异常无法抛出。直接上源码:

image.png

image.png

image.png

image.png

由上述代码可见,在同步执行过程中,事务无法保证一致性。因为在遇到异常的情况下,会打印日志而将异常吞掉。那该怎么解决呢? 其实,通过对于EventBus的构造方法可以看见,是可以传一个exceptionHandler作为参数的:

image.png 也就是说,我们只需要自定义一个异常处理器,继承SubscriberExceptionHandler这个类进行异常处理,将异常抛出即可

image.png 然后将此类作为构造参数,传进我们构造EventBus的地方。

尽量不要使用EventBus

我们项目中使用EventBus主要是为了进行解耦,但是在实际的对于一致性要求很强的功能中,使用EventBus绝对不是一个很好的方案。对于一致性要求很强的功能,还是建议使用保证消息不丢失的消息队列进行解耦。

包括谷歌官方都不建议使用EventBus,下面的说明大致说的是,EventBus很多年前就设计了,并且列举了一些缺点,感兴趣的可以查看EventBus的说明文档:github.com/google/guav…

image.png

原创不易,请勿转载。如有任何问题,或者描述不清的地方,望请评论区或者私信指教

参考

www.jianshu.com/p/f791e2208…

www.nhooo.com/note/qa3oyj…

juejin.cn/post/684490…

juejin.cn/post/684490…

juejin.cn/post/702821…