使用 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 社群所有即將到來的活動。

檢視全部