使用JSpecify和NullAway實現Spring應用中的空安全

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

注意:後續博文使用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 中也不一致,通常沒有自動化檢查,你無法真正知道引數/返回值是否真的是非空的,或者庫作者是否只是忘記文件化它是可空的。這本身就容易出錯,並且你沒有適當的方法來解決這個問題。

JSpecify 和 NullAway

解決這個潛在問題的方案是,使所有 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 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 開發人員來說也是如此,他們將看到慣用的空安全 API,就像 Spring 是用 Kotlin 編寫的一樣。

但最大的改進是,整個 Spring 團隊目前正在努力在整個 Spring 產品組合中提供空安全 API,並進行相關的構建時檢查以確保一致性。這是一個持續進行的過程,目前還不能保證在 Spring Boot 4.0 於 11 月釋出時能夠完成,但我們會盡可能接近完全覆蓋。Project ReactorMicrometer 也在範圍之內。

當 Spring Boot 4 釋出並在您的應用程式中使用時,特別是如果您也在應用程式級別啟用這些空性檢查,生產環境中的 NullPointerException 風險將大大降低甚至消除,因為它將僅存在於來自第三方庫的型別。透過明確指定空引用可能發生的位置、處理這些程式碼路徑以及引入相關的自動檢查,我們將“十億美元的錯誤”轉化為零成本抽象,允許表達值的潛在缺失,從而顯著提高 Spring 應用程式的安全性。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有