使用 JSpecify 和 NullAway 实现 Spring 应用中的空安全

工程 | Sébastien Deleuze | 2025 年 3 月 10 日 | ...

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 中也并非一致,通常没有自动化检查,你无法真正知道一个参数/返回值是否真的非空,或者库作者是否只是忘记了文档说明它是可空的。这种设计本身就容易出错,并且你没有适当的方法来解决这个问题。

JSpecify 和 NullAway

解决这个隐患问题的方案是使所有 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 null safety warning

虽然我们可以忽略或漏掉这些 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 即将发布的重大版本中的下一级空安全

Spring Framework 7(目前处于里程碑阶段)已将其整个代码库切换到 JSpecify。您可以在此处找到相关文档。与之前版本相比的一个关键改进是,现在还为数组/可变参数元素以及泛型类型指定了空性。这对 Java 开发者来说很棒,对于 Kotlin 开发者来说也是如此,他们将看到像 Spring 用 Kotlin 编写一样惯用的空安全 API。

但最大的改进是整个 Spring 团队目前正在努力在整个 Spring 产品组合中提供空安全 API,并进行相关的构建时检查以确保一致性。这是一个持续进行的过程,目前还不能保证在 11 月发布 Spring Boot 4.0 时能完全完成,但我们正在努力尽可能接近全面覆盖。Project ReactorMicrometer 也在此范围内。

当 Spring Boot 4 发布并在您的应用中使用时,特别是如果您也在应用级别启用这些空性检查,生产环境中的 NullPointerException 风险将大大降低,甚至消除,因为它只会发生在来自第三方库的类型上。通过明确指定空引用可能发生的位置,处理这些代码路径,并引入相关的自动化检查,我们将“十亿美元的错误”转变为零成本的抽象,从而能够表达值可能不存在的情况,显著提高了 Spring 应用的安全性。

获取 Spring 通讯

订阅 Spring 通讯,保持连接

订阅

抢先一步

VMware 提供培训和认证,助力您加速成长。

了解更多

获取支持

Tanzu Spring 通过简单的订阅,为 OpenJDK™、Spring 和 Apache Tomcat® 提供支持和二进制文件。

了解更多

即将到来的活动

查看 Spring 社区所有即将到来的活动。

查看全部