7. 观察者模式(下):自定义事件监听器、Guava EventBus实战、观察者模式在消息队列中的应用

好,咱们接着聊观察者模式的下半场。

上一章我们把观察者模式的理论和基础实现讲透了。但说实话,工作中你很少会手写一个观察者模式——除非你在造轮子。更多时候,我们会用现成的框架,或者把它跟消息队列结合起来。

这一章,我带你看看观察者模式在真实项目里的三种玩法:自定义事件监听器Guava EventBus、以及消息队列中的观察者

7.1 自定义事件监听器:自己动手,丰衣足食

先说说自定义事件监听器。这玩意儿其实不复杂,说白了就是观察者模式的一个变种。

我记得刚工作那会儿,接手一个老项目。里面有个模块,用户下单后要发邮件、发短信、更新库存、记录日志……所有逻辑都写在一个方法里。那代码,啧啧,一个方法上千行。后来我重构的时候,第一件事就是引入事件监听器。

怎么做?三步走:

  1. 定义事件——就是观察者模式里的「主题状态」
  2. 定义监听器接口——就是观察者
  3. 定义事件发布器——就是被观察者

来看代码:

// 1. 定义事件
public class OrderEvent {
    private Long orderId;
    private String orderType;
    private LocalDateTime createTime;
    // getter/setter 省略
}

// 2. 定义监听器接口
public interface OrderEventListener {
    void onOrderCreated(OrderEvent event);
}

// 3. 定义事件发布器
public class OrderEventPublisher {
    private List<OrderEventListener> listeners = new ArrayList<>();
    
    public void register(OrderEventListener listener) {
        listeners.add(listener);
    }
    
    public void publish(OrderEvent event) {
        for (OrderEventListener listener : listeners) {
            listener.onOrderCreated(event);
        }
    }
}

嗯,看着眼熟吧?其实就是观察者模式。但这里有个坑——同步调用。如果监听器里有个耗时操作,比如发邮件要3秒,那整个下单流程就得等3秒。用户可没这耐心。

避坑指南:我曾经在一个支付系统里用同步监听器处理风控校验,结果支付接口平均耗时从200ms飙到了2秒。后来改成异步线程池处理,才恢复正常。

所以,生产级的自定义事件监听器,一般会搭配线程池:

public class AsyncEventPublisher {
    private ExecutorService executor = Executors.newFixedThreadPool(4);
    private List<OrderEventListener> listeners = new CopyOnWriteArrayList<>();
    
    public void publishAsync(OrderEvent event) {
        for (OrderEventListener listener : listeners) {
            executor.submit(() -> listener.onOrderCreated(event));
        }
    }
}

这里用 CopyOnWriteArrayList 而不是 ArrayList,是因为监听器可能在运行时动态注册,要避免并发修改异常。

7.2 Guava EventBus:谷歌帮你封装好了

自己写事件监听器,其实挺麻烦的。你要管理监听器列表、处理线程安全、还要考虑异常处理……

这时候,Guava 的 EventBus 就派上用场了。它把观察者模式封装得特别优雅,你只需要关注业务逻辑。

我个人的习惯是:小项目用 EventBus,大项目用消息队列。EventBus 适合进程内的事件分发,轻量、无依赖。

来看怎么用:

// 1. 定义事件
public class OrderCreatedEvent {
    private Long orderId;
    // 构造方法、getter 省略
}

// 2. 定义监听器(用 @Subscribe 注解标记)
public class EmailListener {
    @Subscribe
    public void handleOrderCreated(OrderCreatedEvent event) {
        System.out.println("发送邮件,订单ID:" + event.getOrderId());
    }
}

public class SmsListener {
    @Subscribe
    public void handleOrderCreated(OrderCreatedEvent event) {
        System.out.println("发送短信,订单ID:" + event.getOrderId());
    }
}

// 3. 使用 EventBus
public class OrderService {
    private EventBus eventBus = new EventBus();
    
    public OrderService() {
        eventBus.register(new EmailListener());
        eventBus.register(new SmsListener());
    }
    
    public void createOrder(Long orderId) {
        // 业务逻辑...
        eventBus.post(new OrderCreatedEvent(orderId));
    }
}

你看,代码简洁多了。不需要自己维护监听器列表,不需要写循环调用。EventBus 内部帮你做了所有事情。

EventBus 还有几个好用的特性:

  • DeadEvent:如果某个事件没有监听器处理,EventBus 会把它包装成 DeadEvent 发出来。你可以监听 DeadEvent 来记录日志,排查问题。
  • AsyncEventBus:如果你需要异步处理,直接用 new AsyncEventBus(executor) 就行。
  • 事件继承:监听父事件的方法,也能收到子事件。这个特性有时候很实用。
小技巧:我习惯在项目里定义一个全局的 EventBus 单例,所有模块共用。这样模块之间解耦得很干净,A模块发事件,B模块监听,彼此完全不知道对方存在。

7.3 观察者模式在消息队列中的应用

好了,前面说的都是进程内的事件分发。但真实的大型系统,往往是分布式的。这时候,观察者模式就演变成了消息队列

你想想看,消息队列的「生产者-消费者」模型,本质上不就是观察者模式吗?

  • 生产者 = 被观察者(主题)
  • 消费者 = 观察者
  • 消息 = 事件
  • 消息队列 = 事件通道

我在项目中用过 RabbitMQ、Kafka、RocketMQ。它们虽然实现细节不同,但核心思想是一样的:解耦生产者和消费者

来看一个典型的场景:用户注册后,需要发欢迎邮件、送积分、同步到CRM系统。

如果用消息队列,代码大概是这样的:

// 生产者:用户注册后发送消息
public class UserService {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void register(String userId) {
        // 保存用户...
        // 发送消息
        rabbitTemplate.convertAndSend("user.exchange", "user.register", userId);
    }
}

// 消费者1:发邮件
@Component
@RabbitListener(queues = "email.queue")
public class EmailConsumer {
    @RabbitHandler
    public void handle(String userId) {
        System.out.println("发送欢迎邮件给用户:" + userId);
    }
}

// 消费者2:送积分
@Component
@RabbitListener(queues = "points.queue")
public class PointsConsumer {
    @RabbitHandler
    public void handle(String userId) {
        System.out.println("赠送100积分给用户:" + userId);
    }
}

你看,跟观察者模式的结构一模一样。只不过观察者(消费者)不在同一个进程里,甚至可以用不同的语言实现。

这里有个关键区别:消息队列是异步的、跨进程的、可靠的。而进程内的观察者模式,默认是同步的、同进程的、不可靠的(如果监听器抛异常,发布者可能感知不到)。

核心观点:观察者模式是思想,消息队列是它的分布式实现。理解了这个,你就知道什么时候该用 EventBus,什么时候该上 MQ。

7.4 三种方案的对比与选型

说了这么多,到底该怎么选?我画了一张图,帮你理清思路:

观察者模式三种实现方案对比 自定义事件监听器 ✅ 完全可控 ✅ 无外部依赖 ❌ 需要自己处理线程安全 ❌ 功能单一 适用场景: 小项目、简单回调 框架内部事件 学习/教学场景 Guava EventBus ✅ 开箱即用 ✅ 注解驱动,代码简洁 ✅ 支持同步/异步 ❌ 仅限进程内 适用场景: 单体应用事件分发 模块解耦 不需要持久化 消息队列(MQ) ✅ 跨进程/跨语言 ✅ 消息持久化 ✅ 高可用、高吞吐 ❌ 引入外部依赖 适用场景: 分布式系统 需要可靠投递 异步解耦 复杂度 → 逐渐增加,能力也逐渐增强

我个人总结的选型原则:

  • 单体应用、模块少:用自定义监听器就够了,别引入多余依赖
  • 单体应用、模块多:上 Guava EventBus,代码更优雅
  • 分布式系统:必须上消息队列,这是唯一靠谱的方案
  • 混合场景:进程内用 EventBus,跨进程用 MQ,两者不冲突

7.5 一个实战案例:订单状态变更通知

最后,我分享一个真实案例。之前做一个电商平台,订单状态变更后,需要通知多个系统:

  • 通知用户(短信、邮件、站内信)
  • 通知库存系统
  • 通知物流系统
  • 通知数据分析系统

一开始,代码是这样的:

public void updateOrderStatus(Long orderId, String status) {
    orderDao.updateStatus(orderId, status);
    sendSms(orderId);
    sendEmail(orderId);
    updateInventory(orderId);
    notifyLogistics(orderId);
    pushToDataAnalysis(orderId);
}

嗯,你懂的,又臭又长。而且每次新增一个通知方,都要改这个方法。

后来我用 EventBus 重构了进程内的通知,用 MQ 处理跨系统的通知:

// 进程内:用 EventBus 通知站内信、日志等
eventBus.post(new OrderStatusChangeEvent(orderId, status));

// 跨进程:用 MQ 通知库存、物流等
rabbitTemplate.convertAndSend("order.exchange", "status.change", message);

这样改完之后,新增一个通知方,只需要新增一个监听器或者消费者,完全不用改核心代码。这就是观察者模式最大的价值——开闭原则

我的经验:观察者模式用得好的项目,扩展性都特别强。我见过一个项目,核心流程几乎全是事件驱动的,新增功能只需要加监听器,老代码一行都不用动。这才是真正的「对扩展开放,对修改关闭」。

好了,观察者模式的内容就讲到这里。从基础理论到自定义实现,从 Guava EventBus 到消息队列,你应该能感受到:设计模式不是死板的模板,而是一种思维方式。理解了它的本质,你就能在不同的场景下灵活运用。


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