主题
观察者与事件驱动:从监听器到消息总线
本文是设计模式系统学习系列的 L2 核心篇。前置:代理与装饰器。 学完可以配合面试题食用:Spring 事件机制、为什么需要 MQ。
观察者模式在解决什么问题
你有一个对象状态变了,需要通知一堆依赖方。最直接的做法是在状态变更处挨个调一遍:
java
order.status = PAID;
smsService.send("订单已支付");
emailService.send("订单已支付");
logService.log("支付成功");每加一个通知方就要改订单代码,而且通知方和订单对象绑死了。观察者模式把"谁要通知"抽出来:Subject 只管维护一个观察者列表,状态一变就广播出去,观察者自己决定接不接。
观察者模式本体:推模型与拉模型
Subject 持有 List<Observer>,提供 attach/detach/notify。观察者注册到 Subject 上,收到通知后执行自己的逻辑。
推模型:Subject 把完整状态推给观察者。优点是观察者不用再查 Subject 拿数据,缺点是 Subject 得知道观察者要什么,Payload 容易膨胀。
拉模型:Subject 只通知"我变了",观察者主动调 Subject 的 getter 拿数据。解耦更彻底,但观察者需要知道 Subject 的具体类型。
Java 标准库的设计是混合的:Observable.notifyObservers(arg) 传一个 Object arg,观察者可以推也可以拉。
java
// 观察者接口(拉模型 + 推模型共存)
public interface Observer {
void update(Observable o, Object arg);
}
// 具体商品
public class Product extends Observable {
private double price;
public void setPrice(double price) {
this.price = price;
setChanged();
notifyObservers(price); // 推:直接传新价格
}
public double getPrice() {
return price; // 拉:观察者也可以调这个
}
}Observable/Observer 为什么被 JDK 9 废弃
java.util.Observable 和 java.util.Observer 在 JDK 9 标为废弃。几个硬伤:
- Observable 是类不是接口:Java 单继承,你想让订单类继承 Observable,它就不能再继承别的。倒逼组合,但标准库没提供接口形式。
- 没有泛型:
update(Observable o, Object arg)里的Object arg要强转,编译期不检查类型安全。 - 线程安全简陋:
setChanged()用synchronized但不保证通知顺序,也不能在并发场景下可靠地增量更新。 - 通知顺序不可控:观察者以
Vector存储,同步但无序,业务场景需要优先级时得自己处理。
官方推荐替代:java.beans.PropertyChangeListener(属性级别的观察)、javax.swing.event 系列、或者直接用 Flow API(响应式流)。
Spring 事件机制:ApplicationEvent + @EventListener
Spring 实现了进程内的事件总线,是观察者模式在企业级框架中最典型的升级版。
发布与监听
java
// 1. 定义事件
@Getter
public class OrderPaidEvent extends ApplicationEvent {
private final Long orderId;
private final BigDecimal amount;
public OrderPaidEvent(Object source, Long orderId, BigDecimal amount) {
super(source);
this.orderId = orderId;
this.amount = amount;
}
}
// 2. 发布事件
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher publisher;
@Transactional
public void payOrder(Long orderId) {
// 业务逻辑...
OrderPaidEvent event = new OrderPaidEvent(this, orderId, amount);
publisher.publishEvent(event);
}
}
// 3. 监听事件
@Component
public class OrderEventListener {
@EventListener // 同步,默认与发布者在同一事务
public void handleOrderPaid(OrderPaidEvent event) {
log.info("订单 {} 已支付 {} 元", event.getOrderId(), event.getAmount());
// 发货、积分、短信...
}
@EventListener
@Async // 异步执行,不阻塞主流程
public void sendNotification(OrderPaidEvent event) {
// 发短信/推送,慢一点没关系
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) // 事务提交后才触发
public void afterCommit(OrderPaidEvent event) {
// 事务已提交,此时发 MQ 才安全——否则 MQ 消费者读到数据时事务可能还没落盘
mqTemplate.send("order.paid", event);
}
}Spring 事件机制的好处:
- 发布者与监听器完全解耦:
OrderService只知道发了事件,不知道谁会消费。 - 同步/异步一把切换:
@Async一行注解就把阻塞变成线程池异步。 - 事务感知:
@TransactionalEventListener能在事务提交/回滚/完毕后触发,避免"事务没提交、监听器已经开始查数据"的问题。
一个容易踩的坑
java
@Transactional
public void payOrder(Long orderId) {
orderDao.updateStatus(orderId, "PAID");
// 事件发布——但此时事务还没提交
publisher.publishEvent(new OrderPaidEvent(this, orderId, amount));
// 如果监听器去查数据库,会读到未提交的更新(取决于隔离级别)
// 而且在事务回滚时,监听器可能已经执行了不可逆的操作(如发短信)
}用 @TransactionalEventListener(phase = AFTER_COMMIT) 解决:事务提交后才执行监听器,保证数据库已落盘。
从进程内到进程间:尺度的跨越
观察者模式的核心思想——"一个状态变化,多方自动反应"——在不同尺度上表现为不同的技术形态:
| 尺度 | 实现 | 特点 |
|---|---|---|
| 进程内 | 观察者模式 | 同步/异步,单 JVM,无序列化 |
| 进程内升级 | 事件总线(Guava EventBus / Spring Events) | 松耦合,可异步,可事务绑定 |
| 进程间 | 消息队列(RabbitMQ / Kafka / RocketMQ) | 跨服务,持久化,削峰填谷 |
这三者不是替代关系,是不同层级的工具。一个支付系统里,进程内事件做缓存失效、短信通知;MQ 做跨服务通知(订单服务通知积分服务)。
工程坑:观察者模式用得不对比不用还糟
- 事件风暴:一个事件挂十几个监听器,全是同步执行,发布线程被拖死。解决方案:该异步的异步,该下放 MQ 的下放 MQ。
- 监听器异常吞掉主流程:默认同步监听器抛异常,异常会传到发布者。如果监听器是"可有可无"的(比如发日志),异常却让主业务回滚了。Spring 里可以用
@EventListener(value = ...)配合@Order控制优先级,让关键监听器先执行,非关键监听器异步处理。 - 事件顺序不保证:多个监听器之间的执行顺序依赖注册顺序或
@Order,但异步场景下执行顺序完全不可控。如果业务依赖顺序,应改为同步 + 显式编排,或者单线程 MQ 消费。 - 事务绑定被忽略:事务内发事件,监听器去查数据库发现数据还没提交——这是最常见的"线上 bug 但本地测不出"的问题。
@TransactionalEventListener能解决,但很多人不知道这个注解的存在。
动手实操
完整实现一个"下单后事务安全地发通知"的 Spring 事件示例:
java
// 1. 事件类
@Getter
public class OrderCreatedEvent extends ApplicationEvent {
private final Long orderId;
private final Long userId;
private final BigDecimal total;
public OrderCreatedEvent(Object source, Long orderId, Long userId, BigDecimal total) {
super(source);
this.orderId = orderId;
this.userId = userId;
this.total = total;
}
}
// 2. 发布者
@Component
public class OrderPublisher {
@Autowired
private ApplicationEventPublisher publisher;
@Transactional
public void createOrder(Long userId, List<Long> skuIds) {
// 1. 校验库存
// 2. 创建订单
Order order = new Order(userId, skuIds);
// 3. 保存订单(事务内)
// orderDao.save(order);
// 4. 发布事件(事务提交后才真正触发监听器)
publisher.publishEvent(new OrderCreatedEvent(this, order.getId(), userId, order.getTotal()));
}
}
// 3. 监听器(事务提交后)
@Component
public class OrderEventListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
// 此时订单已落盘,可以安全操作
// 1. 发送 MQ 通知积分服务
// 2. 发送短信/邮件
// 3. 记录审计日志
log.info("订单 {} 已创建,用户 {},金额 {},执行后续操作",
event.getOrderId(), event.getUserId(), event.getTotal());
}
@EventListener
@Async
public void sendCoupon(OrderCreatedEvent event) {
// 异步发送优惠券——不影响主流程
// couponService.send(event.getUserId());
}
}关键点记忆
- 观察者模式本质:Subject 维护观察者列表,状态变化时通知,支持推/拉两种数据传递方式
- JDK 9 废弃 Observable 的原因:是类不是接口、无泛型、线程安全差——用组合或者 Spring 事件替代
- Spring 事件机制的三层能力:
@EventListener同步监听、@Async异步监听、@TransactionalEventListener事务绑定 - 从单机到分布式的尺度演进:观察者 → 事件总线 → MQ,思想一致,实现不同
- 工程坑记住三条:同步监听器异常会打崩主流程、事务内发事件记得用
@TransactionalEventListener、异步事件顺序不可控
参考
参考:Spring Framework 文档 — Events、JDK 9
Observable废弃说明(JEP 383)、Google Guava EventBus 设计文档。