空安全
尽管 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 注解来暴露空安全 API,并使用 NullAway 作为其构建的一部分来检查这些空值性声明的一致性。建议依赖 Spring Framework 和 Spring 组合项目以及其他与 Spring 生态系统相关的库(Reactor、Micrometer 和 Spring 社区项目)也这样做。
在 Spring 应用程序中利用 JSpecify 注解
使用支持空值性注解的 IDE 开发应用程序时,当空值性契约未被遵守时,将在 Java 中提供警告,在 Kotlin 中提供错误,从而使 Spring 应用程序开发人员能够完善其空值处理,以防止在运行时抛出 NullPointerException。
此外,Spring 应用程序开发人员可以选择标注其代码库并使用构建插件(如 NullAway)在构建时在应用程序级别强制执行空安全。
指南
本节的目的是分享一些建议,用于明确指定 Spring 相关库或应用程序的空值性。
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) 进行元注解,因此它们仅适用于类型用法。这影响了这些注解的放置位置,无论是为了遵守 相关的 Java 规范 还是为了遵循代码风格最佳实践。从风格角度来看,建议通过将这些注解放置在与被注解类型相同的行上并紧跟其前,来体现其类型用法的性质。
例如,对于字段
private @Nullable String fileEncoding;
或对于方法参数和方法返回类型
public @Nullable String buildMessage(@Nullable String message,
@Nullable Throwable cause) {
// ...
}
|
当重写方法时,JSpecify 注解不会从原始方法继承。这意味着如果您想重写实现并保持相同的空值性语义,JSpecify 注解应该复制到重写方法中。 |
对于典型用例,@NonNull 和 @NullUnmarked 很少需要。
数组和可变参数
对于数组和可变参数,您需要能够区分元素的空值性与数组本身的空值性。请注意 Java 规范定义的语法,这最初可能会令人惊讶。例如,在 @NullMarked 代码中
-
@Nullable Object[] array表示单个元素可以是null,但数组本身不能。 -
Object @Nullable [] array表示单个元素不能是null,但数组本身可以是。 -
@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 能够识别org.springframework.lang包中的 @Contract 注解,该注解可用于表达补充语义,以避免代码库中不相关的警告。
@Contract 声明的一个很好的例子是 Assert.notNull(),它被注解为 @Contract("null, _ → fail")。有了这个契约声明,NullAway 将理解在成功调用 Assert.notNull() 后,作为参数传递的值不能为 null。
可选地,可以将 NullAway:JSpecifyMode=true 设置为启用 对完整 JSpecify 语义的检查,包括数组、可变参数和泛型上的注解。请注意,此模式 仍在开发中,需要 JDK 22 或更高版本(通常与 --release Java 编译器标志结合使用以配置预期的基线)。建议仅在第二步启用 JSpecify 模式,即在确保代码库在使用本节前面提到的推荐配置下不产生任何警告之后。
警告抑制
在一些有效的使用场景中,NullAway 会错误地检测到空值问题。在这种情况下,建议抑制相关警告并记录原因
-
在字段、构造函数或类级别使用
@SuppressWarnings("NullAway.Init")可以避免由于字段的延迟初始化而产生的多余警告——例如,由于类实现了InitializingBean。 -
当 NullAway 数据流分析无法检测到涉及空值问题的路径永远不会发生时,可以使用
@SuppressWarnings("NullAway") // Dataflow analysis limitation。 -
当 NullAway 不考虑在 lambda 外部为 lambda 内的代码路径执行的断言时,可以使用
@SuppressWarnings("NullAway") // Lambda。 -
对于某些已知会返回非空值(即使 API 无法表达)的反射操作,可以使用
@SuppressWarnings("NullAway") // Reflection。 -
当
Map#get调用使用已知存在的键并且之前已插入非空相关值时,可以使用@SuppressWarnings("NullAway") // Well-known map keys。 -
当超类未定义空值性时(通常当超类来自外部依赖时),可以使用
@SuppressWarnings("NullAway") // Overridden method does not define nullability。 -
当 NullAway 无法检测泛型方法中的类型变量空值性时,可以使用
@SuppressWarnings("NullAway") // See github.com/uber/NullAway/issues/1075。
从 Spring 空安全注解迁移
Spring 空安全注解 @Nullable、@NonNull、@NonNullApi 和 @NonNullFields 在 Spring Framework 5 中引入,当时 JSpecify 尚不存在,最好的选择是利用 JSR 305(一个休眠但广泛使用的 JSR)的元注解。自 Spring Framework 7 起,它们已被弃用,转而使用 JSpecify 注解,后者提供了显著的增强,例如定义明确的规范、没有拆分包问题的规范依赖、更好的工具、更好的 Kotlin 集成以及针对更多用例更精确地指定空值性的能力。
一个关键区别是,Spring 已弃用的空安全注解(遵循 JSR 305 语义)适用于字段、参数和返回值;而 JSpecify 注解适用于类型用法。这种微妙的差异在实践中非常重要,因为它允许开发人员区分元素和数组/可变参数的空值性,以及定义泛型类型的空值性。
这意味着数组和可变参数的空安全声明必须更新以保持相同的语义。例如,带有 Spring 注解的 @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 来“取消”空值标记代码中的可空声明。只需将其声明为未注解,空值标记的默认值将适用(除非明确标注为可空,否则类型用法被视为非空)。