空安全 (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 文件在包级别设置此注解,例如:
@NullMarked
package org.springframework.core;
import org.jspecify.annotations.NullMarked;显式可空性 (Explicit nullability)
在标注了 @NullMarked 的代码中,可空类型用法通过 @Nullable 显式定义。
JSpecify 的 @Nullable / @NonNull 注解与其他变体的一个关键区别在于:JSpecify 注解设置了 @Target(ElementType.TYPE_USE),因此它们仅应用于类型用法。这影响了注解的放置位置。从风格角度建议:由于这些注解具有类型用法(Type-use)性质,应将它们与被标注类型放在同一行,并紧接在类型之前。
示例(字段):
private @Nullable String fileEncoding;示例(方法参数和返回值):
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 ValueWrapperjakarta.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 项目中,推荐的做法是:
- 在每个顶级包下创建
package-info.java。 - 加上
@NullMarked注解。 - 这样你的 IDE 会立即帮你标出所有“可能没处理空值”的地方。
对于 Kotlin 开发者来说,这简直是福音:Spring 的 Java API 在 Kotlin 中将不再是 String!(平台类型),而是直接确定为 String 或 String?,让编译器帮你守好最后一道防线。
4. 关于迁移的建议
如果你正在维护一个老项目:
- 不要急着批量替换。旧版注解在 Spring 7 中依然有效,只是被标记为
@Deprecated。 - 优先在新代码中使用 JSpecify。IDE 通常能同时理解这两套规范。
- 注意数组语法的变更:这是迁移中最容易出错的一点,建议配合 NullAway 等静态分析工具进行“查漏补缺”。