7. 观察者模式(下):自定义事件监听器、Guava EventBus实战、观察者模式在消息队列中的应用
好,咱们接着聊观察者模式的下半场。
上一章我们把观察者模式的理论和基础实现讲透了。但说实话,工作中你很少会手写一个观察者模式——除非你在造轮子。更多时候,我们会用现成的框架,或者把它跟消息队列结合起来。
这一章,我带你看看观察者模式在真实项目里的三种玩法:自定义事件监听器、Guava EventBus、以及消息队列中的观察者。
7.1 自定义事件监听器:自己动手,丰衣足食
先说说自定义事件监听器。这玩意儿其实不复杂,说白了就是观察者模式的一个变种。
我记得刚工作那会儿,接手一个老项目。里面有个模块,用户下单后要发邮件、发短信、更新库存、记录日志……所有逻辑都写在一个方法里。那代码,啧啧,一个方法上千行。后来我重构的时候,第一件事就是引入事件监听器。
怎么做?三步走:
- 定义事件——就是观察者模式里的「主题状态」
- 定义监听器接口——就是观察者
- 定义事件发布器——就是被观察者
来看代码:
// 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秒。用户可没这耐心。
所以,生产级的自定义事件监听器,一般会搭配线程池:
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)就行。 - 事件继承:监听父事件的方法,也能收到子事件。这个特性有时候很实用。
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);
}
}
你看,跟观察者模式的结构一模一样。只不过观察者(消费者)不在同一个进程里,甚至可以用不同的语言实现。
这里有个关键区别:消息队列是异步的、跨进程的、可靠的。而进程内的观察者模式,默认是同步的、同进程的、不可靠的(如果监听器抛异常,发布者可能感知不到)。
7.4 三种方案的对比与选型
说了这么多,到底该怎么选?我画了一张图,帮你理清思路:
我个人总结的选型原则:
- 单体应用、模块少:用自定义监听器就够了,别引入多余依赖
- 单体应用、模块多:上 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