領先一步
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 應用程式的安全性。