Skip to content

单机事件驱动解耦:Guava EventBus ​

「订单创建后要发短信、发邮件、更新库存,这些代码堆在 createOrder 里越滚越长怎么办」——面试里的标准答案之一就是事件驱动解耦。不上 MQ 的单机场景,Guava 的 EventBus 是最轻的落地件:观察者模式的官方工具化实现(模式背景见 Java设计模式全览 的观察者模式一节)。本篇搭一套 Spring 环境下的完整骨架,并补两个实战要点:注册自动化的原理、同步事件异常如何抛回调用方。

1 定位:单机版观察者 ​

EventBus 处理的事情类似观察者模式:观察者订阅自己感兴趣的事件类型,发布方 post 一个事件,总线分发给所有订阅了该类型的方法。价值在解耦:系统里很多地方都会触发订单修改——用户改、客服改、拼团失败系统自动改,每个触发点都要发短信、发邮件,每加一个动作就要改 N 处。改成事件驱动后,触发点只管 post 事件,后续动作各自挂监听,加操作只加 listener。

Guava 提供两种总线:EventBus 同步模式,post 的线程自己依次执行所有 subscriber;AsyncEventBus 异步模式,subscriber 丢进线程池执行,post 立即返回。发短信这类不必等待的事走异步、需要事务内联动的走同步,一个注册中心可同时持有两个。

2 标记注解与依赖 ​

先定义注解,只用于标记"这是个 EventBus 监听器",有了它注册就能自动化,业务方不需要知道任何注册细节:

java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBusListener {
}

依赖即 guava 单包引入(com.google.guava:guava,22.0 即可用)。

3 注册中心:EventBusCenter ​

核心组件,双总线 + 启动时自动注册:

java
@Component
public class EventBusCenter {

    private EventBus syncEventBus = new EventBus();                       // 管理同步事件

    private AsyncEventBus asyncEventBus =                                 // 管理异步事件
            new AsyncEventBus(Executors.newCachedThreadPool());

    public void postSync(Object event) { syncEventBus.post(event); }
    public void postAsync(Object event) { asyncEventBus.post(event); }

    @PostConstruct
    public void init() {
        // 获取所有带 @EventBusListener 的 bean,注册为监听者
        List<Object> listeners = SpringContextUtils.getBeansWithAnnotation(EventBusListener.class);
        for (Object listener : listeners) {
            asyncEventBus.register(listener);
            syncEventBus.register(listener);
        }
    }
}

设计要点:注册中心是 Spring 组件,@PostConstruct 保证所有 bean 就绪后完成注册;同一个 listener 同时挂两条总线,方法订阅哪个事件由 @Subscribe 的参数类型决定,互不干扰。

4 事件、监听器与发布 ​

事件是普通 POJO,按业务动作命名:

java
public class OrderCreatedEvent {
    private long orderId;
    private long userId;
    public OrderCreatedEvent(long orderId, long userId) { /* 赋值 */ }
    // getter 省略
}

监听器:类上打 @EventBusListener,方法上打 Guava 的 @Subscribe,方法参数类型即订阅的事件类型,这是 EventBus 的分发依据:

java
@Component
@EventBusListener
public class OrderChangeListener {

    @Subscribe
    public void created(OrderCreatedEvent event) {
        // 发短信、发邮件等。事件可被多个方法订阅,
        // 没必要在一个方法里塞进所有动作;订阅别的事件再写一个 @Subscribe 方法即可
    }
}

业务代码只依赖注册中心,完全不知道后面有谁在听:

java
@Service
public class OrderService {

    @Autowired
    private EventBusCenter eventBusCenter;

    public void createOrder() {
        // 处理创建订单本身 ...
        eventBusCenter.postAsync(new OrderCreatedEvent(1L, 1L));  // 异步通知所有订阅方
    }
}

5 注册自动化的原理:SpringContextUtils ​

第 3 节用到的 SpringContextUtils 是大部分 Spring 应用的标配工具——通过 BeanFactoryPostProcessor 提前抓下 BeanFactory:

java
@Component
public class SpringContextUtils implements BeanFactoryPostProcessor {

    private static ConfigurableListableBeanFactory beanFactory;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) {
        SpringContextUtils.beanFactory = factory;
    }

    // 注册中心用到的就是这个:按注解取所有 bean(@Component 派生注解也会被匹配)
    public static List<Object> getBeansWithAnnotation(Class<? extends Annotation> type) {
        return beanFactory.getBeansWithAnnotation(type).entrySet()
                .stream().map(Map.Entry::getValue).collect(Collectors.toList());
    }
}

要点:用 BeanFactoryPostProcessor 而非 ApplicationContextAware 拿容器,可以在更早阶段、以纯静态方式取 bean,适合基础设施工具类。

6 同步事件的异常如何抛回调用方 ​

同步模式下,post 线程亲自执行完所有 subscriber 才返回;但 EventBus 默认的异常处理只是打日志,subscriber 抛出的异常不会传给 post 的调用方——对"用同步事件做联动校验"的场景是致命的。解法是自定义 SubscriberExceptionHandler + ThreadLocal 传递:

java
ThreadLocal<ServiceException> threadLocal = new ThreadLocal<>();

private EventBus syncEventBus = new EventBus(new SubscriberExceptionHandler() {
    @Override
    public void handleException(Throwable exception, SubscriberExceptionContext context) {
        if (exception instanceof ServiceException) {
            threadLocal.set((ServiceException) exception);
        }
    }
});

public void postSync(Object event) {
    syncEventBus.post(event);
    ServiceException ex = threadLocal.get();
    if (ex != null) {
        threadLocal.remove();   // 记得清理,防止污染线程池中的下个请求
        throw ex;
    }
}

两个必须知道的细节:同步总线上一个 subscriber 抛异常不会阻止后续 subscriber 执行,异常统一进 handler,多个 subscriber 都抛时 ThreadLocal 被覆盖、最终拿到最后一个;postSync 里务必 remove,否则异常泄漏给复用该线程的下一个请求。

7 边界与注意事项 ​

  • 没有订阅者的事件不报错,会被包装成 DeadEvent 再发一次——可订阅 DeadEvent 排障:post 了没人听,多半是忘加 @EventBusListener 或事件类型对不上;
  • @Subscribe 方法只能有一个参数,分发按参数类型匹配,注意父类型会被同时命中;
  • AsyncEventBus 的线程池要按业务量有界化,CachedThreadPool 在事件风暴下可能拖垮机器;
  • EventBus 是纯单机进程内总线:无持久化、无重试、宕机即丢;集群与可靠性场景直接上 MQ,别拿 EventBus 硬撑。

面试追问链 ​

  • EventBus 和 MQ 的区别?→ 进程内分发 vs 跨进程可靠传输;EventBus 无持久化无重试;
  • 同步和异步怎么选?→ 需要事务内联动、异常要反馈给调用方用同步;通知类动作用异步(AsyncEventBus + 线程池);
  • subscriber 抛异常会怎样?→ 不阻断同事件的其他 subscriber,进 SubscriberExceptionHandler,默认只打日志;要抛回调用方需自定义 handler(ThreadLocal 方案);
  • 监听器怎么自动注册?→ 自定义注解标记 + getBeansWithAnnotation 扫描 + @PostConstruct 统一 register;
  • post 一个没人订阅的事件?→ 包装成 DeadEvent 再发,可订阅 DeadEvent 发现配置遗漏。

面试官视角小结 ​

这道题考的不是"会不会用 Guava",而是解耦的分寸感。及格线是说出观察者模式与"触发点只管发事件";答出同步/异步两条总线的选型、异常回传的 ThreadLocal 方案,说明真在生产里用过;能主动划清边界——进程内解耦用 EventBus、跨服务可靠性用 MQ——才是面试官想听的工程判断。加分点还有 DeadEvent 排障和 AsyncEventBus 线程池有界化。与 Java设计模式全览 的观察者模式对照着复习,一题双吃。

来源:javadoop《单机环境下优雅地使用事件驱动进行代码解耦》(https://javadoop.com/post/guava-eventbus)——本文为研读消化后的重写笔记,原文版权归 javadoop,引用请遵原文。