Skip to content

空安全 (Null-safety)

虽然 Java 本身还不支持在类型系统中直接表达空标记(nullness markers),但 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 开始,代码库全面采用 JSpecify 注解来公开空安全 API,并在构建过程中使用 NullAway 检查这些可空性声明的一致性。建议所有依赖于 Spring Framework、Spring 家族项目以及 Spring 生态系统相关库(如 Reactor、Micrometer 等)的项目也照此办理。

在 Spring 应用中利用 JSpecify 注解

使用支持空性注解的 IDE 开发应用时,如果违反了可空性契约,Java 中会显示警告,Kotlin 中则会报错。这能让 Spring 应用开发者优化其空值处理逻辑,从而防止运行时抛出 NullPointerException

此外,开发者还可以选择标注自己的代码库,并使用 NullAway 等构建插件在构建阶段强制执行应用级别的空安全检查。

指南 (Guidelines)

本节分享了一些为 Spring 相关库或应用指定可空性的建议指南。

JSpecify

默认非空 (Defaults to non-null)

理解 Java 类型默认空性未知(unknown)至关重要,而且非空类型用法比可空用法频繁得多。为了保持代码可读性,我们通常希望默认定义类型用法为非空,除非在特定范围内将其标记为可空。这正是 @NullMarked 的作用。在 Spring 项目中,通常通过 package-info.java 文件在包级别设置此注解,例如:

java
@NullMarked
package org.springframework.core;

import org.jspecify.annotations.NullMarked;

显式可空性 (Explicit nullability)

在标注了 @NullMarked 的代码中,可空类型用法通过 @Nullable 显式定义。

JSpecify 的 @Nullable / @NonNull 注解与其他变体的一个关键区别在于:JSpecify 注解设置了 @Target(ElementType.TYPE_USE),因此它们仅应用于类型用法。这影响了注解的放置位置。从风格角度建议:由于这些注解具有类型用法(Type-use)性质,应将它们与被标注类型放在同一行,并紧接在类型之前。

示例(字段):

java
private @Nullable String fileEncoding;

示例(方法参数和返回值):

java
public @Nullable String buildMessage(@Nullable String message,
                                     @Nullable Throwable cause) {
    // ...
}

TIP

在重写(Override)方法时,JSpecify 注解不会从原方法继承。这意味着如果你想重写实现并保持相同的可空性语义,必须将 JSpecify 注解复制到重写后的方法上。

在典型用例中,很少需要用到 @NonNull@NullUnmarked

数组与可变参数 (Arrays and varargs)

对于数组和可变参数,你需要区分“元素本身的可空性”和“数组容器本身的可空性”。请注意 Java 规范定义的语法,初看可能会感到意外。在 @NullMarked 代码中:

  • @Nullable Object[] array:表示元素可以为 null,但数组本身不能为空。
  • Object @Nullable [] array:表示元素不能为空,但数组本身可以为 null。
  • @Nullable Object @Nullable [] array:表示元素和数组本身都可以为 null。

泛型 (Generics)

JSpecify 注解同样适用于泛型。在 @NullMarked 代码中:

  • List<String>:表示一个包含非空元素的列表(等同于 List<@NonNull String>)。
  • List<@Nullable String>:表示一个包含可空元素的列表。

在声明泛型类型或泛型方法时情况会更复杂,详见 JSpecify 泛型文档

WARNING

泛型类型和泛型方法的空安全性目前尚未被 NullAway 完整支持

嵌套类型与全限定名

Java 规范强制要求:对于带有 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 注解,用于表达补充语义以避免误报。

例如,Assert.notNull() 标注了 @Contract("null, _ -> fail")。有了这个声明,NullAway 会理解:在成功调用该方法后,传入的参数绝不可能为 null。

抑制警告 (Warnings suppression)

在某些有效场景下,NullAway 可能会误报空性问题。建议抑制相关警告并记录原因:

  • @SuppressWarnings("NullAway.Init"):用于延迟初始化的字段(如实现了 InitializingBean 的类)。
  • @SuppressWarnings("NullAway") // Dataflow analysis limitation:数据流分析无法识别某些路径永远不会发生时。
  • @SuppressWarnings("NullAway") // Lambda:NullAway 未考虑到 Lambda 外部执行的断言。

从 Spring 旧版空安全注解迁移

org.springframework.lang 包中的旧版注解(@Nullable, @NonNull, @NonNullApi, @NonNullFields)是在 JSpecify 出现之前的 Spring 5 中引入的。它们在 Spring Framework 7 中已被弃用,取而代之的是 JSpecify 注解。JSpecify 提供了更完善的规范、无冲突的规范依赖、更好的工具支持以及对泛型更精确的控制。

关键区别:

  • 作用目标:旧版注解应用于字段、参数和返回值;而 JSpecify 应用于类型用法 (Type Use)
  • 数组语法:旧版的 @Nullable Object[] array 需要更新为 JSpecify 的 Object @Nullable [] array 才能保持相同的数组可空语义。
  • 代码风格:建议将注解移到紧贴类型的位置。例如:
    • 字段:从 @Nullable private String field 变更为 private @Nullable String field
    • 返回值:从 @Nullable public String method() 变更为 public @Nullable String method()
  • 简化:在 JSpecify 中,重写 @Nullable 方法时,如果想改为非空,只需不加任何注解即可(在 @NullMarked 的默认规则下生效),无需再显式加 @NonNull

补充教学

1. 为什么要从 JSR 305 切换到 JSpecify?

长期以来,Java 社区一直受困于“分裂”的空安全注解(各种包名下的 @Nullable)。

  • JSR 305 的尴尬:Spring 5 使用的底层规范早已停滞。
  • JSpecify 的统一:JSpecify 是由 Google、JetBrains、Oracle 和 Spring 团队共同推动的行业标准。它不仅仅是几个注解,而是一套严谨的类型推导规范。

2. “类型用法 (Type Use)” 是什么黑科技?

这是 JSpecify 最核心的进步。

  • 旧版问题@Nullable String[] list 到底是列表能空,还是里面的字符串能空?语法上非常模糊。
  • JSpecify 方案:利用 Java 8 引入的 TYPE_USE@Nullable String @Nullable [] list
    • 靠近 String 的注解管元素。
    • 靠近 [] 的注解管容器。 这种精确度是防止复杂集合操作中出现 NPE 的关键。

3. 实战:如何开启全局非空检查?

在 Spring Boot 3.4+ / Spring 7 项目中,推荐的做法是:

  1. 在每个顶级包下创建 package-info.java
  2. 加上 @NullMarked 注解。
  3. 这样你的 IDE 会立即帮你标出所有“可能没处理空值”的地方。

对于 Kotlin 开发者来说,这简直是福音:Spring 的 Java API 在 Kotlin 中将不再是 String!(平台类型),而是直接确定为 StringString?,让编译器帮你守好最后一道防线。

4. 关于迁移的建议

如果你正在维护一个老项目:

  • 不要急着批量替换。旧版注解在 Spring 7 中依然有效,只是被标记为 @Deprecated
  • 优先在新代码中使用 JSpecify。IDE 通常能同时理解这两套规范。
  • 注意数组语法的变更:这是迁移中最容易出错的一点,建议配合 NullAway 等静态分析工具进行“查漏补缺”。

Based on Spring Framework.