领先一步
VMware 提供培训和认证,助您加速进步。
了解更多注意:后续博文使用Spring Boot 4实现空安全应用已发布。
Spring中空安全支持的最初引入可以追溯到2017年Spring Framework 5.0的发布。2025年,我们正在改进这一功能,为Java或Kotlin的Spring开发者带来更多附加值。但在深入了解我们正在进行的更改之前,让我解释一下我们为什么要这样做以及预期的好处。
让我们举一个具体的例子,假设我们正在使用一个提供了 TokenExtractor 接口的库,该接口定义如下:
interface TokenExtractor {
/**
* Extract a token from a {@link String}.
* @param input the input to process
* @return the extracted token
*/
String extractToken(String input);
}
如果由于某种原因,实现返回了 null,那么像下面这样访问 token.length() 中的 null 引用就会导致 NullPointerException,这通常会在运行时产生一个带有 500 Internal Server Error 状态码的 HTTP 响应。
package com.example;
String token = extractor.extractToken("...");
System.out.println("The token has a length of " + token.length());
由于这种错误只在某些情况下(例如,在未经过测试的特定输入下)才会发生,因此它可能在生产环境中很晚才被发现,从而导致最终用户沮丧,甚至阻止交易的发生,降低公司收入,损害品牌,并涉及修复的延迟和成本。
这种错误非常常见,以至于 null 引用的发明者 Tony Hoare 曾夸张地为自己的发明道歉,称之为“我的十亿美元错误”。但正如 Kotlin 所出色地证明的那样,根本问题并非 null 引用本身,而是它们未在类型系统中明确指定。
在 Java 中,非原始类型使用的空性未指定。参数可能接受或不接受 null 参数。返回值可能是可空的或非空的。你不知道,必须依赖阅读 Javadoc 或分析实现才能弄清楚。但即使库作者文档化了,它通常在所有 API 中也不一致,通常没有自动化检查,你无法真正知道参数/返回值是否真的是非空的,或者库作者是否只是忘记文档化它是可空的。这本身就容易出错,并且你没有适当的方法来解决这个问题。
解决这个潜在问题的方案是,使所有 API 的类型使用空性明确化,并在我们的 IDE 和构建中进行相关的自动一致性检查。由于 Java 尚未提供 空限制和可空类型,我们需要一种方法来指定 Spring API 的空性。
2017 年,我们选择引入 Spring 可空性注解,它们建立在 JSR 305(一个休眠但广泛使用的 JSR)语义和注解之上。由于技术限制、不明确的状态、缺乏适当的规范,它远非完美,但它是我们当时确定的最佳实用选择。Spring 团队随后加入了一个由 Google 领导的工作组,汇集了 JVM 生态系统中多家公司,如 JetBrains、Oracle、Uber、VMware/Broadcom 等,以设计并贡献一个不依赖于特定验证工具的更好解决方案。这就是 JSpecify 的开端。
我经常观察到一个关于空性的误解是,起初,你可能会觉得它主要是关于选择 众多 @Nullable 变体 中的一种,但这只是冰山一角。这些注解需要有适当的规范、工具支持等。以协作方式就共同的空性规范达成一致是 JSpecify 花费数年才达到 1.0 的原因。
JSpecify 是一套注解、规范和文档,旨在通过 NullAway 等工具,在 IDE 或编译期间确保 Java 应用程序和库的空安全。
一个需要理解的关键点是,默认情况下,Java 中类型使用的空性是未指定的,并且非空类型使用的频率远高于可空类型。为了保持代码库的可读性,我们通常希望默认在特定范围内,类型使用是非空的,除非被标记为可空。这正是 @NullMarked 的目的,它通常通过 package-info.java 文件在包级别设置,例如:
@NullMarked
package org.example;
import org.jspecify.annotations.NullMarked;
此注解将类型使用的默认空性从“未指定”(Java 默认)更改为“非空”(JSpecify @NullMarked 默认)。因此,我们现在可以相应地完善我们的 API 和文档。
package org.example;
interface TokenExtractor {
/**
* Extract a token from a {@link String}.
* @param input the input to process
* @return the extracted token or {@code null} if not found
*/
@Nullable String extractToken(String input);
}
现在,当在返回值上调用方法时,IDE 会正确地警告我们可能存在 NullPointerException,如果传入 null 参数,也会抱怨,因为此空标记代码默认为非空。

虽然我们可以忽略或错过这些 IDE 警告,但可以通过配置为抛出错误的 NullAway 在构建时检查代码库中空注解的一致性。如果发现不一致,构建将中断,从而在设计上防止发布空不安全的 API(除了来自第三方依赖项的未注解类型)。
> Task :compileJava FAILED
/Users/sdeleuze/workspace/jspecify-nullway-demo/src/main/java/org/example/Main.java:7: error: [NullAway] dereferenced expression token is @Nullable
System.out.println("The token has a length of " + token.length());
^
(see http://t.uber.com/nullaway )
1 error
如果你想自己尝试或查看相关 Gradle 构建 的示例,请参阅 https://github.com/sdeleuze/jspecify-nullway-demo。
这些空性错误强制使用这些 API 的开发人员显式处理空引用。
String token = extractor.extractToken("...");
if (token == null) {
System.out.println("No token found");
}
else {
System.out.println("The token has a length of " + token.length());
}
你可能会反对说 Java 的 Optional<T> 旨在表达值的存在与否。但在实践中,Optional<T> 在许多用例中不可用,因为它会引入运行时开销(至少在 Project Valhalla 值类可用之前),增加了代码和 API 的复杂性,不适合作为参数,并且会破坏现有的 API 签名。
Spring Framework 7(目前处于里程碑阶段)已经将其整个代码库切换到 JSpecify。你可以在此处找到相关文档。与之前的版本相比,一个关键的改进是空性现在也为数组/可变参数元素以及泛型类型指定。这对于 Java 开发人员来说非常棒,对于 Kotlin 开发人员来说也是如此,他们将看到惯用的空安全 API,就像 Spring 是用 Kotlin 编写的一样。
但最大的改进是,整个 Spring 团队目前正在努力在整个 Spring 产品组合中提供空安全 API,并进行相关的构建时检查以确保一致性。这是一个持续进行的过程,目前还不能保证在 Spring Boot 4.0 于 11 月发布时能够完成,但我们会尽可能接近完全覆盖。Project Reactor 和 Micrometer 也在范围之内。
当 Spring Boot 4 发布并在您的应用程序中使用时,特别是如果您也在应用程序级别启用这些空性检查,生产环境中的 NullPointerException 风险将大大降低甚至消除,因为它将仅存在于来自第三方库的类型。通过明确指定空引用可能发生的位置、处理这些代码路径以及引入相关的自动检查,我们将“十亿美元的错误”转化为零成本抽象,允许表达值的潜在缺失,从而显著提高 Spring 应用程序的安全性。