抢先一步
VMware 提供培训和认证,助力您加速成长。
了解更多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()
中的空引用会导致 NullPointerException
,这通常会在运行时导致 HTTP 响应的状态码为 500 Internal Server Error
。
package com.example;
String token = extractor.extractToken("...");
System.out.println("The token has a length of " + token.length());
由于这种错误只可能在某些情况下发生(例如使用某些未经过测试的特定输入),它可能在生产环境中相当晚才被发现,从而引起最终用户的不满,甚至阻止交易发生,减少公司收入,损害品牌形象,并且修复起来耗时费力。
这种错误如此频繁,以至于空引用的发明者本人 Tony Hoare 夸张地为此发明道歉,称其为“我的十亿美元错误”。但正如 Kotlin 精彩地证明的那样,根本问题不在于空引用本身,而在于它们没有在类型系统中显式指定。
在 Java 中,非原始类型使用中的空性(nullness)是未指定的。一个参数可能接受或不接受 null 参数。返回值可能是可空的或非空的。你无从得知,只能依赖阅读 Javadoc 或分析实现来弄清楚。但即使库作者对此进行了文档说明,通常在所有 API 中也并非一致,通常没有自动化检查,你无法真正知道一个参数/返回值是否真的非空,或者库作者是否只是忘记了文档说明它是可空的。这种设计本身就容易出错,并且你没有适当的方法来解决这个问题。
解决这个隐患问题的方案是使所有 API 的类型使用空性明确化,并在我们的 IDE 和构建中进行相关的自动化一致性检查。由于 Java 尚未提供 空限制和可空类型,我们需要一种方法来指定 Spring API 的空性。
在 2017 年,我们选择引入 Spring 可空性注解,这些注解是基于 JSR 305(一个不活跃但广泛使用的 JSR)的语义和注解构建的。由于技术限制、状态不明确、缺乏适当的规范,它远非完美,但那是我们在当时能确定的最佳务实选择。随后,Spring 团队加入了一个由 Google 牵头的工作组,汇集了 JetBrains、Oracle、Uber、VMware/Broadcom 等多家投入 JVM 生态的公司,共同设计和贡献一个不依赖于特定验证工具的更好解决方案。这标志着 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
参数,也会发出警告,因为这段被标记为 null-marked 的代码默认是非空的。
虽然我们可以忽略或漏掉这些 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 开发者来说也是如此,他们将看到像 Spring 用 Kotlin 编写一样惯用的空安全 API。
但最大的改进是整个 Spring 团队目前正在努力在整个 Spring 产品组合中提供空安全 API,并进行相关的构建时检查以确保一致性。这是一个持续进行的过程,目前还不能保证在 11 月发布 Spring Boot 4.0 时能完全完成,但我们正在努力尽可能接近全面覆盖。Project Reactor 和 Micrometer 也在此范围内。
当 Spring Boot 4 发布并在您的应用中使用时,特别是如果您也在应用级别启用这些空性检查,生产环境中的 NullPointerException
风险将大大降低,甚至消除,因为它只会发生在来自第三方库的类型上。通过明确指定空引用可能发生的位置,处理这些代码路径,并引入相关的自动化检查,我们将“十亿美元的错误”转变为零成本的抽象,从而能够表达值可能不存在的情况,显著提高了 Spring 应用的安全性。