DEBUG.WIKI
野草云;香港 200Mbps 峰值;不限月流量 · BGP VPS;查看套餐 →
晚安云;香港 CN2 ¥15/月
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS

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 的所有空安全检查。