空安全
目录
尽管 Java 目前还无法在类型系统中表达可空性标记,但 Project Reactor 的代码库使用 JSpecify 注解来标注其 API、字段及相关类型用法的可空性。强烈建议阅读 JSpecify 用户指南,以熟悉这些注解及其语义。
这种空安全安排的主要目标,是通过编译期检查在运行时避免抛出 NullPointerException,并以显式的可空性来表达“值可能不存在”。在 Java 中,可以借助 NullAway 等可空性检查工具,或支持 JSpecify 注解的 IDE(如 IntelliJ IDEA 和 Eclipse,后者需要手动配置)来发挥作用。在 Kotlin 中,JSpecify 注解会自动映射为 Kotlin 的空安全。
1. 用 JSpecify 注解标注类库
从 Reactor Core 3.8.0 起,Project Reactor 的代码库开始使用 JSpecify 注解来暴露空安全的 API,并在构建过程中借助 NullAway 检查这些可空性声明的一致性。建议每一个依赖 Reactor Core 及 Project Reactor 系列项目的类库也照此办理。
2. 在应用中运用 JSpecify 注解
使用支持可空性注解的 IDE 开发应用时,一旦违反可空性约定,Java 代码会给出警告、Kotlin 代码会报错,从而帮助应用开发者改进空值处理,避免在运行时抛出 NullPointerException。
应用开发者还可以选择给自己的代码库加上注解,并使用 NullAway 之类的构建插件,在构建期于应用层面强制空安全。
3. 约定指南
本节的目的,是分享一些为 Reactor 相关类库或应用显式声明可空性而提出的约定指南。
3.1. JSpecify
3.1.1. 默认非空
需要理解的一个关键点是:在 Java 中,类型的可空性默认是未知的,而非空的类型用法远比可空用法常见。为了保持代码库的可读性,我们通常希望默认规定类型用法为非空,除非在特定范围内显式标记为可空。这正是 @NullMarked 的用途,在 Project Reactor 项目中它通常通过 package-info.java 文件设置在包级别,例如:
@NullMarked
package reactor.core;
import org.jspecify.annotations.NullMarked;
3.1.2. 显式声明可空性
在 @NullMarked 的代码中,可空的类型用法用 @Nullable 显式声明。
JSpecify 的 @Nullable / @NonNull 注解与大多数其他变体的一个关键区别在于:JSpecify 注解带有 @Target(ElementType.TYPE_USE) 元注解,因此它们只作用于类型用法。这影响了这些注解应放置的位置——既要符合相关 Java 规范,也要遵循代码风格的最佳实践。从风格角度,建议顺应这些注解的“类型用法”特性,把它们放在与被注解类型同一行、且紧挨着类型之前的位置。
例如,对于字段:
private @Nullable String field;
或者对于方法参数与方法返回类型:
public @Nullable String buildMessage(@Nullable String message,
@Nullable Throwable cause) {
// ...
}
重写方法时,JSpecify 注解不会从原方法继承。也就是说,如果您想重写实现并保持相同的可空性语义,就应当把 JSpecify 注解复制到重写方法上。
在典型场景中,@NonNull 和 @NullUnmarked 很少会用到。
3.1.3. 数组与可变参数
对于数组和可变参数,您需要能够区分“元素的可空性”与“数组本身的可空性”。请注意 Java 规范所定义的语法,它乍看之下可能有些反直觉。例如,在 @NullMarked 代码中:
-
@Nullable Object[] array表示单个元素可以为null,但数组本身不可以。 -
Object @Nullable [] array表示单个元素不可以为null,但数组本身可以。 -
@Nullable Object @Nullable [] array表示单个元素和数组都可以为null。
3.1.4. 泛型
JSpecify 注解同样适用于泛型。例如,在 @NullMarked 代码中:
-
List<String>表示一个由非空元素组成的列表(等价于List<@NonNull String>) -
List<@Nullable String>表示一个由可空元素组成的列表
在声明泛型类型或泛型方法时,情况会稍复杂一些。更多细节请参阅相关的 JSpecify 泛型文档。
Reactor 的 Flux 和 Mono 不支持可空的类型参数(例如 Flux<@Nullable T> 或 Mono<@Nullable T>),因为不允许出现 null 信号。但请注意,Mono#block()、Flux#blockFirst() 以及类似方法的签名允许返回 null,用以表示完成时没有值(例如 @Nullable T block())。
泛型类型与泛型方法的可空性尚未被 NullAway 完全支持。
3.1.5. 嵌套类型与全限定类型
Java 规范还规定:用 @Target(ElementType.TYPE_USE) 定义的注解——例如 JSpecify 的 @Nullable 注解——在内部类型名或全限定类型名中,必须声明在最后一个点(.)之后:
-
Sinks.@Nullable Many -
io.micrometer.context.ContextSnapshot.@Nullable Scope
3.2. NullAway
3.2.1. 配置
推荐的配置是:
NullAway:OnlyNullMarked=true,以便只对标注了@NullMarked的包做可空性检查。
此外,还可以设置 NullAway:JSpecifyMode=true 来启用对完整 JSpecify 语义的检查,包括对数组、可变参数和泛型上注解的检查。请注意,该模式仍在开发中,并需要 JDK 22 或更高版本(通常配合 Java 编译器的 --release 参数来设定期望的基线)。建议先确保代码库在前面提到的推荐配置下不再产生任何警告之后,再作为第二步启用 JSpecify 模式。
3.2.2. 抑制警告
在少数合理场景中,NullAway 会误报可空性问题。在这种情况下,建议抑制相关警告并注明原因:
-
可以在字段、构造函数或类级别使用
@SuppressWarnings("NullAway.Init"),以避免因字段延迟初始化而产生不必要的警告。 -
当 NullAway 的数据流分析无法识别“涉及可空性问题的那条路径实际上永远不会发生”时,可以使用
@SuppressWarnings("NullAway") // Dataflow analysis limitation。 -
当 NullAway 没有把 lambda 之外所做的断言考虑进 lambda 内部代码路径时,可以使用
@SuppressWarnings("NullAway") // Lambda。 -
对于某些已知会返回非空值、但 API 无法表达这一点的反射操作,可以使用
@SuppressWarnings("NullAway") // Reflection。 -
当
Map#get调用所使用的 key 已知一定存在、且此前已插入了对应的非空值时,可以使用@SuppressWarnings("NullAway") // Well-known map keys。 -
当父类没有定义可空性时(通常是父类来自外部依赖的情况),可以使用
@SuppressWarnings("NullAway") // Overridden method does not define nullability。 -
当 NullAway 无法检测泛型方法中类型变量的可空性时,可以使用
@SuppressWarnings("NullAway") // See [github.com/uber/NullAway/issues/1075](https://github.com/uber/NullAway/issues/1075)。
4. 从 Reactor 空安全注解迁移
`reactor.util.annotation` 包中的 Reactor 空安全注解 @Nullable、@NonNull、@NonNullApi 和 @NonNullFields,是在 JSpecify 尚未出现时引入 Project Reactor 的;当时最好的选择是借助 JSR 305(一个已休眠但被广泛采用的 JSR)的元注解。从 Reactor Core 3.8.0 起,它们已被弃用,转而推荐 JSpecify 注解。JSpecify 带来了显著增强:规范定义完善、提供无分裂包问题的规范依赖、工具支持更好、与 Kotlin 集成更佳,并且能在更多场景中更精确地指定可空性。
一个关键区别是:遵循 JSR 305 语义的旧版空安全注解作用于字段、参数和返回值;而 JSpecify 注解作用于类型用法。这个细微差别在实践中相当重要,因为它让开发者既能区分元素的可空性与数组/可变参数的可空性,也能定义泛型类型的可空性。
这意味着数组和可变参数的空安全声明必须相应更新,以保持相同的语义。例如,旧 Reactor 注解写法 @Nullable Object[] array 需要改成 JSpecify 写法 Object @Nullable [] array。可变参数同理。
同时也建议把字段和返回值上的注解移到更贴近类型、且与类型同一行的位置,例如:
-
对于字段,不要使用 Spring 注解风格的
@Nullable private String field,而应使用 JSpecify 注解风格的private @Nullable String field。 -
对于方法返回类型,不要使用 Spring 注解风格的
@Nullable public String method(),而应使用 JSpecify 注解风格的public @Nullable String method()。
此外,在 JSpecify 中,当您重写父方法中某个标注了 @Nullable 的类型用法时,无需再显式写 @NonNull 来在 null-marked 代码中“撤销”该可空声明。只要不加注解即可,此时将采用 null-marked 的默认规则(类型用法默认视为非空,除非显式标注为可空)。