Skip to content

约 5 分钟阅读

观察者模式 ​

观察者(Observer)把一个变化通知给多个订阅者,发布方只依赖通知接口。订单已支付后,页面提示和进程内统计可以分别响应;增加一个非关键统计监听器,不必把它的实现塞进支付处理流程。但“通知函数返回”与“所有业务后续都可靠完成”是不同的保证。

进程内事件分发与订阅生命周期

通知的是事实,还是要求必须完成的动作 ​

OrderPaid 表示已支付这一事实;DeductInventory 则是要求扣减库存的命令。若库存扣减必须与订单状态共同满足一致性约束,就不能把它随意移入一个失败后被忽略的监听器。观察者减少直接依赖,却不会消除事务、时间顺序和失败依赖。

可以先用通知栏理解:一个任务完成后,页面刷新、统计计数、桌面提醒都想知道结果。通知栏不必知道三个实现的细节;其中桌面提醒权限被拒绝,也不应把已经完成的任务改成失败。反过来,如果“校验通过才允许付款”是前置规则,就应在主流程中显式执行,而不是靠监听顺序暗中保证。

一个有明确边界的分发器 ​

下面的程序同步、单线程、仅在进程内运行。注册使用唯一标识,重复标识拒绝;每次发布先复制订阅快照,通知期间退订从下一次发布生效。非关键监听器的运行时异常被记录为结果,其他监听器继续;不捕获 JVM Error,也不提供可靠重试。

保存为 ObserverDemo.java,使用 Java 17+ 编译运行:

java
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;

public class ObserverDemo {
    record OrderPaid(String eventId, String orderId, long amountFen) {
        OrderPaid {
            if (eventId == null || eventId.isBlank()
                    || orderId == null || orderId.isBlank() || amountFen < 0) {
                throw new IllegalArgumentException("事件标识、订单或金额非法");
            }
        }
    }

    interface Subscription extends AutoCloseable {
        @Override
        void close();
    }

    record Registration(String id, Consumer<OrderPaid> listener) {}
    record Delivery(String id, boolean success, String errorType) {}

    static final class Publisher {
        private final Map<String, Registration> registrations = new LinkedHashMap<>();
        private boolean publishing;

        Subscription subscribe(String id, Consumer<OrderPaid> listener) {
            if (id == null || id.isBlank()) throw new IllegalArgumentException("订阅标识为空");
            Registration registration = new Registration(id, Objects.requireNonNull(listener));
            if (registrations.putIfAbsent(id, registration) != null) {
                throw new IllegalArgumentException("订阅标识重复");
            }
            return () -> {
                if (registrations.get(id) == registration) registrations.remove(id);
            };
        }

        List<Delivery> publish(OrderPaid event) {
            Objects.requireNonNull(event);
            if (publishing) throw new IllegalStateException("本分发器不支持重入发布");
            List<Registration> snapshot = List.copyOf(registrations.values());
            List<Delivery> results = new ArrayList<>();
            publishing = true;
            try {
                for (Registration registration : snapshot) {
                    try {
                        registration.listener().accept(event);
                        results.add(new Delivery(registration.id(), true, ""));
                    } catch (RuntimeException failure) {
                        results.add(new Delivery(registration.id(), false,
                                failure.getClass().getSimpleName()));
                    }
                }
                return List.copyOf(results);
            } finally {
                publishing = false;
            }
        }
    }

    public static void main(String[] args) {
        Publisher publisher = new Publisher();
        List<String> calls = new ArrayList<>();
        Subscription ui = publisher.subscribe("ui", event -> calls.add("ui:" + event.eventId()));
        publisher.subscribe("optional-metric", event -> {
            throw new IllegalStateException("演示统计失败");
        });
        Subscription audit = publisher.subscribe("local-log",
                event -> calls.add("log:" + event.eventId()));
        OrderPaid first = new OrderPaid("E-1", "O-1", 10001);
        List<Delivery> results = publisher.publish(first);
        if (!calls.equals(List.of("ui:E-1", "log:E-1"))
                || results.size() != 3 || results.get(1).success()
                || !results.get(2).success()) {
            throw new AssertionError("监听器失败隔离错误");
        }
        audit.close();
        audit.close();
        calls.clear();
        List<Delivery> second = publisher.publish(new OrderPaid("E-2", "O-2", 500));
        if (!calls.equals(List.of("ui:E-2")) || second.size() != 2) {
            throw new AssertionError("退订未生效");
        }
        ui.close();
        System.out.println(results);
        System.out.println("失败监听器不阻断后续;重复退订安全;下次发布不再通知已退订者");
    }
}

这里的注册顺序是该示例的显式合同,不是所有观察者框架的共同保证。不能让监听器 B 假定监听器 A 已经写好了共享数据;有这样的前后依赖时,应把它们放在同一个处理流程,或明确事件阶段。

每次事件携带不可变数据,监听器无需回头读取发布者的“当前订单”。若改用拉取方式,通知 E-1 后再读取当前状态可能已经看到 E-2;异步处理尤其需要事件版本或快照。不可变 record 也只冻结字段引用,本例的字段是不可变字符串与数值,含可变集合时还需复制。

订阅关系也有生命周期 ​

组件卸载、页面关闭或会话结束时,应关闭相应订阅,否则长生命周期发布者会一直持有监听器及其捕获对象。弱引用不是通用替代:没有其他强引用时,监听器可能被回收并无声停止通知;后台清理线程自身也需要有退出方式。

本例使用订阅快照,因此监听器在一次回调中新增或取消订阅不会修改当前循环。取消不代表中断已经开始的回调;多线程版本要进一步定义“退订返回后,是否还允许旧快照调用”。简单换成 synchronizedList 不会让复合操作、复制与遍历自动全部原子化。

publishing 保护防止本分发器递归发布形成通知环,但它不是跨线程锁。所有方法都必须由同一个事件循环调用;若要跨线程发布,应另行设计串行调度或并发合同,不能把这个程序直接注册成任意线程可用的单例服务。

同步、异步与可靠消息 ​

方式调用返回代表什么需要额外解决的问题
同步本地回调本次回调已执行或失败异常传播、监听耗时、重入与顺序
提交到本地线程池任务已提交,可能尚未执行容量、拒绝、上下文、关闭、取消与结果收集
带持久化的消息分发由具体确认协议决定事务衔接、重投、去重、顺序和重放

观察者与发布订阅有相近思想,不能一面把两者完全等同,一面又断言前者只能同步、后者一定依赖消息代理。进程内事件总线也可以做订阅过滤;消息代理则可能提供跨进程路由和持久化,但这些能力须由实际实现与配置保证。

把每个监听器放到 parallelStream() 并不会自动提高吞吐,还会改变顺序、增加共享状态竞争,并可能阻塞公共线程池。异步分发应设置有界容量,明确慢订阅者如何积压、拒绝或降级;需要可靠投递时,也要持久记录失败和重试,而不是只打印异常。

Spring 事件与事务提交 ​

Spring 的 ApplicationEventPublisher 负责把事件交给多播器,单凭调用接口不能推断同步或异步。默认简单多播器通常在发布线程调用监听器,也可以配置执行器;方法监听器的 @EventListener 应放在处理方法上。框架的实际执行合同需要结合所用版本与配置判断。

@TransactionalEventListener 可以绑定到事务阶段,默认 AFTER_COMMIT;没有活动事务时默认不会执行,除非明确开启 fallbackExecution。提交前发送外部邮件可能产生“数据库回滚但邮件已发送”的不一致;提交后回调又存在“数据库已提交、进程在通知前退出”的丢失窗口。

因此,提交后监听器适合有相应丢失容忍度的进程内后续动作,不能单独保证通知可靠。需要订单事件可恢复时,可在业务事务中一起写出站事件,再由独立发布流程重试与确认,机制见事件驱动架构。提交后回调如需再写数据库,还要明确新的事务边界,不能假设旧事务会再次提交。

RxJava等流式API还定义订阅、完成、错误和调度合同,冷流可能为每个订阅者重新运行生产逻辑;不能只看有多个 subscribe() 就认定每人消费的是同一次广播。背压和可靠存储也应分别判断。

失败与验收 ​

至少检查无订阅者、重复订阅标识、正常通知、某个监听器失败、通知中注册或退订、重复退订、重入发布和发布者关闭。确认“失败隔离”是否只适用于非关键动作,关键一致性要求应回到主流程或可靠事件机制中。

日志应能关联事件标识、对象版本、监听器、开始结束和失败类型;敏感支付数据不应随整个事件对象输出。同步耗时应覆盖失败回调,异步耗时还应区分排队与执行。事件被重复投递时,需要副作用幂等,不能靠“观察者天然只通知一次”的假设保护账目。

别急,先让缓存热一下。