Reactor 3.8.0 的 doOnSuccess 在 Kotlin 报非空参数为 null
Mono.empty 正常完成也会向 doOnSuccess 传 null。Reactor 3.8.0 的空性声明会让 Kotlin 回调错误地拒绝它;3.8.1 已修正声明。
Kotlin 项目升级到 Reactor Core 3.8.0 后,Mono.empty<String>().doOnSuccess { ... } 可能在正常完成时抛出异常:
java.lang.NullPointerException: Parameter specified as non-null is null
堆栈指向 Kotlin lambda 的 value 参数,并经过 MonoPeekTerminal。即使回调只打印参数、没有调用任何方法,也可能失败。这里要检查 doOnSuccess 的空性声明:空 Mono 成功完成时本来就会传入 null,3.8.0 的 JSpecify 签名却没有准确表达这一点。
空完成也是成功
doOnSuccess 不是“有一个非空结果才执行”。它有两种成功输入:发出一个值时接收该值,没有值而直接完成时接收 null。上游失败则不走这个成功回调。
因此下面的逻辑必须能够接受空完成。示例针对 Reactor 3.8.1 或已包含同项修正的兼容版本:
import reactor.core.publisher.Mono
val result: String? = Mono.empty<String>()
.doOnSuccess { value: String? ->
if (value == null) {
println("completed without a value")
} else {
println(value)
}
}
.block()
block() 在这里也返回 null。不要为了让断言通过,先把空结果改造成空字符串;那会改变原有业务语义。示例的阻塞仅用于普通测试或独立诊断,不能搬到 WebFlux 的事件循环里。
故障组合中,Reactor 3.8.0 配合 Kotlin 2.2.21,会把推断出的 lambda 参数当作非空,在回调入口生成参数检查。检查先于回调体发生,所以在体内加一条 if (value == null) 未必能挽救已经错误编译的代码。
采用修正声明的版本,并重新编译消费者
3.8.0 的方法参数是 Consumer<? super T>;3.8.1 改为 Consumer<? super @Nullable T>,正式发行说明也明确列出这项修复。这个变化修正类型信息,没有把 Mono 改成允许发出 null 元素。
若工程由 Spring Boot 或 Reactor BOM 管理依赖,按兼容的管理版本升级,查看最终解析的 reactor-core,避免只修改一处声明而运行时仍加载旧 JAR。框架版本还有其他依赖约束,不能仅凭这个异常把全部模块强行覆盖成同一个数字。
还要让包含回调的 Kotlin 代码重新编译。只把服务器上的 JAR 换掉、继续使用由错误空性信息编译出来的 lambda,可能留下入口的非空检查。保留原锁文件或依赖声明,回退时连同重新构建的应用一起恢复。
若业务回调只处理真实元素,可使用 doOnNext。它不会在空完成时执行,适合“收到一条数据就处理”的语义;若原来需要记录空查询、释放某项成功资源或统计所有成功完成,直接替换会漏掉这些工作。
验收要包含空成功、有值和失败
在受控测试中分别覆盖 Mono.empty<String>()、Mono.just("ok") 与一个明确的错误信号。检查空成功进入可空分支、有值成功得到完整字符串、错误仍按原错误路径传播。不能只用非空数据证明修复成功。
若升级并重新编译后仍报相同异常,读取堆栈中的具体 lambda 参数及实际 JAR 来源。业务自身声明的非空回调、其他操作符或另一个重复版本都可能产生相似文字;本题的修复范围是 doOnSuccess 在 3.8.0 的声明,不是关闭 Kotlin 的所有空安全检查。


