java中的EventBus事件总线技术

概念

EventBus 中文一般称为“事件总线”。采用发布订阅的模式(有时描述为注册监听的模式),创建并维护一个【事件发送器】,通过 【注册器】将【订阅者】注册到【总线】中,维护【事件】和【订阅者】的订阅关系;应用程序运行中 ,通过【事件发送器】发布具体【事件载体】,然后使用【事件分发器】调用【订阅者】来执行具体的订阅者消费逻辑。

组织结构

eventbus主要有以下几部分组成:
在这里插入图片描述

  1. eventbus、asyncEventBus:事件发送器。

EventBus 本身确实扮演着事件总线的角色。在这一架构中,EventBus 是一个中心化的消息传输机制,允许不同的组件(无论是Android中的Activity、Fragment,Vue中的组件,还是Java应用中的各种对象)通过它来相互通信,而无需这些组件直接引用彼此。组件可以向EventBus发布事件,同时其他组件可以订阅这些事件。EventBus负责匹配和分发这些事件,确保发布的事件能到达所有注册了相应类型事件的订阅者那里。因此,EventBus既是事件的接收站(收集发布的事件),也是转发器(将事件传递给订阅者),实质上起到了事件总线的作用。
事件发送器,尤其是在使用EventBus这一框架的上下文中,它确实就是那个作为通信中枢的“总线”,负责事件的传递与管理

  1. event:事件承载单元,用于携带数据。
  2. SubscriberRegistry:订阅者注册器,将订阅者注册到总线上,即将有注解 Subscribe 的方法 和 event绑定起来。

一般的描述中,我们说的是将订阅者注册到事件总线上。这意味着订阅者并不直接注册到某个特定的事件上,而是向事件总线(EventBus)声明自己对某种或某些类型事件的兴趣。事件总线扮演着中介的角色,负责在事件发生时,根据事件类型将事件分发给所有对此事件感兴趣的(即已经注册的)订阅者。因此,表述得更准确一些,是订阅者通过事件总线注册其对特定事件类型的订阅

  1. Dispatcher:事件分发器,调用事件的订阅者来执行逻辑。
  2. Subscriber、SynchronizedSubscriber:订阅者,前者并发订阅,后者同步订阅。

核心类图

在这里插入图片描述

运行原理
在这里插入图片描述

数据结构

订阅关系用线程安全的 map 维护,所谓的【注册】流程,就是启动过程中,去维护这个map (对应源码中的 subscribers)
在这里插入图片描述

源码理解

Subscribe 注解

应用程序编码中,在订阅者类中,一般有如下的写法

public interface BaseSubscriber {


}

/**
* 业务类  ASubscriber
*/

@Component
public class ASubscriber implements BaseSubscriber{

    // 略

    @Subscribe
    public void subscribe(AEvent event){
       // 略
	}
}

以下是注解对应的源码
在这里插入图片描述

@Target(ElementType.METHOD)是Java注解的一个元注解,用于指定被标注的注解可以在哪些类型的Java元素上使用。在这个例子中,@Subscribe注解被定义为只能应用于方法上,因为它的@Target元注解值为ElementType.METHOD。

这意味着当你在代码中看到@Subscribe注解时,它应该被放置在一个方法的声明之前,表示这个方法是一个事件订阅方法。在事件驱动编程中,特别是使用诸如EventBus这样的库时,这样的方法会在对应的事件被发布到EventBus时被自动调用。简言之,通过@Target(ElementType.METHOD)的设定,@Subscribe注解限制了它只能用于标记那些作为事件处理器的方法,从而指导编译器或框架(如EventBus)正确地识别和处理这些方法。

默认单线程

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

MoreExecutors.directExecutor()实际上并不是一个典型的线程池实现,它并不提供线程池的功能。从源码角度理解,这个方法返回的是一个DirectExecutor实例,该实例不是一个真正的线程池,而是一个简单的执行器(Executor),它直接在调用它的线程中执行提交的任务,没有线程管理和重用的机制。

注册 register
业务编码实现

TestEventBus 是业务代码。

/**
* 1:Component 注解,目的是托管给spring,这样应用程序启动后,会实例化这个  TestEventBus 的一个实例
* 2:实现 InitializingBean, ApplicationContextAware 接口,重写 afterPropertiesSet 从而将应用中的订阅者注册到总线中(TestEventBus扮演了总线的角色,相当于简单包装了一层 eventBus)
* 3:是否线程安全
* 因为本身 Component 注解决定了 TestEventBus 是单例的,应用程序启动过成中  afterPropertiesSet 可以线程安全的执行
*/
@Component
public class TestEventBus implements InitializingBean, ApplicationContextAware {

    private EventBus eventBus;

    private ApplicationContext applicationContext;

    public void post(Object event) {
        if (eventBus != null) {
            eventBus.post(event);
        }
    }@Override
    public void afterPropertiesSet() {
    // applicationContext.getBeansOfType(Class<T> type) 方法在 Spring框架中用于查找并返回所有匹配指定类型(或其子类型)的已注册bean实例的集合。这意味着它返回的是类对应的实例对象的集合,而不是类对象(即类定义)本身。
        Map<String, BaseSubscriber> subscriberBeanMap = this.applicationContext.getBeansOfType(BaseSubscriber.class);
        List<BaseSubscriber> subscribers = new ArrayList<>(subscriberBeanMap.values());
		// eventBus = new EventBus() 这里是简单写个事件发送器
		// 如果追求高性能的话,可以灵活传入参数去控制线程池类型和异常处理器,以及线程名称这些,参考 EventBus 的几个构造方法
        eventBus = new EventBus();
        // subscriber 是订阅者的实例对象
        subscribers.forEach(subscriber -> eventBus.register(subscriber));
    }
    

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

在Spring框架中,ApplicationContext是一个核心接口,提供了多种访问和管理bean的能力,包括通过名称获取bean、通过类型获取bean集合等。当你调用getBeansOfType方法时,实际上是期望从Spring IoC容器中获取指定类型的全部bean实例。

关于“哪个ApplicationContext在生效”的问题,这取决于你的应用上下文是如何被初始化和配置的。Spring提供了多种ApplicationContext的实现,比如AnnotationConfigApplicationContext、ClassPathXmlApplicationContext、FileSystemXmlApplicationContext等,具体使用哪种实现取决于你的应用如何启动和配置Spring容器。

如果你在基于Java配置的Spring
Boot应用中,可能通过SpringApplication.run(YourMainClass.class,
args)自动创建了一个ApplicationContext,这时候往往是AnnotationConfigApplicationContext或者更特化的实现(如BootstrapApplicationContext)在背后工作。
如果你的应用使用XML配置,可能会显式创建如ClassPathXmlApplicationContext或FileSystemXmlApplicationContext来加载bean定义。
当你在代码中没有直接指定ApplicationContext的实现类,并且直接使用了getBeansOfType这样的方法,通常意味着这个方法的调用是在某个已经初始化好的ApplicationContext实例上下文中发生的。例如,你可能通过依赖注入的方式获得了对ApplicationContext的引用,或者在某些框架提供的回调方法中(比如实现了ApplicationContextAware接口的类)间接得到了它。

例如,如果你的类实现了ApplicationContextAware接口,Spring会自动将当前的ApplicationContext注入到你的类中,这样你就可以调用getBeansOfType方法了

在Spring框架中,当一个类被@Component标记并且实现了InitializingBean接口时,Spring容器会在以下时机执行该类的afterPropertiesSet方法:Bean的初始化阶段:在所有必需的属性(依赖注入)完成后,Spring容器会自动调用afterPropertiesSet方法。这意味着在该方法被调用之前,所有使用@Autowired或其他注解标记的依赖都已经注入完毕。

关于线程安全性,afterPropertiesSet方法的执行是由Spring容器管理的,它在单例Bean的情况下,通常是在容器初始化单例Bean的线程中执行的。由于Spring默认情况下管理的单例Bean在初始化后会被缓存,并且在后续请求中复用,因此对于单例Bean而言,afterPropertiesSet方法通常只会在容器启动时被执行一次,这时是线程安全的,因为容器初始化过程通常是同步进行的。

然而,需要注意的是,尽管afterPropertiesSet方法的调用时机本身是线程安全的,但该方法内部的操作仍然需要考虑线程安全。如果你在afterPropertiesSet方法中执行了修改共享状态的操作,你需要自行确保这些操作是线程安全的,尤其是在涉及到非final的实例字段或静态字段时。如果Bean的设计导致了状态的共享,你可能需要使用锁机制(如synchronized关键字或Lock接口)或其他并发控制工具来保护这些状态,以防止多线程并发访问导致的问题。

总之,afterPropertiesSet的调用时机由Spring容器保证线程安全,但方法内的逻辑需要根据实际情况来确保线程安全。

findAllSubscribers 方法如下:根据入参数的订阅者类型,取出订阅者类中被声明为 Subscribe 的方法,

相关源码

SubscriberRegistry 是注册器源代码。

final class SubscriberRegistry {

  /**
   * All registered subscribers, indexed by event type.
   *
   * <p>The {@link CopyOnWriteArraySet} values make it easy and relatively lightweight to get an
   * immutable snapshot of all current subscribers to an event without any locking.
   */
  private final ConcurrentMap<Class<?>, CopyOnWriteArraySet<Subscriber>> subscribers =
      Maps.newConcurrentMap();

  /**
   * The event bus this registry belongs to.
   */
  @Weak private final EventBus bus;

  SubscriberRegistry(EventBus bus) {
    this.bus = checkNotNull(bus);
  }

  /**
   * Registers all subscriber methods on the given listener object.
   */
  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);
    }
  }


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

}

在这里插入图片描述

在这个代码片段中,findAllSubscribers方法的目的是为给定的监听器对象找到并收集所有订阅者(Subscriber)信息,这些订阅者关联到监听器类中特定事件的处理方法。这里的事件处理方法通常遵循一定的约定,比如使用注解(如@Subscribe)来标记它们,并且它们往往接受一个参数作为事件类型。

当遍历监听器类的所有注解方法时,通过method.getParameterTypes()获取到的是该方法参数类型的数组。假设按照常见的事件处理模式,这些方法通常只有一个参数,即事件本身。因此,数组parameterTypes的第一个元素parameterTypes[0]就代表了该事件的类型(eventType)。

简而言之,eventType =
parameterTypes[0]是因为假设每个被检查的方法都遵循单一事件参数的设计模式,这样可以直接取第一个参数类型作为事件类型。然后,该事件类型与创建的Subscriber对象一同存储在Multimap中,以便后续根据事件类型分发事件给相应的订阅者。

eventType 不类对象,是代表参数类型的 Class<?> 对象。在 Java 反射中,Class<?>
类型用于表示一个类或者接口。当你调用 method.getParameterTypes() 时,它返回一个 Class<?>[]
数组,其中每个元素都是一个 Class 实例,对应于该方法参数的类型。

所以,当代码中出现 Class<?> eventType = parameterTypes[0]; 时,这意味着 eventType
变量存储的是方法第一个参数的实际类型信息,比如 String.class、Integer.class 或自定义类 MyEvent.class
等等

在这里插入图片描述
186 行判断方法是否识别为 Subscribe
在这里插入图片描述

发布 post
业务编码实现

见 TestEventBus 类的post 方法

相关源码

在这里插入图片描述
214行,入参数传入具体的事件对象,
215行,通过最初启动后维护好的订阅关系,查到对应事件的订阅者集合。
217行,通过调用事件分发器的 dispatch 方法,执行具体的订阅者逻辑。其参数中第2个参数是具体的订阅者集合。
218行,如果此事件没有对应的订阅者,则视为死亡事件。
在这里插入图片描述
以下是分发器的3种实现类。具体使用哪种实现类,取决于创建 EventBus时的传入参数。默认是 PerThreadQueuedDispatcher
在这里插入图片描述
在这里插入图片描述
进入分发器的 dispatch 逻辑:
从数据结构上使用了 ThreadLocal,目的在于 在同一个EventBus中,发送器 PerThreadQueuedDispatcher 是一个成员变量。当并发调用 EventBus 的post方法时,对于
PerThreadQueuedDispatcher 来说,等价于并发调用 dispatch 方法。EventBus 类中对于 dispatch 是同步调用的,需要等待 dispatch 的阻塞时间。在同一个 EventBus 中,多次调用post 方法的时候,各个post方法之间是串行的。综上所述,为了提高并行度,PerThreadQueuedDispatcher 在执行 dispatch 方法时,采用了 ThreadLocal 的数据结构去维护n个线程请求对应的线程上线文信息,可以理解为提高了 PerThreadQueuedDispatcher#dispatch方法的并行度。从而提高上层post 方法的执行效率。
在这里插入图片描述
在这里插入图片描述

Queue 的使用,这里使用队列,目的在于提高单线程情况下频繁调用 dispatch 的执行效率。
每个线程对应一个队列。
当线程T1,循环调用 dispatch 方法时,101行首次加入队列尾部,会在首次调用时103行设置为 true,表示T1线程正在处理中。后续107行从头部取出元素处理。109行内部异步执行。
循环调用T1期间,101行正常入队列,102行是否进去需要依据最初114行是否执行完毕。因为在当前的实现中,dispatch 在一个线程内部是单线程执行的,所以即便是T1线程循环调用 dispatch,第1次的调用依然会执行完112行的代码段,第2次调用的102行依然会是 this.dispatching.get() == false.‘
从实际效果来看,还是单线程内部顺序执行的对 Queue 的处理。
在这里插入图片描述
109行进去后,如下,是线程池里提交执行的。
在这里插入图片描述
在这里插入图片描述

这段代码定义了一个 invokeSubscriberMethod 方法,它的主要职责是使用反射来调用某个预设的 method,并将 event 作为参数传递给该方法。这个方法带有 @VisibleForTesting
注解,表明它主要是为了测试目的而设计为可见的,可能在类的正常运行时逻辑中并不直接被外部调用。下面是该方法的关键点分析:

反射调用: 使用 method.invoke(target, checkNotNull(event))
来动态地调用目标对象(target)上的方法,并传入事件(event)作为参数。这里假设 method
是一个之前通过其他方式(如在订阅事件时)获得的 java.lang.reflect.Method 实例,而
checkNotNull(event) 确保了事件参数不为 null,以防止空指针异常。

异常转换和重新抛出:

IllegalArgumentException: 如果方法调用因参数不合法被拒绝(例如,类型不匹配),则捕获此异常并将其转换为
Error,附带详细的错误信息,表明是参数问题导致的。 IllegalAccessException:
如果在调用时失去了访问权限(这在正常的运行环境中应该很少发生,除非有安全管理器的介入),同样转换为 Error 并提供相关信息。
InvocationTargetException: 当被调用的方法内部抛出异常时被捕获。如果该异常是 Error
的实例,则直接重新抛出;否则,原样抛出 InvocationTargetException,保留原始的异常链,以便于调试和问题追踪。
测试友好: 由于标记了
@VisibleForTesting,说明这个方法的设计考虑到了单元测试的需求,允许测试代码直接调用它来验证事件处理逻辑是否正确,包括异常处理路径是否按预期工作。

总之,invokeSubscriberMethod
是事件处理机制的核心部分,它负责执行实际的事件处理逻辑,同时通过细致的异常处理确保了错误情况下的健壮性和可调试性,且对测试友好,便于进行单元测试和功能验证。

回顾注册阶段维护的订阅者信息,Subscriber.create(bus, listener, method) 方法参数中的 listener,即是订阅者类的实例对象。对应运行原理中的【A1Subscriber】的类的具体的一个实例对象。
在这里插入图片描述

相关设计模式

不是单例模式

在这里插入图片描述

在 JVM 中,Guava 的 EventBus 类本身并不保证是单例的。它作为一个普通对象,可以根据应用程序的需求创建多个实例。是否采用单例模式完全取决于开发者如何在应用程序中实例化和使用 EventBus。

如果您的应用程序需要在整个应用中共享一个 EventBus 实例来发布和订阅事件,那么您需要自行实施单例模式,正如之前代码示例所示。这样可以确保所有组件都使用的是同一个 EventBus 实例,从而达到事件通信和管理的一致性与高效性。

如果没有特别配置为单例,每次通过 new EventBus() 创建的就是一个新的实例,这样在不同的上下文中可以有各自独立的事件总线,适用于隔离关注点或者在微服务架构中每个服务有自己的事件处理逻辑的情况。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值