Null-safety
尽管 Java 目前在其类型系统中尚不支持表达空值标记,但 Spring Framework 的代码库已使用 JSpecify 注解来声明其 API、字段及相关类型使用的可空性。强烈建议阅读 JSpecify 用户指南,以便熟悉这些注解及其语义。
这种空安全机制的主要目标是通过编译时检查防止在运行时抛出 NullPointerException,并使用显式的可空性来表达值可能不存在的情况。
在 Java 中,这可以通过利用诸如 NullAway 等空值检查工具,或支持 JSpecify 注解的 IDE(如 IntelliJ IDEA 和 Eclipse,后者需要手动配置)来实现。在 Kotlin 中,JSpecify 注解会自动转换为 Kotlin 的空安全机制。
Nullness Spring API 可在运行时用于检测类型用法、字段、方法返回类型或参数的可空性。它全面支持 JSpecify 注解、Kotlin 空安全以及 Java 原始类型,并可对任何 @Nullable 注解(无论其所属包)进行务实的检查。
使用 JSpecify 注解标注库
从 Spring Framework 7 开始,Spring Framework 代码库利用 JSpecify 注解来暴露空安全(null-safe)的 API, 并在其构建过程中使用 NullAway 来检查这些可空性声明的一致性。建议所有依赖 Spring Framework 和 Spring 产品组合项目的库, 以及其他与 Spring 生态系统相关的库(如 Reactor、Micrometer 和 Spring 社区项目), 也采用相同的做法。
在 Spring 应用中利用 JSpecify 注解
使用支持空值注解的 IDE 开发应用程序时,如果未遵守可空性契约,Java 会发出警告,而 Kotlin 则会报错,从而帮助 Spring 应用开发者完善其空值处理逻辑,避免在运行时抛出 NullPointerException。
可选地,Spring 应用程序开发者可以对其代码库添加注解,并使用诸如 NullAway 之类的构建插件,在构建时于应用程序级别强制实施空值安全性。
指南
本节的目的是分享一些关于显式指定 Spring 相关库或应用程序可空性(nullability)的建议准则。
JSpecify
默认为非空
需要理解的一个关键点是,在 Java 中默认情况下类型的可空性是未知的,而且非空类型的使用频率远高于可空类型。为了保持代码库的可读性,我们通常希望默认定义类型使用为非空,除非在特定作用域内明确标记为可空。这正是 @NullMarked 的目的,它通常在 Spring 项目中通过 package-info.java 文件在包级别进行设置,例如:
@NullMarked
package org.springframework.core;
import org.jspecify.annotations.NullMarked;
显式空性
在 @NullMarked 代码中,可空类型的使用通过 @Nullable 显式定义。
JSpecify 的 @Nullable / @NonNull 注解与其他大多数变体的一个关键区别在于,JSpecify 注解使用了 @Target(ElementType.TYPE_USE) 进行元注解,因此它们仅适用于类型使用(type usage)。这会影响此类注解的放置位置,无论是为了符合相关的 Java 规范,还是遵循代码风格的最佳实践。从代码风格的角度来看,建议充分利用这些注解的“类型使用”特性,将其放在与被注解类型相同的行上,并紧邻该类型之前。
例如,对于一个字段:
private @Nullable String fileEncoding;
或者用于方法参数和方法返回类型:
public @Nullable String buildMessage(@Nullable String message,
@Nullable Throwable cause) {
// ...
}
|
重写方法时,JSpecify 注解不会从原始方法继承。这意味着,如果你想重写实现并保持相同的可空性语义,则应将 JSpecify 注解复制到重写的方法中。 |
@NonNull 和
@NullUnmarked 在典型使用场景中很少需要用到。
数组和可变参数
对于数组和可变参数(varargs),你需要能够区分数组元素的可空性与数组本身的可空性。请注意Java 规范中定义的语法,这种语法初看可能会令人感到意外。例如,在 @NullMarked 代码中:
-
@Nullable Object[] array表示数组中的各个元素可以为null,但数组本身不能为 null。 -
Object @Nullable [] array表示数组中的各个元素不能为null,但数组本身可以为2。 -
@Nullable Object @Nullable [] array表示数组中的各个元素以及数组本身都可以为null。
泛型
JSpecify 注解也适用于泛型。例如,在 @NullMarked 代码中:
-
List<String>表示一个非空元素的列表(等同于List<@NonNull String>) -
List<@Nullable String>表示一个包含可为空元素的列表
当你声明泛型类型或泛型方法时,情况会稍微复杂一些。更多详细信息,请参阅相关的JSpecify 泛型文档。
| 泛型类型和泛型方法的可空性 NullAway 尚未完全支持。 |
NullAway
配置
推荐的配置是:
-
NullAway:OnlyNullMarked=true用于仅对使用@NullMarked注解的包执行可空性检查。 -
NullAway:CustomContractAnnotations=org.springframework.lang.Contract,该配置使 NullAway 能够识别https://docs.spring.io/spring-framework/docs/7.0.6/javadoc-api/org/springframework/lang/Contract.html包中的 @Contract 注解, 该注解可用于表达补充语义,以避免在代码库中产生无关的警告。
一个展示 @Contract 声明优势的良好示例可以在 Assert.notNull() 中看到,它使用了 @Contract("null, _ → fail") 注解。通过该契约声明,NullAway 将理解在成功调用 Assert.notNull() 后,作为参数传入的值不可能为 null。
可选地,可以设置 NullAway:JSpecifyMode=true 以启用完整的 JSpecify 语义检查,包括对数组、可变参数(varargs)和泛型上的注解进行检查。请注意,此模式仍在开发中,并且需要 JDK 22 或更高版本(通常结合使用 --release Java 编译器标志来配置预期的基线版本)。建议仅在确保代码库在本节前面提到的推荐配置下不产生任何警告之后,再将 JSpecify 模式作为第二步启用。
警告抑制
在某些合法的使用场景中,NullAway 可能会错误地检测到空值问题。在这种情况下,建议抑制相关的警告,并记录原因:
-
@SuppressWarnings("NullAway.Init")在字段、构造函数或类级别使用,可以避免因字段的延迟初始化(例如,由于类实现了InitializingBean)而产生的不必要警告。 -
@SuppressWarnings("NullAway") // Dataflow analysis limitation可在 NullAway 数据流分析无法检测到涉及空值问题的代码路径永远不会执行时使用。 -
@SuppressWarnings("NullAway") // Lambda可在 NullAway 未考虑在 lambda 表达式外部执行的断言对 lambda 内部代码路径的影响时使用。 -
@SuppressWarnings("NullAway") // Reflection可用于某些反射操作,这些操作已知会返回非空值,即使 API 无法表达这一点。 -
@SuppressWarnings("NullAway") // Well-known map keys可在使用已知存在的键调用Map#get方法时使用,前提是之前已插入了与非空相关的值。 -
@SuppressWarnings("NullAway") // Overridden method does not define nullability可在父类未定义可空性时使用(通常父类来自外部依赖)。 -
@SuppressWarnings("NullAway") // See github.com/uber/NullAway/issues/1075可在 NullAway 无法检测泛型方法中类型变量的可空性时使用。
从 Spring 空安全注解迁移
Spring 空安全注解 @Nullable、
@NonNull、
@NonNullApi 和
@NonNullFields(位于 org.springframework.lang 包中)是在 Spring Framework 5 中引入的,当时 JSpecify 尚未出现,而当时的最佳选择是利用来自 JSR 305(一个已停滞但广泛使用的 JSR)的元注解。自 Spring Framework 7 起,这些注解已被弃用,转而推荐使用
JSpecify 注解。JSpecify 提供了显著的增强功能,例如明确定义的规范、无分包问题的规范依赖、更好的工具支持、更佳的 Kotlin 集成,以及能够为更多用例更精确地指定可空性的能力。
一个关键区别在于:Spring 已弃用的空安全注解遵循 JSR 305 语义,适用于字段、参数和返回值;而 JSpecify 注解则适用于类型使用(type usage)。这一细微差别在实践中具有相当重要的意义,因为它允许开发者区分元素的可空性与数组/可变参数的可空性,并且能够定义泛型类型的可空性。
这意味着数组和可变参数(varargs)的空值安全性声明必须进行更新,以保持相同的语义。例如,使用 Spring 注解时的 @Nullable Object[] array 在使用 JSpecify 注解时需要更改为 Object @Nullable [] array。同样的规则也适用于可变参数(varargs)。
还建议将字段和返回值注解移至更靠近类型的位置,并放在同一行上,例如:
-
对于字段,不要使用 Spring 注解的
@Nullable private String field,而应使用 JSpecify 注解的private @Nullable String field。 -
对于方法返回类型,使用 JSpecify 注解时,应采用
@Nullable public String method()的形式,而不是 Spring 注解中的public @Nullable String method()。
此外,使用 JSpecify 时,在 null 标记代码中重写父类方法中标注为 @NonNull 的类型用法时,无需显式指定 @Nullable 来“撤销”可空声明。只需将其声明为无注解形式,null 标记的默认规则就会生效(除非显式标注为可空,否则该类型用法被视为非空)。