1. 观察者模式到底在解决什么问题?——从“改一处,崩一片”说起
观察者模式不是Java里一个冷门的API调用,它本质上是解耦事件通知与业务逻辑的通用契约。我带过三届校招新人,几乎所有人第一次写订单系统时都会掉进同一个坑:用户下单后,要发短信、更新库存、记录日志、触发风控、通知物流……这些操作全堆在orderService.createOrder()方法里。结果呢?产品经理说“短信模板要换”,你得改这个方法;运营说“风控规则要加白名单”,你又得改这个方法;运维说“日志太重影响性能”,你还得改这个方法。最后这个方法长达300行,每次上线前心跳加速,生怕动错一行就导致支付失败——这不是代码能力问题,是缺乏对变化边界的认知。
观察者模式就是为这种场景而生的。它把“谁该被通知”和“通知后做什么”彻底分开。核心就三句话:
- 被观察者(Subject):只管“发生了什么事”,比如“订单已创建”“库存已扣减”“用户已注销”;
- 观察者(Observer):只管“我听到这件事后要干什么”,比如“发短信”“写ES日志”“调物流接口”;
- 注册机制:观察者主动向被观察者登记自己关心哪些事件,而不是被观察者硬编码调用一堆服务。
你可能马上想到Spring里的ApplicationEventPublisher,但别急——这恰恰说明观察者模式早已不是教科书里的概念,而是现代Java工程的呼吸器官。Spring Boot启动时发布ContextRefreshedEvent,MyBatis自动注册监听器初始化Mapper;用户登录成功后发布AuthenticationSuccessEvent,安全模块、审计模块、统计模块各自响应,互不干扰。这种松耦合不是靠运气,而是靠模式约束出来的结构纪律。
为什么JDK原生提供了java.util.Observable和java.util.Observer却几乎没人用?因为它们设计得太“瘦”——没有线程安全保证、不支持泛型、事件类型只能是Object、注册/移除观察者必须同步锁住整个对象。我2015年维护一个老系统时试过,光是处理并发下单时的观察者通知顺序问题,就写了两版重试逻辑。后来换成自定义接口+CopyOnWriteArrayList,代码量没增加,但稳定性直接从“每周重启一次”变成“连续97天零告警”。这不是技术炫技,是模式落地时必须面对的真实水位线:理论模型要经得起高并发、异常流、灰度发布、监控埋点的反复冲刷。
所以当你看到“设计模式期末”“设计模式大作业”这类热搜词,别只想着画UML图交差。真正值得花时间琢磨的是:如果现在让你重构一个电商秒杀系统的通知链路,你会怎么拆?短信服务要不要和风控服务共享同一个观察者实例?如果物流接口超时,该让观察者自己重试,还是由被观察者统一兜底?这些决策背后,才是观察者模式的血肉。
2. JDK原生实现的致命缺陷与重构逻辑——为什么Observable在生产环境等于“禁用”
2.1java.util.Observable的五个硬伤
先看一段典型的JDK原生用法:
public class OrderSubject extends Observable { public void createOrder(Order order) { // 业务逻辑... setChanged(); // 必须手动标记状态变更 notifyObservers(order); // 通知所有观察者 } }表面看很简洁,但实际踩坑记录比代码还长。我整理了团队三年内因Observable引发的线上故障,归结为五大硬伤:
状态机设计反直觉:
setChanged()必须在notifyObservers()前调用,且调用后内部changed标志位会自动清零。这意味着如果某个观察者执行耗时较长,期间其他线程调用createOrder(),setChanged()会被覆盖,导致部分观察者收不到通知。我们曾因此丢失过23%的风控日志,排查三天才发现是Observable的changed字段被并发覆盖。泛型缺失导致类型擦除灾难:
notifyObservers(Object arg)参数是Object,观察者收到后必须强制转型:public void update(Observable o, Object arg) { Order order = (Order) arg; // 运行时ClassCastException风险 }当系统接入新业务方,传入
String或Integer时,崩溃发生在深夜三点——而编译器全程沉默。线程安全形同虚设:
Observable的observers列表是Vector,虽然addObserver()等方法加了synchronized,但notifyObservers()内部遍历Vector时并未同步。JDK文档明确警告:“子类必须确保在调用notifyObservers时,观察者列表不被修改”。可现实是:风控模块在运行中动态启停策略,会调用deleteObserver(),和通知线程形成竞态。我们用jstack抓到过17次线程死锁,根源全是Vector的iterator()和removeElementAt()冲突。生命周期管理真空:没有提供
onDetach()或destroy()钩子。当Spring容器销毁Bean时,Observable不会自动清理注册的观察者。内存泄漏像慢性病——三个月后Full GC频率从每天2次升到每小时1次,最终OOM。jmap -histo显示OrderSubject持有上千个已失效的SmsObserver实例。事件传播不可控:
notifyObservers()是广播式通知,无法按条件过滤。比如“仅通知VIP用户的观察者”或“库存不足时跳过物流通知”,都得在每个观察者内部写if判断。结果就是10个观察者里有8个在做if (!order.isVip()) return;,既冗余又违背单一职责。
提示:JDK9开始已将
Observable标记为@Deprecated,官方文档明确建议“使用更现代的事件总线框架替代”。这不是版本迭代的偶然,而是对设计缺陷的正式盖章。
2.2 重构方案:轻量级接口定义 + 线程安全容器
我们团队的解决方案极其朴素:放弃继承,拥抱组合;放弃Vector,拥抱CopyOnWriteArrayList;放弃Object参数,拥抱泛型。核心接口只有三行:
public interface EventPublisher<T> { void register(EventHandler<T> handler); void unregister(EventHandler<T> handler); void publish(T event); } public interface EventHandler<T> { void handle(T event); }关键实现细节:
CopyOnWriteArrayList在publish()时复制快照,遍历时即使其他线程调用unregister()也不会ConcurrentModificationException;publish()方法内部加ReentrantLock控制注册/注销的临界区,但遍历通知时不加锁,性能提升3倍;- 事件类型
T由具体业务决定,OrderCreatedEvent、InventoryDeductedEvent各自独立,编译期类型安全。
对比数据:某次大促压测中,原Observable方案在QPS 5000时平均延迟127ms,错误率0.8%;重构后相同QPS下延迟降至23ms,错误率归零。这不是魔法,是把设计模式从“理论正确”推进到“工程可靠”的必然路径。
3. Spring生态下的观察者模式实战——从ApplicationEvent到事件驱动架构
3.1 Spring事件机制的三层抽象:为什么它能扛住百万级QPS
Spring的事件体系不是对JDKObservable的简单封装,而是构建了完整的事件生命周期管理模型。它的精妙之处在于分层解耦:
| 层级 | 组件 | 职责 | 典型问题 |
|---|---|---|---|
| 事件源 | ApplicationEventPublisher | 发布事件的入口,隐藏底层实现 | 不应包含业务逻辑 |
| 事件总线 | SimpleApplicationEventMulticaster | 事件分发中枢,支持异步、排序、过滤 | 默认同步阻塞,需显式配置线程池 |
| 事件处理器 | @EventListener标注的方法 | 具体业务逻辑,可声明式指定条件 | 方法签名必须匹配事件类型 |
我们重构支付回调系统时,发现旧架构在支付成功后串行调用6个下游服务,平均耗时2.8秒。引入Spring事件后,将PaymentSuccessEvent发布到总线,6个@EventListener方法并行执行,总耗时压缩至420ms。但这里有个关键陷阱:默认情况下所有监听器都在主线程执行。如果你没配置异步,那只是把串行改成“伪并行”——所有监听器仍在Web请求线程里排队跑。
解决方案是注入自定义ApplicationEventMulticaster:
@Bean public ApplicationEventMulticaster applicationEventMulticaster() { SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster(); eventMulticaster.setTaskExecutor(new ThreadPoolTaskExecutor() {{ setCorePoolSize(10); setMaxPoolSize(50); setQueueCapacity(1000); setThreadNamePrefix("event-handler-"); }}); return eventMulticaster; }注意:ThreadPoolTaskExecutor的queueCapacity必须设为正数,否则任务直接拒绝。我们曾因设为0导致大促时支付成功事件积压,最终Redis队列爆满。
3.2 条件化监听与事件过滤——让通知真正“按需送达”
真实业务中,90%的观察者不需要接收全部事件。比如风控模块只关心“高风险订单”,短信服务只关心“用户手机号有效”的订单。Spring提供了两种精准过滤方式:
方式一:@EventListener的condition属性
@EventListener(condition = "#event.order.amount > 10000") public void handleHighValueOrder(PaymentSuccessEvent event) { // 仅当订单金额超1万元时触发 }原理是SpEL表达式在事件发布时动态计算,#event指向事件对象。但要注意:SpEL解析有性能开销,高频事件(如用户点击)慎用。
方式二:自定义事件继承体系
public abstract class OrderEvent extends ApplicationEvent { protected final Order order; public OrderEvent(Object source, Order order) { super(source); this.order = order; } } public class HighRiskOrderEvent extends OrderEvent { ... } public class VipOrderEvent extends OrderEvent { ... }然后监听器只订阅特定子类:
@EventListener public void handleVipOrder(VipOrderEvent event) { ... } // 自动过滤非VIP事件这种方式零运行时开销,但要求事件发布方主动判断类型。我们采用混合策略:基础事件用继承过滤,动态条件用SpEL,兼顾性能与灵活性。
3.3 事件溯源与可靠性保障——当消息可能丢失时怎么办?
Spring事件是内存级的,应用重启后未处理事件永久丢失。对于支付、订单等强一致性场景,必须升级为持久化事件总线。我们的方案是:
- 所有关键事件先落库(MySQL
event_log表),状态为PENDING; - 启动定时任务扫描
PENDING事件,通过ApplicationEventPublisher发布; - 监听器处理成功后,事务内更新事件状态为
PROCESSED。
关键代码:
@Transactional public void publishWithPersistence(PaymentSuccessEvent event) { // 1. 插入事件记录 eventLogRepository.insert(new EventLog( UUID.randomUUID().toString(), "PaymentSuccessEvent", JSON.toJSONString(event), "PENDING" )); // 2. 发布事件(此时监听器可能失败) applicationEventPublisher.publishEvent(event); } @EventListener public void handlePaymentSuccess(PaymentSuccessEvent event) { try { // 核心业务逻辑... smsService.send(event.getOrder().getPhone(), "支付成功"); // 3. 更新事件状态(同一事务) eventLogRepository.updateStatus( event.getEventId(), "PROCESSED"); } catch (Exception e) { // 记录错误,触发告警 log.error("Event handling failed", e); throw e; // 事务回滚,事件状态保持PENDING } }这套机制让我们实现了99.999%的事件投递成功率。去年双11,支付系统峰值QPS 8万,事件积压最高达2300条,全部在12分钟内消化完毕,零人工干预。
4. 观察者模式的边界与误用警示——不是所有“通知”都该用它
4.1 三种典型误用场景及替代方案
观察者模式常被滥用为“万能胶水”,但过度使用反而制造新问题。以下是三个血泪教训:
误用一:用观察者替代简单方法调用
场景:用户注册后,需要发送欢迎邮件、初始化用户积分、创建默认地址。
错误做法:定义UserRegisteredEvent,三个监听器分别处理。
问题:三个操作强依赖、必须按序执行、任一失败需整体回滚。观察者模式天然不保证顺序和事务性。
正确方案:本地事务内顺序调用
@Transactional public User register(User user) { User saved = userRepository.save(user); emailService.sendWelcome(saved); pointService.initPoints(saved.getId()); addressService.createDefault(saved.getId()); return saved; }观察者适用于“弱一致性”场景(如日志、统计、通知),不适用于“强一致性”场景(如资金、库存)。
误用二:在循环中频繁发布事件
场景:批量导入10万条商品数据,每处理一条就发ProductImportedEvent。
错误后果:10万个事件对象瞬间占满堆内存,GC风暴导致服务假死。
正确方案:事件聚合 + 批量处理
// 批量导入时只发一次事件 @Scheduled(fixedDelay = 60000) public void processBatchEvents() { List<ProductImportBatchEvent> events = eventRepository.findPendingBatches(); for (ProductImportBatchEvent event : events) { // 批量处理1000条商品 handleBatch(event.getProductIds()); eventRepository.markProcessed(event.getId()); } }误用三:用观察者实现跨服务调用
场景:订单服务发布OrderCreatedEvent,库存服务监听并扣减库存。
致命问题:两个服务间形成隐式依赖,库存服务宕机导致订单事件堆积,最终MQ积压爆炸。
正确方案:明确的RPC契约 + 重试机制
// 订单服务主动调用库存服务 try { inventoryClient.deduct(order.getItems()); } catch (RpcException e) { // 重试3次,失败后降级为消息队列异步补偿 messageQueue.send(new InventoryCompensationMessage(order)); }观察者模式只适用于同一进程内、低延迟、高可靠的组件通信。跨服务必须用消息队列(Kafka/RocketMQ)或gRPC,并配套死信队列、幂等设计、监控告警。
4.2 性能红线:什么时候该给观察者“上锁”?
观察者模式的性能瓶颈往往不在通知本身,而在事件构造成本。我们曾遇到一个诡异问题:订单创建接口RT从200ms飙升至2s,arthas追踪发现90%时间耗在JSON.toJSONString(event)上。原因竟是某个监听器为了“方便调试”,在handle()方法里把整个订单对象序列化成JSON存入日志。
解决方案是事件对象轻量化:
- 只携带必要字段:
orderId、userId、timestamp,而非整个Order实体; - 复杂数据通过ID异步查询,避免阻塞主线程;
- 对事件对象做
@Data+@Builder,禁止toString()重载。
更关键的是监听器执行超时控制。Spring未提供原生超时机制,我们通过AOP实现:
@Around("@annotation(org.springframework.context.event.EventListener)") public Object enforceTimeout(ProceedingJoinPoint joinPoint) throws Throwable { return CompletableFuture .supplyAsync(() -> { try { return joinPoint.proceed(); } catch (Throwable t) { throw new RuntimeException(t); } }, taskExecutor) .orTimeout(3, TimeUnit.SECONDS) // 强制3秒超时 .exceptionally(throwable -> { log.warn("Event handler timeout: {}", joinPoint.getSignature()); return null; }) .get(); }这个切面让所有监听器具备熔断能力,避免单个慢监听器拖垮整个系统。
5. 从课堂作业到工业级落地——观察者模式的演进路线图
5.1 学习路径:为什么“设计模式期末”考题总在偏离重点?
高校教材常以天气预报站为例讲解观察者模式:WeatherStation作为被观察者,CurrentConditionsDisplay、StatisticsDisplay作为观察者。这个例子的问题在于:
- 它假设所有观察者实时在线,忽略网络分区、服务启停;
- 它把观察者当作被动接收者,忽视观察者可能主动查询、重试、降级;
- 它用
ArrayList存储观察者,完全没提并发安全。
这导致学生考完试仍不会解决真实问题。比如“设计模式大作业”要求实现电商系统,90%的学生代码类似:
public class OrderService { private List<OrderObserver> observers = new ArrayList<>(); public void createOrder(Order order) { // ...业务逻辑 for (OrderObserver observer : observers) { observer.update(order); // 没有异常处理! } } }当observer.update()抛出NullPointerException时,整个订单创建流程中断——而生产环境要求“核心流程不因旁路功能失败而中断”。
真正的学习路径应该是:
- 理解本质:观察者模式是“发布-订阅”思想的面向对象实现,核心价值是解耦变化点;
- 掌握缺陷:亲手用
Observable写一个并发场景,体会ConcurrentModificationException的痛; - 工业实践:用Spring事件重构一个真实接口,配置线程池、添加超时、接入监控;
- 架构延伸:对比Kafka的Topic-Partition模型,理解观察者模式与消息队列的适用边界。
5.2 工程化 checklist:上线前必须验证的12个点
我把观察者模式落地经验浓缩为一份上线前核对清单,每项都来自真实故障:
| 序号 | 检查项 | 验证方法 | 不通过后果 |
|---|---|---|---|
| 1 | 事件对象是否实现Serializable? | new ObjectOutputStream(new ByteArrayOutputStream()).writeObject(event) | 集群环境下事件无法序列化传输 |
| 2 | 监听器方法是否加@Transactional? | 查看数据库连接是否复用 | 事务不生效,数据不一致 |
| 3 | 是否配置了异步线程池? | jstack查看线程名是否含event-handler | 主线程阻塞,接口超时 |
| 4 | 事件发布是否在事务提交后? | 在@Transactional方法末尾打日志 | 事务回滚时事件已发出,数据不一致 |
| 5 | 是否有死循环监听? | 检查handle()内是否又发布同类型事件 | CPU 100%,服务雪崩 |
| 6 | 事件存储表是否有唯一索引? | show create table event_log | 重复事件导致业务重复执行 |
| 7 | 监听器是否处理空指针? | 用null参数调用handle() | NullPointerException中断通知链 |
| 8 | 是否有降级逻辑? | 关闭监听器服务,观察主流程 | 主流程因旁路失败而中断 |
| 9 | 事件大小是否超过1MB? | event.toString().length() | Kafka消息体超限,投递失败 |
| 10 | 是否记录事件处理耗时? | micrometer监控event.handle.time | 无法定位慢监听器 |
| 11 | 是否有告警阈值? | alert: event_handle_duration_seconds > 5 | 慢监听器长期无人处理 |
| 12 | 是否支持灰度开关? | @ConditionalOnProperty("event.enabled.vip") | 新功能上线无法灰度验证 |
这份清单我们贴在团队知识库首页,每次CR(Code Review)必须逐项确认。去年有次紧急上线,新人漏了第4项,导致支付成功事件在事务回滚后仍被消费,造成12笔重复扣款。从此这条成了“红线中的红线”。
5.3 终极思考:观察者模式正在消亡吗?
看到“体验『设计创意模式』”这类热搜词,有人觉得设计模式过时了。但我的观察恰恰相反:观察者模式正以更隐蔽的方式渗透到每个角落。前端Vue的$emit/$on、React的Context API、Node.js的EventEmitter、甚至Kubernetes的Operator模式,底层都是观察者思想的变体。
真正的淘汰不是模式本身,而是僵化的实现方式。JDK的Observable被淘汰,是因为它把“如何通知”和“通知什么”耦合在了一起;Spring事件被广泛采用,是因为它把“发布”“分发”“处理”拆成可插拔的组件;而云原生时代的事件网格(Event Mesh),则把观察者模式扩展到了跨集群、跨云、跨协议的维度。
所以别纠结“学设计模式有没有用”,要问自己:当需求变更时,你的代码是需要改3个地方,还是只改1个?当流量突增时,你的通知链路是线性扩容,还是自动伸缩?当出现故障时,你能5分钟定位到是哪个观察者拖慢了全局,还是得翻两天日志?——这些才是观察者模式留给工程师的终极考卷。
我在生产环境写过最自豪的一行代码不是什么高深算法,而是:
// OrderService.java eventPublisher.publish(new OrderCreatedEvent(order.getId()));这行代码背后,是17个监听器的并行执行、是3个数据中心的事件同步、是每秒2万次的稳定投递、是故障时自动降级的优雅退场。它不炫技,不复杂,但像空气一样不可或缺——而这,才是设计模式本该有的样子。