22. 观察者模式:发布-订阅、事件驱动、实战:Spring事件机制

观察者模式,说白了就是“你盯着我,我变了就告诉你”。

我在团队里带新人时,经常用这个模式来解耦业务逻辑。比如用户注册成功后,要发邮件、送积分、记录日志。如果把这些代码全塞在注册方法里,那维护起来就是一场噩梦。观察者模式就是解决这类问题的利器。

22.1 核心概念:发布-订阅

观察者模式定义了一种一对多的依赖关系。当一个对象(主题)的状态发生变化时,所有依赖它的对象(观察者)都会得到通知并自动更新。

你想想看,这就像你关注了一个公众号。公众号发文章,你就能收到推送。你不用天天去刷它,它也不会单独给你打电话。这就是典型的发布-订阅模型。

核心角色:
  • Subject(主题):持有观察者列表,提供注册、移除、通知方法。
  • Observer(观察者):定义一个更新接口,接收主题的通知。
  • ConcreteSubject(具体主题):维护状态,状态变化时通知所有观察者。
  • ConcreteObserver(具体观察者):实现更新接口,处理通知。

我个人习惯把观察者模式分成两种实现方式:

  • 推模型:主题把详细信息推给观察者。适合状态变化频繁的场景。
  • 拉模型:主题只通知“我变了”,观察者自己来拉数据。适合观察者只关心部分信息的场景。

我在项目中遇到过推模型滥用的情况。有一次,一个订单状态变更事件,推了整整20个字段过去。结果大部分观察者只用了订单ID。后来我改成了拉模型,只传一个ID,观察者按需查询。性能提升了不少。

22.2 事件驱动:从轮询到回调

事件驱动编程,本质上是观察者模式的一种高级应用。它把“状态变化”抽象成“事件”,把“观察者”抽象成“监听器”。

传统的轮询方式,你得每隔几秒去检查一下状态。比如:

// 轮询:每隔1秒检查一次
while (true) {
    if (order.isPaid()) {
        sendEmail(order);
        break;
    }
    Thread.sleep(1000);
}

这种写法,说白了就是浪费CPU。而且代码耦合严重,你很难扩展新的处理逻辑。

事件驱动的方式就优雅多了:

// 事件驱动:支付完成后自动触发
order.onPaid(() -> sendEmail(order));
order.onPaid(() -> addScore(order));

你看,代码瞬间变得干净了。每个监听器只关心自己那点事,互不干扰。

我的经验:事件驱动特别适合异步场景。比如用户注册后,发邮件可以异步执行,不用阻塞主流程。但要注意,如果监听器抛出异常,可能会影响其他监听器的执行。我一般会在监听器内部加try-catch,保证一个监听器挂了,不影响其他人。

22.3 实战:Spring事件机制

Spring框架内置了完整的事件机制,底层就是观察者模式。我几乎在每个Spring Boot项目里都会用到它。

Spring事件机制有三个核心组件:

  • 事件(Event):继承 ApplicationEvent 的POJO类。
  • 发布者(Publisher):注入 ApplicationEventPublisher,调用 publishEvent()
  • 监听器(Listener):使用 @EventListener 注解的方法。

来看一个完整的例子。假设我们要实现用户注册后发送欢迎邮件和赠送积分:

// 1. 定义事件
public class UserRegisteredEvent extends ApplicationEvent {
    private final Long userId;
    private final String email;

    public UserRegisteredEvent(Object source, Long userId, String email) {
        super(source);
        this.userId = userId;
        this.email = email;
    }
    // getters...
}

// 2. 发布事件
@Service
public class UserService {
    @Autowired
    private ApplicationEventPublisher publisher;

    public void register(String email) {
        // 注册逻辑...
        Long userId = saveUser(email);
        // 发布事件
        publisher.publishEvent(new UserRegisteredEvent(this, userId, email));
    }
}

// 3. 监听事件
@Component
public class EmailListener {
    @EventListener
    public void handleUserRegistered(UserRegisteredEvent event) {
        // 发送欢迎邮件
        System.out.println("发送邮件到:" + event.getEmail());
    }
}

@Component
public class ScoreListener {
    @EventListener
    public void handleUserRegistered(UserRegisteredEvent event) {
        // 赠送积分
        System.out.println("给用户 " + event.getUserId() + " 赠送100积分");
    }
}

嗯,这里要注意:@EventListener 默认是同步执行的。也就是说,发布者线程会阻塞,直到所有监听器执行完毕。如果监听器里有耗时操作,会影响主流程的性能。

避坑指南:我曾经在一个高并发项目里,直接用同步事件处理日志记录。结果用户注册接口的响应时间从50ms飙升到500ms。后来我加上了 @Async 注解,把监听器改成异步执行,问题才解决。

Spring还支持事件排序和条件过滤。比如:

@EventListener
@Order(1)  // 控制执行顺序
public void handleFirst(UserRegisteredEvent event) {
    // 先执行
}

@EventListener(condition = "#event.email.endsWith('@company.com')")
public void handleCompanyUser(UserRegisteredEvent event) {
    // 只处理公司邮箱注册的事件
}

条件过滤这个功能,我在做多租户系统时用过。不同租户的事件处理逻辑不一样,用condition就能优雅地分流。

22.4 知识体系图

下面这张图,帮你理清观察者模式、事件驱动和Spring事件机制的关系:

观察者模式 → 事件驱动 → Spring事件机制 观察者模式 Subject / Observer 推模型 vs 拉模型 抽象化 事件驱动 事件 / 监听器 异步回调 框架实现 Spring事件 ApplicationEvent @EventListener @Async + @Order 核心思想:解耦发布者与订阅者,让代码更灵活、可扩展 适用场景:跨服务通知、异步处理、日志记录、状态变更

22.5 总结与避坑

观察者模式,说白了就是“别来烦我,有事我喊你”。它让代码的耦合度大大降低,扩展性也更好。

最后分享几个我踩过的坑:

  • 内存泄漏:观察者注册后忘记移除,会导致对象无法被GC。我曾在长连接场景中吃过亏,后来改用弱引用解决了。
  • 异常处理:同步监听器抛异常,会中断后续监听器的执行。我建议每个监听器都自己处理异常,或者用 @Order 控制关键监听器先执行。
  • 性能问题:事件机制虽好,但不要滥用。如果一个事件有几十个监听器,建议考虑用消息队列(如RabbitMQ)来解耦。

记住,设计模式是工具,不是目的。用对了地方,代码就优雅;用错了,反而增加复杂度。

公众号:蓝海资料掘金营,微信deep3321