Null-safety
尽管Java目前还不允许你使用类型系统表达空值标记,但Spring框架的代码库使用了JSpecify注解来声明其API、字段及相关类型使用的可空性。为了熟悉这些注解及其语义,强烈建议阅读JSpecify用户指南。
此空安全机制的主要目标是通过构建时检查防止在运行时抛出NullPointerException,并利用明确的可空性来表达值可能缺失的情况。
这对于Java特别有用,可通过利用如NullAway这样的空指针检查器或支持JSpecify注解的IDE(如IntelliJ IDEA和Eclipse,后者需要手动配置)来实现。在Kotlin中,JSpecify注解会自动转换为Kotlin的空安全特性。
Spring APINullness 可在运行时用于检测类型使用、字段、方法返回类型或参数的空值状态。它全面支持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的目的,它通常通过一个package-info.java文件在Spring项目中以包级别设置,例如:
@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规范中定义的语法,这可能初看有些意外。例如,在Java代码中:
-
@Nullable Object[] array表示单个元素可以为null,但数组本身不可以。 -
Object @Nullable [] array表示单个元素不能为null,但数组本身可以。 -
@Nullable Object @Nullable [] array表示个体元素和数组都可以为null。
泛型
JSpecify注解同样适用于泛型。例如,在Java代码中:
-
List<String>表示一个非空元素的列表(等同于List<@NonNull String>) -
List<@Nullable String>表示一个可包含空值元素的列表
当声明泛型类型或泛型方法时,情况会稍微复杂一些。请参阅相关的 JSpecify泛型文档以获取更多详细信息。
| 泛型类型和泛型方法的空值属性 尚未被NullAway完全支持. |
NullAway
配置
推荐的配置是:
-
NullAway:OnlyNullMarked=true以便仅对标注有@NullMarked的包执行空值检查。 -
NullAway:CustomContractAnnotations=org.springframework.lang.Contract使得 NullAway 能够识别 @Contract 注解在org.springframework.lang包中,可用于表达互补的语义,以避免代码库中的不相关警告。
一个关于@Contract声明好处的好例子可以在
Assert.notNull()
中看到,它被@Contract("null, _ → fail")注解。有了那个合同声明,NullAway就能理解
在成功调用Assert.notNull()后,作为参数传递的值不能为null。
可选地,可以设置NullAway:JSpecifyMode=true以启用
对JSpecify完整语义的检查,包括数组、可变参数和泛型上的注解。请注意,此模式仍
在开发中,并要求使用
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 包中是在 JSpecify 尚未存在时,Spring Framework 5 引入的。当时最好的选择是利用 JSR 305(一个休眠但广泛应用的 JSR)中的元注解。随着 Spring Framework 7,这些注解已被弃用,推荐使用
JSpecify 注解,后者提供了显著增强功能,包括明确的规范定义、无分割包问题的标准依赖、更佳的工具支持、与 Kotlin 更好的集成,以及为更多使用场景提供更精确的空值性指定能力。
一个主要的区别是,Spring 中已废弃的空安全注解遵循 JSR 305 语义,应用于字段、参数和返回值;而 JSpecify 注解则应用于类型使用。这一细微差别在实际应用中意义重大,因为它允许开发者区分元素的空值状态与数组/可变参数的空值状态,并且能够定义泛型类型的空值性。
这意味着数组和可变参数的空安全性声明必须更新以保持相同的语义。例如,使用Spring注解的
@Nullable Object[] array需要更改为使用JSpecify注解的Object @Nullable [] array。这对于可变参数也同样适用。
同时,建议将字段和返回值注解移至类型附近,并保持在同一行上,例如:
-
对于字段,使用JSpecify注解时请输入
private @Nullable String field,而不是Spring注解中的@Nullable private String field。 -
对于方法返回类型,使用JSpecify注解时请改用
public @Nullable String method(),而非Spring注解中的@Nullable public String method()。
此外,使用JSpecify时,您无需在覆盖使用@Nullable注解的类型时指定@NonNull
以在标记为null的代码中“撤销”可空声明。只需声明为未注解,null标记的默认设置将适用(类型使用默认视为非null,除非明确注解为可null)。