本文是「设计模式实战解读」系列第四篇。系列文章统一按照 定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ 的结构展开,每篇聚焦一个模式讲透。
一句话定义
观察者模式(Observer):定义对象间一对多的依赖关系------当一个对象状态变化时,所有依赖它的对象自动收到通知并更新。也叫发布-订阅模式(Publish-Subscribe)。
归属:行为型模式。
一、没有观察者时的痛点
假设你在做一个流程引擎,流程实例状态变更(启动、完成、失败)后需要做很多事:
java
public class FlowExecutor {
public void onFlowComplete(FlowInstance instance) {
// 1. 更新数据库状态
flowDao.updateStatus(instance.getId(), "COMPLETED");
// 2. 发送通知给用户
notificationService.send(instance.getUserId(), "流程执行完成");
// 3. 记录审计日志
auditLogService.log(instance.getId(), "COMPLETED", System.currentTimeMillis());
// 4. 更新监控指标
metricsService.increment("flow.complete.count");
metricsService.recordTime("flow.execution.duration", instance.getDuration());
// 5. 触发下游流程(如果有)
if (instance.hasDownstream()) {
downstreamTrigger.trigger(instance.getDownstreamId());
}
// 6. 清理临时数据
tempDataCleaner.clean(instance.getId());
// 7. 发送 Webhook 回调
webhookService.callback(instance.getCallbackUrl(), instance.toJson());
// 还会继续增长...
}
}
问题:
- FlowExecutor 知道太多东西了------它直接依赖了通知、日志、监控、下游触发、Webhook 等七八个服务
- 每加一个后置动作都要改 FlowExecutor------违反开闭原则
- 某个后置动作失败会影响整条链路------notificationService 超时,后面的全部阻塞
- 无法灵活配置------某些流程不需要 Webhook 回调,但代码里硬编码了
- 单元测试极其困难------要 mock 七八个依赖
核心诉求:发布者只负责"事情发生了",不关心"谁想知道"、"知道了要干什么"。
二、模式结构
┌────────────────────────────┐
│ Subject (被观察者/发布者) │
├────────────────────────────┤
│ - observers: List<Observer>│
├────────────────────────────┤
│ + addObserver(observer) │
│ + removeObserver(observer) │
│ + notifyAll(event) │
└──────────┬─────────────────┘
│ 通知
↓
┌──────────────────────┐ ┌──────────────────────┐
│ Observer A (观察者) │ │ Observer B (观察者) │
├──────────────────────┤ ├──────────────────────┤
│ + onEvent(event) │ │ + onEvent(event) │
└──────────────────────┘ └──────────────────────┘
发布者和观察者之间是松耦合的------
发布者不知道具体有哪些观察者,只知道观察者实现了统一接口。
三、核心实现
3.1 手写基础版
java
// 事件定义
public class FlowEvent {
private final String flowId;
private final String status; // STARTED / COMPLETED / FAILED
private final long timestamp;
private final Map<String, Object> context;
// constructor、getter 省略
}
// 观察者接口
public interface FlowEventListener {
void onEvent(FlowEvent event);
// 是否关心这个事件(过滤用)
default boolean supports(FlowEvent event) {
return true;
}
}
// 事件发布器(被观察者)
public class FlowEventPublisher {
private final List<FlowEventListener> listeners = new CopyOnWriteArrayList<>();
public void addListener(FlowEventListener listener) {
listeners.add(listener);
}
public void removeListener(FlowEventListener listener) {
listeners.remove(listener);
}
public void publish(FlowEvent event) {
for (FlowEventListener listener : listeners) {
if (listener.supports(event)) {
try {
listener.onEvent(event);
} catch (Exception e) {
// 单个观察者异常不影响其他观察者
log.error("Listener {} failed", listener.getClass().getSimpleName(), e);
}
}
}
}
}
3.2 具体观察者实现
java
// 观察者1:更新数据库状态
@Component
public class FlowStatusUpdater implements FlowEventListener {
@Override
public void onEvent(FlowEvent event) {
flowDao.updateStatus(event.getFlowId(), event.getStatus());
}
}
// 观察者2:发送通知
@Component
public class FlowNotificationListener implements FlowEventListener {
@Override
public void onEvent(FlowEvent event) {
if ("COMPLETED".equals(event.getStatus()) || "FAILED".equals(event.getStatus())) {
notificationService.send(event.getUserId(), buildMessage(event));
}
}
@Override
public boolean supports(FlowEvent event) {
// 只关心完成和失败事件
return "COMPLETED".equals(event.getStatus()) || "FAILED".equals(event.getStatus());
}
}
// 观察者3:监控指标
@Component
public class FlowMetricsListener implements FlowEventListener {
@Override
public void onEvent(FlowEvent event) {
metricsService.increment("flow." + event.getStatus().toLowerCase() + ".count");
}
}
// 观察者4:审计日志
@Component
public class FlowAuditListener implements FlowEventListener {
@Override
public void onEvent(FlowEvent event) {
auditLogService.log(event.getFlowId(), event.getStatus(), event.getTimestamp());
}
}
3.3 改造后的 FlowExecutor
java
public class FlowExecutor {
private final FlowEventPublisher eventPublisher;
public void onFlowComplete(FlowInstance instance) {
// 只做一件事:发布事件
eventPublisher.publish(new FlowEvent(
instance.getId(),
"COMPLETED",
System.currentTimeMillis(),
instance.getContext()
));
}
}
从依赖 7 个服务变成依赖 1 个 EventPublisher------发布者彻底解耦。新增后置逻辑只需加一个 Listener 实现,不改 FlowExecutor。
3.4 结合 Spring 的事件机制
Spring 自带了观察者模式的基础设施:
java
// 事件定义
public class FlowCompletedEvent extends ApplicationEvent {
private final String flowId;
private final String status;
public FlowCompletedEvent(Object source, String flowId, String status) {
super(source);
this.flowId = flowId;
this.status = status;
}
}
// 发布事件(一行代码)
@Component
public class FlowExecutor {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void onFlowComplete(FlowInstance instance) {
eventPublisher.publishEvent(
new FlowCompletedEvent(this, instance.getId(), "COMPLETED")
);
}
}
// 监听事件(注解驱动,零配置注册)
@Component
public class FlowMetricsListener {
@EventListener
public void handle(FlowCompletedEvent event) {
metricsService.increment("flow.complete.count");
}
}
@Component
public class FlowNotificationListener {
@EventListener(condition = "#event.status == 'FAILED'")
public void handleFailed(FlowCompletedEvent event) {
notificationService.send(event.getFlowId(), "流程执行失败");
}
}
// 异步监听(不阻塞主流程)
@Component
public class FlowAuditListener {
@Async
@EventListener
public void handle(FlowCompletedEvent event) {
auditLogService.log(event.getFlowId(), event.getStatus());
}
}
Spring 事件机制的优势:
- 不需要手动注册观察者(自动发现 @EventListener)
- 支持条件过滤(
conditionSpEL 表达式) - 支持异步执行(
@Async) - 支持事务绑定(
@TransactionalEventListener)
四、真实应用场景
4.1 框架级应用
| 框架/技术 | 被观察者 | 事件 | 观察者 |
|---|---|---|---|
| Spring | ApplicationContext | ContextRefreshedEvent | 各种 Listener |
| Spring Boot | SpringApplication | ApplicationStartedEvent | 自动配置模块 |
| Java Swing | JButton | ActionEvent | ActionListener |
| DOM (前端) | HTMLElement | click/input | addEventListener |
| Vue.js | 响应式数据 | 数据变化 | 视图更新函数 |
| Redis | Key 空间 | 过期/修改事件 | 订阅者 |
| RocketMQ | Topic | 消息发布 | Consumer Group |
4.2 业务场景
| 业务事件 | 谁发布 | 谁订阅(干什么) |
|---|---|---|
| 用户注册 | UserService | 发欢迎邮件、初始化配额、推送到 CRM |
| 流程完成 | FlowExecutor | 通知用户、记日志、更新指标、触发下游 |
| 订单支付成功 | PaymentService | 更新订单状态、减库存、发短信、记账 |
| 配置变更 | NacosClient | 刷新 Bean、清缓存、重建连接池 |
| 连接器授权更新 | OAuthService | 刷新 Token 缓存、通知使用方、记审计日志 |
| 节点执行失败 | NodeExecutor | 重试调度、告警通知、快照写入 |
4.3 iPaaS 流程引擎中的事件驱动
在数环通 iPaaS 引擎中,流程执行状态变更是典型的事件驱动场景:
FlowExecutionEngine(发布者)
├── 发布: FlowStartedEvent
│ ├── 监控 Listener: 记录开始时间
│ └── 限流 Listener: 检查并发配额
│
├── 发布: NodeExecutedEvent
│ ├── 日志 Listener: 持久化节点执行日志
│ └── 进度 Listener: 更新流程执行进度
│
├── 发布: FlowCompletedEvent
│ ├── 通知 Listener: 回调用户 Webhook
│ ├── 指标 Listener: 记录执行时长
│ └── 清理 Listener: 释放临时资源
│
└── 发布: FlowFailedEvent
├── 恢复 Listener: 写入恢复快照
├── 告警 Listener: 发送失败通知
└── 重试 Listener: 判断是否自动重试
引擎本身不关心"流程完成后要做什么"------它只负责把事件丢出去。所有后置逻辑通过 Listener 独立实现,可以按环境按租户灵活配置。
五、常见变种
5.1 同步 vs 异步观察者
java
// 同步(默认):publish 方法内逐个调用观察者,阻塞直到全部完成
publisher.publish(event); // 所有 listener 执行完才返回
// 异步:每个观察者在独立线程中执行
public class AsyncEventPublisher {
private final ExecutorService executor = Executors.newFixedThreadPool(4);
public void publish(FlowEvent event) {
for (FlowEventListener listener : listeners) {
executor.submit(() -> {
try {
listener.onEvent(event);
} catch (Exception e) {
log.error("Async listener failed", e);
}
});
}
}
}
选择依据:
- 同步:需要保证执行顺序、需要在同一事务内、需要知道是否全部成功
- 异步:后置逻辑耗时长、不影响主流程、允许最终一致性
5.2 事件总线(EventBus)
Google Guava 提供的 EventBus 是观察者模式的工业级实现:
java
// 创建事件总线
EventBus eventBus = new EventBus("flow-events");
// 异步版
AsyncEventBus asyncBus = new AsyncEventBus(executorService);
// 订阅(通过注解)
public class FlowMetricsSubscriber {
@Subscribe
public void onComplete(FlowCompletedEvent event) {
metricsService.increment("flow.complete");
}
}
// 注册
eventBus.register(new FlowMetricsSubscriber());
// 发布
eventBus.post(new FlowCompletedEvent(flowId));
5.3 跨进程观察者(消息队列)
进程内的观察者模式扩展到分布式------就是消息队列:
进程内观察者 跨进程观察者
─────────── ───────────
EventPublisher RocketMQ Producer
↓ publish() ↓ send(topic, msg)
List<Listener> Consumer Group
↓ onEvent() ↓ consumeMessage()
从架构上看,RocketMQ / Kafka / RabbitMQ 本质就是跨进程的观察者模式。区别在于:
- 进程内:低延迟、强一致、无持久化
- 跨进程:高可用、可持久化、支持重试/死信
5.4 响应式流(Reactive Streams)
当事件是连续流时(如实时数据推送),观察者模式演变为响应式编程:
java
// Project Reactor
Flux<FlowEvent> eventStream = flowEventSource.stream();
eventStream
.filter(e -> "FAILED".equals(e.getStatus()))
.buffer(Duration.ofSeconds(10)) // 每10秒批量处理
.subscribe(batch -> alertService.batchAlert(batch));
六、优缺点
| 优点 | 缺点 |
|---|---|
| 发布者和订阅者松耦合 | 通知顺序不确定(同步模式下按注册顺序) |
| 符合开闭原则(加订阅者不改发布者) | 观察者太多时性能下降 |
| 支持动态注册/注销 | 调试困难(事件链路不直观) |
| 天然适配事件驱动架构 | 可能导致内存泄漏(忘了注销) |
| 可以轻松切换同步/异步 | 循环依赖难以发现(A 通知 B,B 又通知 A) |
七、避坑指南
坑 1:观察者异常导致后续观察者不执行
java
// 错误写法
public void publish(Event event) {
for (Listener l : listeners) {
l.onEvent(event); // 如果这里抛异常,后面的 listener 全部跳过
}
}
// 正确写法
public void publish(Event event) {
for (Listener l : listeners) {
try {
l.onEvent(event);
} catch (Exception e) {
log.error("Listener {} error", l.getClass().getSimpleName(), e);
// 继续执行下一个
}
}
}
坑 2:观察者持有发布者引用导致内存泄漏
匿名内部类/Lambda 会隐式持有外部类引用。如果发布者生命周期长(如全局单例)、观察者生命周期短(如请求级对象),忘了 remove 会导致内存泄漏。
防御:用弱引用存储观察者,或者严格配对 add/remove。
坑 3:同步观察者阻塞主流程
一个慢观察者(比如发邮件耗时 3 秒)会把整个 publish 调用阻塞 3 秒。
防御 :耗时操作的观察者必须异步化(@Async 或独立线程池)。
坑 4:事件风暴
观察者内部又发布了新事件,导致递归/循环通知------最终 StackOverflow 或者 CPU 打满。
java
// 危险:观察者内部又触发了事件
@EventListener
public void handle(FlowCompletedEvent event) {
// 处理完成后发布新事件
publisher.publishEvent(new AuditEvent(...)); // AuditEvent 的监听者又发布...
}
防御:事件链路最多 2-3 层,禁止循环发布。可以加深度计数器或者用异步打断循环。
坑 5:Spring @EventListener 在事务提交前执行
默认的 @EventListener 是在事务提交前执行的。如果你在 Listener 里读数据库查刚 INSERT 的数据,可能查不到。
防御 :用 @TransactionalEventListener(phase = AFTER_COMMIT) 确保事务提交后再执行。
坑 6:观察者顺序依赖
如果多个观察者之间有执行顺序要求(A 必须在 B 之前),不要依赖注册顺序------这不可靠。
防御 :用 @Order 注解(Spring)显式指定顺序,或者把有序逻辑放在同一个观察者内。
八、观察者模式 vs 发布-订阅模式
这两个术语经常混用,但有微妙区别:
| 维度 | 观察者模式 | 发布-订阅模式 |
|---|---|---|
| 耦合度 | 发布者知道观察者接口 | 发布者和订阅者完全不知道对方 |
| 中间层 | 无(直接通知) | 有(EventBus / Broker) |
| 典型实现 | Java Observer、DOM Event | RocketMQ、Redis PubSub、EventBus |
| 通信 | 进程内同步 | 可跨进程异步 |
| 过滤 | 观察者自己过滤 | 中间层按 Topic 过滤 |
在实际讨论中,两者经常等同使用,不必过于纠结概念边界。
九、常见问题(FAQ)
Q:观察者模式和回调有什么区别?
A:回调是"一对一"(调用方注册一个回调函数),观察者是"一对多"(一个事件通知多个监听者)。回调适合"完成通知"场景,观察者适合"广播通知"场景。
Q:什么时候应该用消息队列代替进程内观察者?
A:三个信号:① 观察者需要跨服务(不在同一个 JVM);② 观察者需要可靠重试(进程内异常就丢了);③ 发布速率远大于消费速率(需要削峰)。满足任一条就应该上 MQ。
Q:观察者模式会影响性能吗?
A:同步模式下,N 个观察者的总耗时 = 各观察者耗时之和。如果观察者多且部分耗时,会显著拖慢 publish。解法:① 异步化耗时观察者;② 减少不必要的观察者(用 supports() 过滤);③ 批量通知代替逐条通知。
Q:Spring 的 @EventListener 是异步的吗?
A:默认是同步的(在 publishEvent 调用线程中执行)。要异步需要额外加 @Async + 开启 @EnableAsync。注意异步后事件的事务绑定和异常处理策略都需要调整。
Q:如何测试观察者?
A:① 单测观察者本身(传入 mock 事件,验证行为);② 集成测试发布→监听链路(发布事件,验证观察者的副作用)。不要把发布者和所有观察者放在一个测试里------那就变成集成测试了。
Q:事件溯源(Event Sourcing)和观察者模式的关系?
A:事件溯源把"状态变更"记录为不可变的事件序列,通过重放事件恢复状态。观察者模式是事件溯源的"通知层"------事件产生后,由观察者消费并产生副作用。两者是互补关系:事件溯源管"记录",观察者管"响应"。
十、小结
观察者模式的核心价值:把"事情发生了"和"要对此做什么"彻底解耦。
三个实践要点:
- 用 Spring @EventListener 而不是手写 Observer------框架帮你管理注册、支持条件过滤和异步
- 每个观察者用 try-catch 包裹------一个失败不能影响其他
- 耗时操作必须异步------否则慢观察者会拖垮整个事件链
设计模式不是银弹,但观察者模式几乎是所有事件驱动系统的基石。理解它,就理解了从 DOM 事件到消息队列到响应式编程的底层统一模型。
标签:#设计模式 #观察者模式 #Observer #发布订阅 #事件驱动 #EventListener #Spring #EventBus #Java #行为型模式 #解耦 #消息队列 #面向对象 #软件工程