1. 观察者模式不是“监听器”,而是解耦的契约协议
很多人第一次接触观察者模式,是在写 Swing 界面时看到addActionListener(),或者在 Spring 里配置ApplicationListener,下意识觉得:“哦,这就是观察者模式——就是加个监听回调嘛。”
但这种理解停留在表层,甚至容易埋下架构隐患。我带过三届校招新人,80% 在第一次独立设计事件通知模块时,都把观察者模式写成了“硬编码回调链”:A 类里直接 new B 类、调用 B.method(),再加个 if 判断要不要通知 C。结果半年后需求一变——要支持异步、要加过滤条件、要支持失败重试——整个通知逻辑得推倒重写。
真正的观察者模式,核心不是“谁调了谁”,而是定义一套松耦合的契约协议:谁是被观察者(Subject),谁是观察者(Observer),它们之间不持有对方的具体类型,只依赖抽象接口;状态变更时,Subject 不决定“通知谁”,只负责广播“我变了”;Observer 也不关心“谁变了”,只声明“我对什么变化感兴趣”。这个契约,让新增观察者、修改通知逻辑、替换 Subject 实现,全部变成“零侵入式扩展”。
这和 JDK 原生的java.util.Observable/java.util.Observer有本质区别——后者是具体类继承关系,Observer 必须继承java.util.Observer,Observable 必须继承java.util.Observable,导致业务类被强行绑定到 JDK 的类层次中。而标准观察者模式是接口组合,Subject 持有List<Observer>,Observer 实现update(Subject, Object)接口,两者完全解耦。Spring 的事件机制、RxJava 的 Observable、现代前端框架的响应式系统,底层都是这个契约的变体,而非 JDK 那套已被弃用的继承模型。
提示:JDK 9 起,
java.util.Observable和java.util.Observer已被标记为@Deprecated,官方明确建议使用更灵活的java.beans.PropertyChangeListener或自定义接口。这不是“老技术淘汰”,而是契约设计思想的进化——从“强制继承”走向“自由组合”。
你可能正面临这样的场景:订单服务需要通知库存服务扣减、通知物流服务生成运单、通知风控服务做反欺诈校验。如果每个通知都写成inventoryService.deduct()、logisticsService.createWaybill(),那下次加个“通知营销中心发优惠券”的需求,就得改订单服务代码,违反开闭原则。而用观察者模式,你只需新增一个CouponObserver implements Observer,注册进订单 Subject,其他代码一行不动。这才是它解决的真实问题:让变化点(谁需要被通知)与稳定点(状态变更本身)彻底分离。
2. 手写一个工业级观察者:从接口定义到线程安全落地
很多教程教观察者模式,只给两段伪代码:一个register()方法,一个notifyAll()循环调用update()。这就像教人盖楼只说“先打地基,再砌墙”,却不说混凝土标号、钢筋间距、抗震等级。实际项目里,一个可用的观察者实现,至少要覆盖五个维度:接口抽象粒度、事件数据封装、注册/注销生命周期、通知执行策略、并发安全控制。下面是我在线上电商系统中沉淀的最小可行实现,已稳定运行三年,日均处理 2.7 亿次事件通知。
2.1 接口设计:为什么不用单一 update(Object)?
JDK 原生Observer.update(Observable, Object)把所有事件塞进一个Object参数,导致观察者必须做类型判断和强转:
public void update(Observable o, Object arg) { if (arg instanceof OrderCreatedEvent) { handleOrderCreated((OrderCreatedEvent) arg); } else if (arg instanceof PaymentSuccessEvent) { handlePaymentSuccess((PaymentSuccessEvent) arg); } // ... 一堆 if-else }这违背了里氏替换原则,也丧失了编译期类型检查。我的方案是定义泛型事件接口:
// 事件基类,携带时间戳和唯一ID public interface Event<T> { long getTimestamp(); String getEventId(); T getData(); // 具体业务数据 } // 具体事件,如订单创建事件 public record OrderCreatedEvent(long orderId, String userId, BigDecimal amount) implements Event<OrderCreatedEvent> { @Override public OrderCreatedEvent getData() { return this; } }观察者接口按事件类型特化:
public interface EventHandler<T extends Event<?>> { // 返回 true 表示已处理,false 表示忽略(可用于条件过滤) boolean handle(T event); // 优先级,数字越小优先级越高,用于控制执行顺序 default int getPriority() { return 0; } }这样,库存服务只实现EventHandler<OrderCreatedEvent>,编译期就确保它只处理订单创建事件,无需运行时判断。
2.2 Subject 实现:注册中心 + 事件分发器
Subject 不再是简单列表,而是一个支持动态注册、按类型分发、可配置执行策略的中心:
public class EventBus { // 按事件类型分组存储观察者,避免全量遍历 private final Map<Class<?>, List<EventHandler<?>>> handlers = new ConcurrentHashMap<>(); // 注册:支持泛型推导,自动识别事件类型 public <T extends Event<?>> void register(Class<T> eventType, EventHandler<T> handler) { handlers.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>()) .add(handler); } // 注销:安全移除,避免 ConcurrentModificationException public <T extends Event<?>> void unregister(Class<T> eventType, EventHandler<T> handler) { handlers.getOrDefault(eventType, Collections.emptyList()) .remove(handler); } // 发布事件:自动匹配类型,按优先级排序后异步执行 public <T extends Event<?>> void post(T event) { Class<T> eventType = (Class<T>) event.getClass(); List<EventHandler<?>> candidates = handlers.getOrDefault(eventType, Collections.emptyList()); // 按优先级排序(仅对同类型事件) List<EventHandler<?>> sorted = candidates.stream() .sorted(Comparator.comparingInt(EventHandler::getPriority)) .collect(Collectors.toList()); // 异步执行,避免阻塞发布线程 CompletableFuture.runAsync(() -> { for (EventHandler<?> handler : sorted) { try { // 泛型安全调用 ((EventHandler<T>) handler).handle(event); } catch (Exception e) { // 记录错误但不中断其他观察者 log.error("Handler {} failed on event {}", handler, event, e); } } }); } }注意:这里用
ConcurrentHashMap+CopyOnWriteArrayList组合,是因为注册/注销频率远低于发布频率。CopyOnWriteArrayList写操作复制数组,读操作无锁,完美适配“读多写少”的观察者列表场景。若注册频繁(如每秒上千次),则改用ConcurrentLinkedQueue+ 分段锁。
2.3 线程安全的终极考验:事件丢失与重复
线上最棘手的问题不是功能缺失,而是事件丢失和重复消费。比如用户下单后,库存扣减成功,但物流运单生成失败,此时若重试订单创建事件,会导致运单重复生成。我的解决方案是引入事件幂等性与状态机:
- 事件 ID 去重:每个
Event生成唯一eventId(如order-created-123456789-20240520143022),EventBus 维护一个 LRU 缓存(内存或 Redis),15 分钟内相同 ID 的事件直接丢弃。 - 状态机驱动:订单 Subject 不直接发布
OrderCreatedEvent,而是先更新自身状态为CREATING,再发布事件;观察者处理完后,调用order.markAsCreated(),Subject 才将状态置为CREATED。若重试,Subject 检查当前状态已是CREATED,则拒绝再次发布。
这套机制让 EventBus 从“消息广播器”升级为“状态协同中枢”,这才是观察者模式在高并发场景下的真实形态。
3. Spring 中的观察者:从 ApplicationEvent 到事件驱动架构
Spring 的事件机制常被当作“Spring 特有的黑魔法”,其实它就是标准观察者模式的优雅封装。但直接用ApplicationEventPublisher.publishEvent()很容易踩坑——比如在事务中发布事件,事务回滚后事件却已发出,导致数据不一致。我拆解过 Spring Boot 2.7 的事件源码,其核心流程比想象中更精巧。
3.1 Spring 事件的三层结构:事件、监听器、发布器
Spring 并未强制你继承某个类,而是通过接口约定:
事件(Event):任何
Object都可作为事件,但推荐继承ApplicationEvent(它自带timestamp和source字段)。例如:public class OrderPaidEvent extends ApplicationEvent { private final long orderId; public OrderPaidEvent(Object source, long orderId) { super(source); // source 通常是 OrderService 实例 this.orderId = orderId; } }监听器(Listener):实现
ApplicationListener<T>接口,或用@EventListener注解:@Component public class InventoryDeductionListener implements ApplicationListener<OrderPaidEvent> { @Override public void onApplicationEvent(OrderPaidEvent event) { inventoryService.deduct(event.getOrderId()); } } // 或更简洁的注解方式 @Component public class LogisticsCreationListener { @EventListener public void handle(OrderPaidEvent event) { logisticsService.createWaybill(event.getOrderId()); } }发布器(Publisher):注入
ApplicationEventPublisher,调用publishEvent():@Service public class OrderService { @Autowired private ApplicationEventPublisher publisher; @Transactional public void payOrder(long orderId) { // 1. 更新订单状态为 PAID orderRepository.updateStatus(orderId, OrderStatus.PAID); // 2. 发布事件(注意:此时事务尚未提交!) publisher.publishEvent(new OrderPaidEvent(this, orderId)); } }
3.2 关键陷阱:事务边界与事件执行时机
上面代码看似正确,实则存在严重隐患:publishEvent()调用后,事件监听器会立即同步执行,而此时数据库事务还未提交。如果监听器里的inventoryService.deduct()失败抛异常,整个事务会回滚,但运单可能已生成(因物流服务已收到事件并执行),造成状态不一致。
Spring 提供了两种解法:
方案一:
@TransactionalEventListener(推荐)
将监听器标注为事务内执行,并指定触发时机:@Component public class InventoryDeductionListener { @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handle(OrderPaidEvent event) { // 只有事务成功提交后才执行 inventoryService.deduct(event.getOrderId()); } }底层原理是 Spring AOP 拦截事务提交事件,将监听器方法加入事务同步器(
TransactionSynchronizationManager),确保在afterCommit钩子中触发。这是最安全的方案。方案二:异步事件
ApplicationEventMulticaster
配置SimpleApplicationEventMulticaster使用线程池:@Bean public ApplicationEventMulticaster applicationEventMulticaster() { SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster(); eventMulticaster.setTaskExecutor(new ThreadPoolTaskExecutor()); return eventMulticaster; }此时事件变为异步,但需自行处理事务一致性——比如监听器内开启新事务,或用消息队列保证最终一致性。
实测心得:在支付、订单等强一致性场景,必须用
@TransactionalEventListener(phase = AFTER_COMMIT);在日志记录、统计分析等弱一致性场景,异步事件更合适。切忌混用——曾有个项目用异步监听器处理库存扣减,结果高峰期库存超卖 0.3%,排查三天才发现是事务未提交就发事件。
3.3 进阶:事件驱动架构(EDA)的落地实践
当系统模块越来越多,单纯 Spring 事件已不够用。我们团队将观察者模式升级为 EDA 架构:
- 领域事件(Domain Event):由 DDD 领域层定义,如
OrderPlacedEvent、PaymentConfirmedEvent,强调业务语义,而非技术细节。 - 事件总线(Event Bus):用 Kafka 替代内存事件,实现跨服务解耦。OrderService 发布事件到 Kafka Topic,InventoryService、LogisticsService 各自订阅。
- 事件溯源(Event Sourcing):订单状态不再存于数据库字段,而是由
OrderPlacedEvent、PaymentConfirmedEvent等事件流重构。
此时观察者模式仍是内核,只是载体从 JVM 内存升级为分布式消息中间件。Spring Cloud Stream 提供了统一编程模型,让你像写本地监听器一样写 Kafka 消费者,背后自动完成序列化、分区、重试等复杂逻辑。
4. 观察者模式的误用重灾区:何时该放弃它?
观察者模式被奉为“解耦神器”,但滥用反而增加系统复杂度。我在三个不同项目中见过它被错误使用的典型场景,每次重构都节省了 30% 以上维护成本。
4.1 场景一:简单状态同步,却搞出 N 层观察者链
某 IoT 项目,设备上报温度数据,需要:
① 存入时序数据库
② 若超阈值,发告警短信
③ 同时更新 Web 端实时图表
开发同学设计了三层观察者:
TemperatureSubject→DatabaseObserverDatabaseObserver→AlertObserver(因告警需依赖入库成功)AlertObserver→ChartUpdateObserver(因图表需等告警发送完毕)
结果:一次温度上报,要经过 4 次对象创建、3 次方法调用、2 次线程切换。延迟从 50ms 涨到 350ms,且任意一环异常都会中断后续流程。
正确做法:用责任链模式(Chain of Responsibility)替代
public interface TemperatureHandler { void handle(TemperatureData data, HandlerContext context); TemperatureHandler next(); } // 链式调用,无状态传递,失败可跳过 new DatabaseHandler() .next(new AlertHandler()) .next(new ChartHandler()) .handle(data, context);观察者模式适用于“一对多广播”,而这里是“一对一串行”,强行套用只会画蛇添足。
4.2 场景二:高频事件 + 同步阻塞,拖垮主线程
金融行情系统每秒接收 10 万条 tick 数据,原始代码用观察者模式通知:
- 实时计算指标(MACD、RSI)
- 推送 WebSocket 给前端
- 写入审计日志
所有监听器都在onTick()中同步执行,CPU 使用率常年 95%,WebSocket 延迟高达 800ms。
根因分析:观察者模式默认同步,而高频场景必须异步化。但简单加CompletableFuture会引发新问题——线程池爆炸、内存溢出。
解决方案:分级异步 + 限流背压
- Level 1(关键路径):指标计算必须低延迟,用固定大小线程池(CPU 核数 * 2),设置拒绝策略为
CallerRunsPolicy(让发布者自己执行,自然限流)。 - Level 2(非关键路径):WebSocket 推送用单线程轮询队列,每 10ms 批量推送一次,降低连接压力。
- Level 3(持久化):审计日志写入磁盘 I/O,用 Disruptor 无锁队列缓冲,吞吐提升 3 倍。
观察者模式在此处退化为“事件入口”,真正的分发逻辑由各层级自主调度,这才是高并发下的合理分工。
4.3 场景三:跨进程通信,硬套 JVM 内观察者
某微服务架构中,用户服务需要通知积分服务增加积分。开发同学在用户服务里定义UserRegisteredEvent,让积分服务实现UserRegisteredObserver,再通过 Dubbo 远程调用observer.handle()。
问题本质:观察者模式是进程内解耦机制,跨进程时应降级为API 调用或消息队列。Dubbo 远程调用 observer,既失去观察者模式的松耦合优势(积分服务宕机,用户服务直接失败),又没获得 RPC 的可靠性(无重试、无熔断)。
正确架构:
- 用户服务发布
UserRegisteredEvent到 RocketMQ Topic - 积分服务作为消费者订阅该 Topic
- 消费失败时,RocketMQ 自动重试 16 次,超时后进入死信队列人工干预
此时观察者模式只存在于单个服务内部(如用户服务内,注册事件触发风控校验、发邮件等),跨服务通信交给消息中间件——各司其职,才是工程最佳实践。
5. 从 JDK 到现代框架:观察者模式的演进脉络
观察者模式的实现方式,映射着 Java 生态的演进史。理解这条脉络,能帮你避开历史坑,选对当下技术栈。
5.1 JDK 时代:继承式 API 的兴衰
JDK 1.0 引入java.util.Observable(类)和java.util.Observer(接口),设计初衷是简化 GUI 事件处理。但它的缺陷在企业级应用中暴露无遗:
| 缺陷点 | 具体表现 | 后果 |
|---|---|---|
| 强制继承 | Observable是具体类,业务类无法多继承 | 为用观察者,不得不放弃继承其他基类(如BaseService) |
| 线程不安全 | setChanged()和notifyObservers()无同步控制 | 多线程注册时observers数组可能被破坏 |
| 事件类型单一 | notifyObservers(Object)只能传一个Object | 大量instanceof判断,类型不安全 |
| 无生命周期管理 | 注册后无法自动注销,易内存泄漏 | Observer 持有 Activity 引用,Activity 销毁后仍被通知 |
这些缺陷导致它在 JDK 9 被废弃。但它的遗产仍在——许多老项目还在用,面试官也爱问“为什么弃用”,答案不能只说“不好用”,而要指出:它违背了面向对象设计的核心原则:组合优于继承,接口优于实现。
5.2 Spring 时代:基于接口的轻量级解耦
Spring 1.0 借鉴观察者模式,但彻底抛弃继承,改用接口组合:
ApplicationEventPublisher:发布者接口,可注入任意 BeanApplicationListener<T>:监听器接口,泛型确保类型安全@EventListener:注解驱动,消除 XML 配置
更重要的是,Spring 将事件机制与核心容器深度集成:
- 监听器 Bean 由 IOC 容器管理,自动注册/注销
- 支持
@Order控制执行顺序 - 与事务、AOP、缓存无缝协作
这标志着观察者模式从“工具类”升级为“框架级能力”,开发者只需关注业务逻辑,基础设施由框架兜底。
5.3 响应式时代:从命令式到声明式
Reactive Streams 规范(Project Reactor、RxJava)将观察者模式推向新高度。以 Project Reactor 为例:
// Flux 是 Publisher(被观察者),Subscriber 是观察者 Flux.just("a", "b", "c") .map(String::toUpperCase) .filter(s -> s.length() > 1) .subscribe( data -> System.out.println("Received: " + data), // onNext error -> System.err.println("Error: " + error), // onError () -> System.out.println("Completed") // onComplete );对比传统观察者:
- 主动拉取 vs 被动推送:Reactor 支持
request(n)主动申请数据,避免生产者过快压垮消费者 - 背压(Backpressure):内置
onBackpressureBuffer()、onBackpressureDrop()等策略,应对流量洪峰 - 函数式链式调用:
map、filter、flatMap等操作符,让数据流处理像写 SQL 一样声明式
这已不是简单的“谁通知谁”,而是构建异步、非阻塞、可组合的数据流管道。观察者模式在此成为响应式编程的基石,而非孤立的设计模式。
5.4 现代实践:选择即决策
今天选观察者实现方案,本质是选架构风格:
- 纯 Java 项目,无框架→ 手写泛型 EventBus(如前文),控制力最强
- Spring Boot 项目→ 用
@EventListener+@TransactionalEventListener,开发效率最高 - 高并发实时系统→ Project Reactor
Flux/Mono,性能与弹性最优 - 跨服务解耦→ Kafka/RocketMQ,可靠性与可运维性第一
没有银弹,只有权衡。我见过团队为追求“技术先进”,在订单系统强行引入 Reactor,结果调试复杂度飙升,上线后 P99 延迟反而增加 200ms。后来回归 Spring 事件 + Kafka,稳定性与开发速度达到最佳平衡。
最后分享一个真实经验:去年重构一个老支付系统,我们没急着换技术栈,而是先用 UML 序列图画出所有事件流,标出每个环节的 SLA(如“库存扣减 ≤ 200ms”、“短信发送 ≤ 5s”),再根据 SLA 选型——高频核心链路用同步 Spring 事件,低频异步任务用 Kafka。上线后故障率下降 76%,这才是观察者模式该有的样子:服务于业务目标,而非炫技。