空安全

尽管 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 支持

嵌套和完全限定类型

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 声明的好处可以在 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 [role="bare"][role="bare"][role="bare"]github.com/uber/NullAway/issues/1075 可用于当 NullAway 无法检测泛型方法中的类型变量空值性时。

从 Spring 空安全注解迁移

Spring 空安全注解 @Nullable@NonNull@NonNullApi@NonNullFieldsorg.springframework.lang 包中,是在 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 来“撤销”空值标记代码中的可空性声明。只需将其声明为未标注,然后将应用空值标记的默认值(除非显式标注为可空,否则类型用法被视为非空)。