Spring Boot + EventBus:构建松耦合的事件驱动系统
Spring Boot + EventBus:构建松耦合的事件驱动系统
在微服务和高并发场景下,"谁调用谁"的强耦合往往会成为系统演进的绊脚石。事件驱动(Event-Driven)通过"发布-订阅"模型让模块之间只约定事件、不约定调用,从而实现真正的松耦合。本文系统讲解在 Spring Boot 中如何用 Event Bus 思想落地事件驱动,涵盖 Spring 原生事件机制、Guava EventBus 以及实战选型。
一、为什么需要事件驱动
先看一个典型的"强耦合"场景:用户下单成功后,需要做三件事——发短信通知、赠送积分、写风控日志。最直觉的写法是在 OrderService 里依次调用三个 Service:
@Service
public class OrderService {
private final SmsService smsService;
private final PointService pointService;
private final RiskService riskService;
public void createOrder(Order order) {
save(order);
smsService.notify(order); // 强耦合
pointService.award(order); // 强耦合
riskService.log(order); // 强耦合
}
}
这种写法有三个问题:
- 耦合:
OrderService必须知道所有下游服务,新增一个下游就要改这里。 - 阻塞:三个调用串行执行,任一变慢整体变慢。
- 事务扩散:如果都在同一事务里,任一失败全部回滚——但发短信失败其实不该让下单失败。
事件驱动的思路是:OrderService 只管"下单成功"这件事,然后发布一个事件;谁关心这个事件,谁自己去订阅处理。OrderService 不需要知道下游有谁。
OrderService ──publish──▶ [OrderCreatedEvent] ──subscribe──▶ SmsListener
──subscribe──▶ PointListener
──subscribe──▶ RiskListener
这就是 Event Bus 的核心:一个事件中心,发布者往里扔事件,订阅者从里取事件,双方互不感知。
二、Spring Boot 自带的事件机制
很多人不知道,Spring 容器本身就是一个 Event Bus。ApplicationContext 内置了一个事件广播器,任何 Bean 都可以通过 ApplicationEventPublisher 发布事件,用 @EventListener 订阅事件。
2.1 定义事件
Spring 4.2 之后事件不再需要继承 ApplicationEvent,普通 POJO 即可:
public record OrderCreatedEvent(Long orderId, Long userId, BigDecimal amount, Instant createdAt) {}
2.2 发布事件
注入 ApplicationEventPublisher 发布:
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
public OrderService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void createOrder(Order order) {
save(order);
// 发布事件,下游由各自的 Listener 处理
publisher.publishEvent(new OrderCreatedEvent(
order.getId(), order.getUserId(), order.getAmount(), Instant.now()));
}
}
2.3 订阅事件
用 @EventListener 注解即可订阅,Spring 会按事件类型自动分发:
@Component
public class OrderEventListeners {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 发短信
System.out.println("发送短信: 订单 " + event.orderId());
}
@EventListener
public void onOrderCreatedForPoint(OrderCreatedEvent event) {
// 送积分
System.out.println("赠送积分: 订单 " + event.orderId());
}
}
同一个事件可以有多个监听器,彼此独立。新增下游只需新增一个 @EventListener 方法,OrderService 一行都不用改。
2.4 异步处理
默认监听器是同步执行的(和发布者在同一线程)。要异步,加 @Async 并开启异步支持:
@Configuration
@EnableAsync
public class AsyncConfig {}
@Component
public class OrderEventListeners {
@Async
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 异步执行,不阻塞下单主流程
}
}
2.5 事务绑定:@TransactionalEventListener
这是 Spring 事件机制最强大的特性之一。普通 @EventListener 在事件发布时立即执行,哪怕发布点在事务内、事务还没提交,监听器就已经跑了——如果后续事务回滚,监听器做的副作用(比如发了短信)就"错发"了。
@TransactionalEventListener 可以把监听器绑定到事务的特定阶段:
| 阶段 | 含义 |
|---|---|
AFTER_COMMIT(默认) |
事务提交成功后执行 |
BEFORE_COMMIT |
事务提交前执行 |
AFTER_ROLLBACK |
事务回滚后执行 |
AFTER_COMPLETION |
事务完成(提交或回滚)后执行 |
@Component
public class OrderEventListeners {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
// 只有订单真正落库成功后才发短信,避免"幽灵短信"
}
}
坑点:
@TransactionalEventListener默认要求发布点处于一个事务中。如果发布时没有事务,事件会被直接丢弃。需要无事务也执行时加fallbackExecution = true。
三、Guava EventBus
Spring 的事件机制绑定在 Spring 容器上,事件在容器内传播。如果想要一个更轻量、不依赖容器、可独立使用的 Event Bus,Guava 的 EventBus 是经典选择。
3.1 引入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.3.1-jre</version>
</dependency>
3.2 基本用法
// 定义事件(普通 POJO)
public record OrderCreatedEvent(Long orderId, BigDecimal amount) {}
// 订阅者
public class OrderListener {
@Subscribe
public void handle(OrderCreatedEvent event) {
System.out.println("处理订单事件: " + event.orderId());
}
}
// 使用
EventBus eventBus = new EventBus("order-bus");
eventBus.register(new OrderListener());
eventBus.post(new OrderCreatedEvent(1001L, new BigDecimal("99.00")));
@Subscribe 标记订阅方法,register 注册订阅者,post 发布事件。EventBus 会根据事件类型自动路由到对应的订阅方法。
3.3 异步:AsyncEventBus
EventBus 是同步的,post 会阻塞直到所有订阅者处理完。要异步,用 AsyncEventBus 配合线程池:
EventBus asyncBus = new AsyncEventBus("async-order-bus", Executors.newFixedThreadPool(8));
3.4 DeadEvent:没有订阅者的事件
当 post 的事件没有任何订阅者时,EventBus 会把它包装成 DeadEvent 再发一次。可以订阅 DeadEvent 来排查"事件发了但没人处理"的问题:
@Subscribe
public void onDead(DeadEvent deadEvent) {
log.warn("无人处理的事件: {}", deadEvent.getEvent());
}
3.5 在 Spring Boot 中集成 Guava EventBus
把 EventBus 声明为 Bean,并自动注册所有带 @Subscribe 方法的 Bean:
@Configuration
public class EventBusConfig {
@Bean
public EventBus eventBus() {
return new AsyncEventBus("app-bus", Executors.newFixedThreadPool(8));
}
// 自动注册所有 @Subscribe 标注的 Bean
@Bean
public EventBusRegistrar eventBusRegistrar(EventBus eventBus, ApplicationContext ctx) {
return new EventBusRegistrar(eventBus, ctx);
}
}
public class EventBusRegistrar implements BeanPostProcessor {
private final EventBus eventBus;
public EventBusRegistrar(EventBus eventBus, ApplicationContext ctx) {
this.eventBus = eventBus;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
for (Method m : bean.getClass().getMethods()) {
if (m.isAnnotationPresent(Subscribe.class)) {
eventBus.register(bean);
break;
}
}
return bean;
}
}
这样业务代码里直接注入 EventBus 调 post,订阅者用 @Subscribe 即可,与 Spring 体系无缝融合。
四、实战:用 Spring 事件重构订单系统
把开头的强耦合例子用 Spring 事件重构。
事件定义:
public record OrderCreatedEvent(Long orderId, Long userId, BigDecimal amount, Instant createdAt) {}
发布(下单主流程):
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher publisher;
public OrderService(OrderRepository orderRepository, ApplicationEventPublisher publisher) {
this.orderRepository = orderRepository;
this.publisher = publisher;
}
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
publisher.publishEvent(new OrderCreatedEvent(
order.getId(), order.getUserId(), order.getAmount(), Instant.now()));
}
}
订阅(各下游独立处理,事务提交后异步执行):
@Component
public class SmsListener {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
smsService.send(event.userId(), "下单成功,订单号 " + event.orderId());
}
}
@Component
public class PointListener {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
pointService.award(event.userId(), event.amount().intValue());
}
}
@Component
public class RiskListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
riskService.log(event.userId(), event.amount());
}
}
对比重构前后:
| 维度 | 重构前 | 重构后 |
|---|---|---|
OrderService 依赖 |
3 个下游 Service | 1 个 EventPublisher |
| 新增下游 | 改 OrderService |
新增一个 Listener |
| 短信失败影响下单 | 是(同事务回滚) | 否(AFTER_COMMIT + Async) |
| 串行耗时 | 3 次调用累加 | 异步并行 |
五、进阶对比与选型
5.1 Spring Events vs Guava EventBus
| 特性 | Spring Events | Guava EventBus |
|---|---|---|
| 依赖 | Spring 容器 | 仅 Guava |
| 事件类型匹配 | 按类型,支持泛型 | 按类型 |
| 异步 | @Async |
AsyncEventBus |
| 事务感知 | @TransactionalEventListener |
无(需自己实现) |
| 跨 Bean 自动注册 | 自动 | 需手动 register |
| DeadEvent 处理 | 无内置 | 内置 DeadEvent |
| 跨进程/微服务 | 需配合 MQ | 不支持 |
| 适用场景 | Spring 应用内 | 轻容器/轻量场景 |
5.2 什么时候不该用 Event Bus
Event Bus 是进程内的事件机制,事件不落盘、不跨进程。以下场景应该用消息队列(Kafka/RabbitMQ/RocketMQ)而非 Event Bus:
- 需要跨服务/跨进程传递事件
- 需要事件持久化、可重放
- 需要严格的顺序保证和削峰填谷
- 服务重启后未处理的事件需要恢复
一个常见的演进路径是:进程内用 Spring Events 解耦,跨进程用 MQ。两者并不冲突,甚至可以组合——监听器收到 Spring 事件后,再转发到 MQ 供其他服务消费。
5.3 注意事项
- 事件不宜过重:事件应携带必要的标识信息(如 orderId),而非整个聚合根。需要的数据由订阅者按需查询。
- 避免事件循环:A 监听 B 的事件又发布新事件,链路过长难以排查。建议事件链路控制在 1-2 跳。
- 异步异常会丢失:
@Async方法抛出的异常不会传播到发布者,需配置AsyncUncaughtExceptionHandler兜底。 @TransactionalEventListener默认丢弃无事务事件:发布点可能不在事务中时,加fallbackExecution = true。- 不要在事件里做核心业务:事件处理是"副作用",核心业务(如扣库存)应在主流程同步完成,事件只做通知、统计、日志等旁路操作。
六、总结
事件驱动的本质是把"调用"换成"通知",用 Event Bus 作为中间媒介实现发布者与订阅者的解耦。在 Spring Boot 生态下:
- 首选 Spring 原生事件机制:
ApplicationEventPublisher+@EventListener+@TransactionalEventListener,零额外依赖,与事务、异步天然集成。 - 轻量/跨容器场景用 Guava EventBus:API 简洁,
DeadEvent便于排查,但需自行处理事务和注册。 - 跨进程用 MQ:Event Bus 不替代消息队列,二者分层互补。
掌握事件驱动,是从"写能跑的代码"到"设计可演进的架构"的关键一步。希望这篇博客能帮你在下一个 Spring Boot 项目里,把那些纠缠在一起的 Service 调用,优雅地拆开。
参考资料:
- Spring Framework Reference — Core Events
- Guava Wiki — EventBus Explained
- 《Spring 实战(第 6 版)》
- 点赞
- 收藏
- 关注作者
评论(0)