EventBus轻量级发布订阅类库源码分析与实现

本文深入解析EventBus的工作原理,涵盖事件发布与订阅机制,以及在Sharding-JDBC和SpringCloud环境下的应用实践,探讨事件机制的三个关键部分:事件、事件监听器和事件源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

说明:EventBus是google-guava提供的消息发布-订阅类库,3个最核心的方法如下:

  1. 发布:即post(Object),发布事件到所有注册的订阅者,当事件被发布到所有订阅者后,这个方法就会返回成功,这个方法会忽略掉订阅者抛出的任何异常;

  2. 注册:即register(Object);注册对象中所有订阅者方法,这些方法都能收到事件。

  3. 解除注册:即unregister(Object);取消已注册对象中所有订阅者方法的注册;

sharding-jdbc使用EventBus发布&订阅时,对EventBus稍微封装了一下,即把EventBus设计为单例,然后通过EventBusInstance.getInstance()方法获取EventBus实例。EventBus单例核心源码如下:

 

public final class EventBusInstance {
    // 饿汉式实现单例模式 
    private static final EventBus INSTANCE = new EventBus();
    public static EventBus getInstance() {
        return INSTANCE;
    }
}

EventBus全类名为com.google.common.eventbus.EventBus

EventBus介绍

EventBus来自于google-guava包中。源码注释如下:

Dispatches events to listeners, 
and provides ways for listeners to register themselves.
The EventBus allows publish-subscribe-style communication between components 
without requiring the components to explicitly 
register with one another (and thus be 
aware of each other).  
It is designed exclusively to replace traditional Java in-process event distribution using explicit registration. 
It is not a general-purpose publish-subscribe
system, nor is it intended for interprocess communication.

翻译:将事件分派给监听器,并为监听器提供注册自己的方法。EventBus允许组件之间的发布 - 订阅式通信,而不需要组件彼此明确注册(并且因此彼此意识到)。 它专门用于使用显式注册替换传统的Java进程内事件分发。 它不是一个通用的发布 - 订阅系统,也不是用于进程间通信。

使用参考

关于EventBus的用例代码提取自sharding-jdbc源码,并结合lombok最大限度的简化;

  • DMLExecutionEvent

DMLExecutionEvent是发布&订阅事件模型,并且有个父类BaseExecutionEvent,申明如下:

@Getter
@Setter
public class BaseExecutionEvent {
    private String id;
}

@Getter
@Setter
public class DMLExecutionEvent extends BaseExecutionEvent{
    private String dataSource;
    private String sql;
}
  • DMLExecutionEventListener

事件监听器,订阅者接收到发布的事件后,进行业务处理:

public final class DMLExecutionEventListener {
    @Subscribe
    @AllowConcurrentEvents
    public void listener(final DMLExecutionEvent event) {
        System.out.println("监听的DML执行事件: " + JSON.toJSONString(event));
        // do something
    }
}
  • 发布&订阅

Main主方法中,注册订阅者监听事件,以及发布事件

public class Main {
    static{
        // 注册监听器
        EventBusInstance.getInstance().register(new DMLExecutionEventListener());
    }

    public static void main(String[] args) throws Exception {
        // 循环发布10个事件
        for (int i=0; i<10; i++) {
            pub();
            Thread.sleep(1000);
        }
    }

    private static void pub(){
        DMLExecutionEvent event = new DMLExecutionEvent();
        event.setId(UUID.randomUUID().toString());
        event.setDataSource("sj_db_1");
        event.setSql("select * from t_order_0 where user_id=10");
        System.out.println("发布的DML执行事件: " + JSON.toJSONString(event));
        EventBusInstance.getInstance().post(event);
    }
}

源码分析

主要分析发布事件以及注册监听器的核心源码;

注册源码分析

//注册Object上所有订阅方法,用来接收事件,上面的使用参考,DMLExecutionEventListener就是这里的object
public void register(Object object) {
    // 根据注册对象,找到所有订阅者(一个注册对象里可以申明多个订阅者)
    Multimap<Class<?>, EventSubscriber> methodsInListener =
            finder.findAllSubscribers(object);
    // 重入写锁保证线程安全
    subscribersByTypeLock.writeLock().lock();
    try {
        // 把订阅者信息放到map中缓存起来(发布事件post()时就会用到)
        subscribersByType.putAll(methodsInListener);
    } finally {
        // 重入琐写锁解锁
        subscribersByTypeLock.writeLock().unlock();
    }
}

说明:Multimap是guava自定义数据结构,类似Map<K, Collection<V>>key就是事件类型(@Subscribe注解方法的第一个入参:即事件DMLExecutionEvent,例如DMLExecutionEvent。value就是EventSubscriber即事件订阅者集合(其中包含target:为订阅者类,method为被@Subscribe注解的方法)。需要注意的是,这个的订阅者集合是指Object里符合订阅者条件的所有方法。例如DMLExecutionEventListener.listener(),DMLExecutionEventListener中可以有多个订阅者,加上注解@Subscribe即可。

  • 注册总结

通过这段源码分析可知,注册的核心就是将注册对象中所有的订阅者信息缓存起来,方便接下来的发布过程找到订阅者。

发布源码分析:根据获取事件及事件的所有非Object父类,查找这些事件对应的所有订阅方法,将订阅方法加入订阅执行队列进行执行;

public void post(Object event) {
    // 得到所有该类以及它的所有父类,父类的父类,直到Object(因为有些注册的监听器是监听其父类)
    Set<Class<?>> dispatchTypes = flattenHierarchy(event.getClass());

    boolean dispatched = false;
    // 遍历类本身以及所有父类
    for (Class<?> eventType : dispatchTypes) {
        // 重入读锁先锁住
        subscribersByTypeLock.readLock().lock();
        try {
            // 得到类的所有订阅者,例如DMLExecutionEvent的订阅者就是DMLExecutionEventListener(EventSubscriber有两个属性:重要的属性target和method,target就是监听器即DMLExecutionEventListener,method就是监听器方法即listener;从而知道DMLExecutionEvent这个事件由哪个类的哪个方法监听处理)
            Set<EventSubscriber> wrappers = subscribersByType.get(eventType);

            if (!wrappers.isEmpty()) {
                // 如果有订阅者,那么dispatched = true,表示该事件可以分发
                dispatched = true;
                // 遍历所有的订阅者,往每个订阅者的队列中都增加该事件
                for (EventSubscriber wrapper : wrappers) {
                    enqueueEvent(event, wrapper);
                }
            }
        } finally {
            // 解锁
            subscribersByTypeLock.readLock().unlock();
        }
    }
    // 如果没有订阅者,且发送的不是DeadEvent类型事件,那么强制发送一个DeadEvent类型事件。
    if (!dispatched && !(event instanceof DeadEvent)) {
        post(new DeadEvent(this, event));
    }

    // 分发进入队列的事件
    dispatchQueuedEvents();
}

enqueueEvent()&dispatchQueuedEvents()方法源码分析:

/** 
 * 核心数据结构为LinkedList,保存的是EventBus.EventWithSubscriber类型数据
 */
private final ThreadLocal<Queue<EventBus.EventWithSubscriber>> eventsToDispatch =
        new ThreadLocal<Queue<EventBus.EventWithSubscriber>>() {
            @Override protected Queue<EventBus.EventWithSubscriber> initialValue() {
                return new LinkedList<EventBus.EventWithSubscriber>();
            }
        };

void enqueueEvent(Object event, EventSubscriber subscriber) {
    // 数据结构为new LinkedList<EventWithSubscriber>(),EventWithSubscriber就是对event和subscriber的封装,LinkedList数据结构保证进入队列和消费队列顺序一致
    eventsToDispatch.get().offer(new EventBus.EventWithSubscriber(event, subscriber));
}

// 分发队列中的事件交给订阅者处理,这个过程称为drain,即排干。排干的过程中,可能有新的事件被追加到队列尾部
void dispatchQueuedEvents() {
    // 如果当前正在分发,则不重复执行
    if (isDispatching.get()) {
        return;
    }

    // 如果没有正在分发,那么利用ThreadLocal设置正在分发即isDispatching为true
    isDispatching.set(true);
    try {
        Queue<EventBus.EventWithSubscriber> events = eventsToDispatch.get();
        EventBus.EventWithSubscriber eventWithSubscriber;
        // 不断从Queue中取出任务处理(调用poll()方法)        
        while ((eventWithSubscriber = events.poll()) != null) {
            // 调用订阅者处理事件(method.invoke(target, new Object[] { event });,method和target来自订阅者)
            dispatch(eventWithSubscriber.event, eventWithSubscriber.subscriber);
        }
    } finally {
        // ThreadLocal可能内存泄漏,用完需要remove
        isDispatching.remove();
        // 队列中的事件任务处理完,清空队列,即所谓的排干(Drain)
        eventsToDispatch.remove();
    }
}

  • 发布总结

总结一下调用了EventBus的post()方法后的流程:

  1. 遍历发布对象本身以及所有父类,每个类都同等待遇;

  2. 得到类的所有订阅者(监听器的有效方法集合);

  3. 如果有订阅者,就往订阅者的队列中增加事件;若果没有订阅者,并且发送的不是DeadEvent,那么强制发送DeadEvent;

  4. 不断取出队列中的事件,交给订阅者处理。

 

 

实际操作案例

事件机制包括三个部分:事件事件监听器事件源

在Spring cloud环境下,使用google公司开源的guava工具类EventBus。
一、引入guava的jar包
二、在config下新建一个类EventBusConfig.java

import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.Subscribe;
import org.reflections.Reflections;
import org.reflections.scanners.MethodAnnotationsScanner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Set;
import java.util.concurrent.Executors;

@Component
public class EventBusConfig {

   @Autowired
   private ApplicationContext context;

   @Bean
   @ConditionalOnMissingBean(AsyncEventBus.class)
   AsyncEventBus createEventBus() {
       AsyncEventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(5));
       Reflections reflections = new Reflections("com.xxx", new MethodAnnotationsScanner());
       Set<Method> methods = reflections.getMethodsAnnotatedWith(Subscribe.class);
       if (null != methods ) {
           for(Method method : methods) {
               try {
                   eventBus.register(context.getBean(method.getDeclaringClass()));
               }catch (Exception e ) {
                   //register subscribe class error
               }
           }
       }

       return eventBus;
   }
}

三、利用接口封装事件发送
1、定义接口LocalEventBus.java

public interface LocalEventBus {
    void post(Event event);
}

2、定义实现类LocalEventBusImpl.java

import com.google.common.eventbus.AsyncEventBus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class LocalEventBusImpl implements LocalEventBus {

    @Autowired
    private AsyncEventBus eventBus;

    @Override
    public void post(Event event) {
        if (null != event) {
            eventBus.post(event);
        }
    }
}

3、接口Event.class

public interface Event<T> {
    T getContent();
}

四、在业务工程里使用:
需要定义事件、消息体、订阅者、发送者。

1、定义login事件

public class LoginEvent implements Event<LoginMsg> {

    private LoginMsg loginMsg;

    public LoginEvent(LoginMsg loginMsg) {
        this.loginMsg = loginMsg;
    }

    @Override
    public LoginMsg getContent() {
        return this.loginMsg;
    }
}

2、定义消息体

public class LoginMsg {
    private Long uid;
    private String mobile;
    private String ip;
    private String osVersion;
    private String deviceModel;
    private String deviceToken;
}

3、定义订阅者

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class LoginSubscriber {

    @Subscribe
    public void onLogin(LoginEvent event) throws BizException {
        LoginMsg msg = event.getContent();
        Long uid = msg.getUid();
        // 具体业务
    }
}

4、定义发送者,把消息发送到EventBus。

@Autowired
private LocalEventBus localEventBus;

LoginMsg msg = new LoginMsg(uid, mobile, ip, osVersion, deviceModel, deviceToken); 

localEventBus.post(new LoginEvent(msg));

 

实际使用中问题

     这个异步框架在使用中有类似切面的功用,同时异步处理为我们前端响应提供了更好的体验;

     但是在我们的实际项目中遇到了一次难以发现问题的生产事故;

     在频繁点击情况下前后端请求参数无法保障一致性;

      事故起因:

            我们将HttpServletRequest  取做内部框架中的内部使用参数;这就导致了事故的发现;

            首先,有个核心概念 HttpServletRequest  的生命周期,其生命周期仅存在一次请求中,

            因此,在异步的环境下,我们主线程的请求完成之后,可能我们容器就会把线程回收或者去处理下一个任务,而当我们线程在处理下一个任务的时候,上一个任务的HttpServletRequest 是还没有结束生命的,这个时候就会存在多线程的数据不一致情况;

            解决方案: 直接获取请求中的参数出来,进行使用,而不对 HttpServletRequest 进行占用;

 

转载:
作者:天草二十六_
链接:https://www.jianshu.com/p/4efbfdc01cf6

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值