publishEvent 返回时,通知发送完了吗

订单处理方法发布 OrderPlaced,监听器更新审计记录并发送通知。默认同步监听下,发布调用返回之前,监听器方法已经返回;为广播器配置线程池后,同一行发布代码却可以在监听器仍然等待时返回。是否完成无法仅从方法名判断,必须检查广播器采用的执行方式和业务完成信号。

Spring 的进程内事件机制负责把事件交给匹配的监听器。它没有自动提供持久化、重试队列或远端送达保证。即使监听器方法同步返回,也只证明这段方法的执行边界;如果该方法又提交了后台任务,真正的通知业务仍可能没有完成。ApplicationContext 事件能力

本文固定 Spring Framework 6.2.11、JDK 21,源码提交为 4c134254642d88e058aa004bdaf44168e1be7bb2。完整实验为 examples/spring-framework-lab/src/main/java/blog/spring/Chapter12.java。默认行为通过 AnnotationConfigApplicationContext 与 EventListener 方法验证,执行器、拒绝和异常对照直接操作 SimpleApplicationEventMulticaster,避免额外引入异步代理。

需要区分的状态 可用证据
发布者已返回 multicastEvent 调用之后的主线程断言
监听器已进入 entered latch 与线程引用
监听业务已完成 completed latch
监听发生异常 发布者捕获或 ErrorHandler 记录
异步资源已释放 shutdown 后 awaitTermination 返回 true

同步递归与异步完成信号的两条执行路径

EventListener 方法怎样进入监听列表

EventListenerMethodProcessor 在单例初始化后的阶段查找适用 Bean 的监听方法,并交给 EventListenerFactory 建立适配器。默认适配器将普通 Java 方法包装成可由广播器调用的监听器。扫描注解和实际事件匹配分属两个阶段:Bean 上存在方法,不表示任意事件都会触发它。监听方法处理器

监听器适配器根据声明参数等信息判断事件是否适用,再准备实参并调用目标方法。实验的两个方法都接收 OrderPlaced,因此不处理 Context 自己发出的 refresh、close 事件。观察日志时,应记录事件类型与标识,避免把基础设施事件误当成业务事件的重复投递。

AbstractApplicationEventMulticaster 根据事件类型和来源筛选监听器,返回排序后的匹配集合。SimpleApplicationEventMulticaster 决定如何调用集合中的每个监听器。前者回答“谁接收以及提交顺序”,后者回答“在哪个执行入口调用以及异常如何处理”。这两个职责拆开,才能解释为什么监听器有 Order 但异步完成顺序仍可能不同。筛选与排序实现

默认执行在发布者线程中完成

没有显式 TaskExecutor 时,SimpleApplicationEventMulticaster 直接 invokeListener。实验保存发布线程 Thread 引用,在监听方法中保存当前线程引用,再用身份比较确认相同。只比较线程名不足以证明身份,因为线程名称可以重复或被修改。

Audit 的 first 监听器顺序为零,second 为一。发布深度为零的 OrderPlaced 时,first 在方法内部再次发布深度为一的事件;深度一不再递归。完整结果是 first:0, first:1, second:1, second:0。内层发布在外层 first 返回前完成,然后外层广播才轮到 second。

1
2
3
4
5
6
7
发布 depth=0
first:0
发布 depth=1
first:1
second:1
second:0
返回调用者

这个顺序由同步调用栈造成,与“事件通常排队等待处理”的想象不同。如果递归发布没有停止条件,会持续增加调用深度,也可能反复修改同一业务状态。实验显式限制深度,证明的是有界重入顺序,不用真正耗尽栈来演示失败。

模式提炼:顺序声明不消除重入

执行序列可以表达为 listener A → nested publish → nested listeners → listener B。通知回调、UI 事件和观察者列表都有相同的重入问题。给监听器排序只能规定每一次遍历的相对顺序,不能阻止监听器内部再次发起遍历。涉及库存或账户状态时,业务应明确是否允许重入及何时提交可见状态。

异常默认打断剩余同步监听器

实验向独立广播器加入两个 Ordered 监听器。第一位记录 failed 并抛 IllegalStateException,第二位记录 later。未安装 ErrorHandler 时,发布调用收到相同类型的运行时异常,trace 只有 failed。后续监听器没有执行,因为前一次 invokeListener 已中断当前循环。广播与异常处理源码

再安装一个把异常保存到 AtomicReference 且正常返回的 ErrorHandler。相同事件下,发布正常返回,trace 变成 failed、later,异常对象仍可被断言。这里的“继续”来自 handler 不再抛出异常;若 handler 自己抛异常,不能继续套用这个结果。

吞掉异常与业务成功之间没有等号。handler 记录了通知失败,后续审计照常执行,只能说明错误处理策略允许广播继续。若上游要求通知失败必须让下单失败,吞异常改变了业务契约;若要求下单独立成功,则需要单独存储失败并安排重试,而不是依赖一次内存事件自然恢复。

显式执行器将发布与完成分开

异步对照使用两个工作线程。第一个监听器保存 worker 引用、释放 entered,然后等待 release。第二个监听器抛受控异常,ErrorHandler 记录异常并释放 errorReceived。主线程调用 multicastEvent 返回后等待 entered 和 errorReceived,再检查 completed 尚未释放。

release 由主线程控制,所以监听器确实无法在断言前完成;这比观察“日志好像晚了一点”更强。随后主线程释放 release,等待 completed,确认业务结束。所有等待最多五秒,finally 无论成功失败都会释放门闩、关闭执行器并验证线程池终止。

广播器先按监听顺序提交任务,但两个任务在哪个线程先运行、何时结束由调度与各自工作量决定。实验中,顺序较后的失败监听器能在顺序较前的阻塞监听器完成前报告错误。将 Order 解释成跨线程完成顺序,会使依赖前序输出的监听器读到尚未更新的状态。

异步异常发生在线程池调用路径,已经返回的发布方法没有调用栈可供它重新抛回。本文采用广播器 ErrorHandler 观察该错误,并断言异常类型;没有把异常只交给标准错误输出。未配置 handler 时最终如何报告还与执行器实现有关,不应一概描述为“调用者仍能 catch 到”。

配置线程池仍有同步分支

固定版本的调用条件是:存在 executor,且监听器 supportsAsyncExecution() 返回 true,才尝试 executor.execute。实验另建一个监听器返回 false,即使已经配置线程池,仍在发布者线程中执行。这条契约允许特定监听器保留线程绑定的执行要求。

另一个容易遗漏的分支是执行器拒绝。6.2.11 的 multicastEvent 捕获 RejectedExecutionException 后会在当前线程直接 invokeListener。实验使用总是拒绝的 Executor,异步能力为 true 的监听器最终仍记录发布者线程。过载或关闭期间的拒绝可能改变延迟和上下文,不能把“配置了线程池”写成所有调用永远异步。

这里的结论严格绑定 SimpleApplicationEventMulticaster 6.2.11。自定义广播器、监听方法上的异步代理和其他消息系统可能采用不同拒绝策略。修改实现之后,应重跑线程身份与异常路径的断言,不能沿用配置名称相同的推断。

上游 ApplicationContextEventTests 的 simpleApplicationEventMulticasterWithTaskExecutorAndNonAsyncListener、simpleApplicationEventMulticasterWithException、simpleApplicationEventMulticasterWithErrorHandler 和 orderedListeners 分别覆盖这些基础契约。本文阅读固定版本测试并运行自己的 12 条断言,未执行上游测试套件。固定版本事件测试

模式提炼:以完成凭据表达业务边界

可靠的完成判断可以写成 submit → observe accepted → await terminal result。本地线程任务可用 Future 或 latch,远端操作需要协议响应和业务状态,持久化消息还要区分写入队列与消费者提交。观察对象必须与需要证明的完成层次一致。一次 publish 返回,只能证明该发布入口自身结束,不能跨越执行器与远端系统替业务作证。

运行与排查

实验工程内使用 JDK 21:

1
./mvnw -q compile exec:java -Dexec.mainClass=blog.spring.Chapter12

evidence/12/local-20261002/run.txt 包含 12 条 CHECK PASS,退出码 0,递归 trace 为上述顺序,最后一条验证 event executor terminated。实验不发送外部消息,不写数据库,也没有验证提交后事务事件;事务监听将在事务边界内单独讨论。

遇到“发布成功但监听没完成”,先确认匹配的监听器是否已注册,再记录事件标识、发布线程、实际执行线程、错误去向和完成凭据。需要比较的是同一次事件实例的路径,混合多次发布的日志无法证明顺序。若异常被 ErrorHandler 正常返回吸收,还应查看该 handler 保存的业务结果。

反例题:一个同步监听器把发送操作交给自己的 Executor,方法随即返回。默认事件同步执行能否证明发送完成?不能。事件机制只同步等待监听方法本身,监听器内部再次建立了异步边界。

可执行练习:在副本中把线程池改成单线程,保留第一个监听器等待 release;将等待 errorReceived 放到 release 之后。预测这个修改为什么必须成对发生:单线程下,第二个任务只能在第一个结束后执行,原先提前等待第二个任务的测试会达到截止时间。这个练习展示任务依赖,而不是用 sleep 掩盖阻塞。

症状 首先检查 可证伪的判断
调用返回而任务未结束 完成凭据与执行器 返回点时 completed 仍未释放
后续监听器没调用 第一处异常与 handler trace 在异常处终止
Order 仍出现结果倒序 比较提交与完成 后提交任务先完成
线程偶尔变成发布线程 supportsAsync 与拒绝 明确进入同步或拒绝回退分支

参考资料