Skip to content

观察者与事件驱动:从监听器到消息总线

本文是设计模式系统学习系列的 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.Observablejava.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 设计文档。

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1