空值安全性(Null-safety)
尽管Java的类型系统目前还无法直接表示空值(null)标记,但Spring Framework的代码库中使用了JSpecify注解来声明其API、字段及相关类型使用的可空性(nullability)。为了熟悉这些注解及其语义,强烈建议阅读JSpecify用户指南。
这种空值安全机制的主要目标是通过编译时检查来防止在运行时抛出NullPointerException,并利用显式的空值性来表达可能存在的值缺失情况。在Java中,可以利用像NullAway这样的空值检查工具,或者支持JSpecify注解的IDE(如IntelliJ IDEA和Eclipse,后者需要手动配置)。在Kotlin中,JSpecify注解会自动转换为Kotlin的空值安全机制(Kotlin’s null safety)。
Nullness Spring API可以在运行时检测类型使用、字段、方法返回类型或参数的为空性。它完全支持JSpecify注解、Kotlin的空安全性以及Java基本数据类型,同时还能对任何@Nullable注解进行实用性的检查(无论其所在的包是什么)。
使用JSpecify注释标注库
从Spring Framework 7开始,该框架的代码库开始使用JSpecify注解来提供空值安全的API,并在构建过程中利用NullAway来检查这些空值处理声明的一致性。建议所有依赖于Spring Framework及其相关项目(如Spring生态系统的其他库,如Reactor、Micrometer和Spring社区项目)的库也采用同样的做法。
在Spring应用中利用JSpecify注解
使用支持空值注解的IDE开发应用程序时,如果未遵守空值性契约,在Java中会收到警告,在Kotlin中会收到错误。这使得Spring应用程序开发者能够优化他们的空值处理机制,从而防止在运行时抛出NullPointerException。
可选地,Spring应用程序开发人员可以对其代码库添加注解,并使用诸如NullAway之类的构建插件,在构建期间在应用程序层面强制实施空值安全性(null-safety)。
指导原则
本节的目的是分享一些关于明确指定与Spring相关的库或应用程序的空值性(nullability)的指导原则。
J指定
默认为非空
需要理解的一个关键点是,在Java中,类型的“空”(null)状态默认是未知的,而非空(non-null)类型的用法远比可为空(nullable)类型的用法更为常见。为了保持代码库的可读性,我们通常希望默认情况下类型的使用都是非空的,除非在特定范围内明确标记为可为空。这正是@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。
数组和可变参数
在使用数组和可变参数(varargs)时,你需要能够区分数组中各个元素的空值状态与数组本身的空值状态。请注意Java规范所定义的语法,这种语法可能一开始会让人感到有些困惑。例如,在@NullMarked代码中:
-
@Nullable Object[] array表示数组中的各个元素可以为null,但数组本身不能为null。 -
Object @Nullable [] array表示数组中的各个元素不能为null,但数组本身可以为null。 -
@Nullable Object @Nullable [] array表示数组中的各个元素和数组本身都可以为null。
泛型
JSpec注释也适用于泛型。例如,在@NullMarked代码中:
-
List<String>表示一个非空元素的列表(相当于List<@NonNull String>) -
List<@Nullable String>表示一个可为空元素的列表
当声明泛型类型或泛型方法时,情况就有点复杂了。有关更多详细信息,请参阅相关的JSpecify generics文档。
嵌套和完全限定类型
Java规范还规定,使用@Target(ElementType.TYPE_USE)定义的注解(如JSpecify的@Nullable注解)必须声明在内部类型名或完全限定类型名的最后一个点(.)之后:
-
Cache.@Nullable ValueWrapper -
jakarta.validation.@Nullable Validator
NullAway
配置
推荐的配置是:
NullAway:OnlyNullMarked=true:仅对带有@NullMarked注释的包执行空值检查。NullAway:CustomContractAnnotations=org.springframework.lang.Contract:使NullAway能够识别org.springframework.lang包中的@Contract注释,该注释可用于表达补充语义,以避免在代码库中出现不相关的警告。
@Contract 声明的好处的一个很好的例子可以在 AssertNotNull() 中看到,该方法被注释为 @Contract("null, _ → fail")。通过这个合约声明,在成功调用 AssertNotNull() 之后,NullAway 将会理解作为参数传递的值不能为 null。
可选地,可以设置 NullAway:JSpecifyMode=true 以启用 对完整的 JSpecify 语义的检查,包括对数组、可变参数(varargs)和泛型(generics)的检测。请注意,这种模式 仍处于开发阶段,并且需要 JDK 22 或更高版本(通常需要结合使用 --release Java 编译器标志来配置预期的基准)。建议仅在确保代码库在使用本节前面提到的推荐配置时不会产生任何警告之后,再作为第二步启用 JSpecify 模式。
警告抑制
在某些情况下,NullAway 可能会错误地检测出空值问题(nullability issues)。在这些情况下,建议抑制相关的警告,并记录下问题的原因:
-
在字段、构造函数或类级别使用
@SuppressWarnings("NullAway.Init")可以避免因字段的延迟初始化而产生的不必要的警告——例如,当一个类实现了InitializingBean时。 -
当NullAway数据流分析无法检测到涉及空值问题的路径永远不会发生时,可以使用
@SuppressWarnings("NullAway") // Dataflow analysis limitation。 -
当NullAway没有考虑到lambda内部之外的断言对lambda代码路径的影响时,可以使用
@SuppressWarnings("NullAway") // Lambda。 -
对于一些已知会返回非空值的反射操作,可以使用
@SuppressWarnings("NullAway") // Reflection,即使API无法表达这一点。 -
当调用
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](https://github.com/uber/NullAway/issues/1075)。
从 Spring 的空值安全注解迁移
org.springframework.lang包中的Spring空值安全注解@Nullable、@NonNull、@NonNullApi和@NonNullFields是在Spring Framework 5中引入的,当时JSpecify还不存在。在那个阶段,最好的选择是利用JSR 305(一个虽然不活跃但应用广泛的JSR标准)中的元注解。从Spring Framework 7开始,这些注解被标记为过时,取而代之的是JSpecify注解。JSpecify注解带来了显著的改进,比如更明确的规范定义、没有包拆分问题的标准化依赖管理、更好的工具支持、更好的Kotlin集成能力,以及能够更精确地为更多用例指定空值属性。
一个关键的区别在于,Spring中已弃用的空值安全性注解遵循JSR 305的规范,这些注解可以应用于字段、参数和返回值;而JSpecify注解则适用于类型的使用。这种微妙的区别在实践中相当重要,因为它允许开发者区分元素本身的空值状态以及数组/可变参数的空值状态,同时还可以定义泛型类型的空值状态。
这意味着数组(array)和可变参数(varargs)的空指针安全性声明必须进行更新,以保持相同的语义。例如,使用Spring注解的@Nullable Object[] array需要更改为使用JSpecify注解的Object @Nullable [] array。对于可变参数(varargs)也是如此。
还建议将字段和返回值注释放在类型附近,并在同一行上,例如:
-
对于字段,不要使用Spring注解中的
@Nullable private String field,而应使用JSpecify注解中的private @Nullable String field。 -
对于方法返回类型,不要使用Spring注解中的
@Nullable public String method(),而应使用JSpecify注解中的public @Nullable String method()。
另外,使用JSpecify时,当你重写在父方法中被标记为@Nullable的类型用法时,无需指定@NonNull来“撤销”代码中关于可空性的声明。只需将其不加注释地声明即可,届时将适用默认的可空性规则(除非明确标注为可空,否则该类型用法被视为非空)。