空安全
儘管 Java 尚未透過其型別系統表達空值標記,但 Spring Framework 的程式碼庫已使用 JSpecify 註解進行標註,以宣告其 API、欄位和相關型別用法的空值性。強烈建議閱讀 JSpecify 使用者指南,以便熟悉這些註解及其語義。
此空安全安排的主要目標是透過構建時檢查防止在執行時丟擲 NullPointerException,並使用顯式空值性來表達值可能缺失。這在 Java 中透過利用空值檢查器(如 NullAway)或支援 JSpecify 註解的 IDE(如 IntelliJ IDEA 和 Eclipse,後者需要手動配置)非常有用。在 Kotlin 中,JSpecify 註解會自動轉換為 Kotlin 的空安全。
執行時可使用 Nullness Spring API 來檢測型別用法、欄位、方法返回型別或引數的空值性。它全面支援 JSpecify 註解、Kotlin 空安全和 Java 基本型別,並對任何 @Nullable 註解(無論其包)進行實用檢查。
使用 JSpecify 註解標註庫
自 Spring Framework 7 起,Spring Framework 程式碼庫利用 JSpecify 註解來暴露空安全 API,並使用 NullAway 作為其構建的一部分來檢查這些空值性宣告的一致性。建議依賴 Spring Framework 和 Spring 組合專案以及其他與 Spring 生態系統相關的庫(Reactor、Micrometer 和 Spring 社群專案)也這樣做。
在 Spring 應用程式中利用 JSpecify 註解
使用支援空值性註解的 IDE 開發應用程式時,當空值性契約未被遵守時,將在 Java 中提供警告,在 Kotlin 中提供錯誤,從而使 Spring 應用程式開發人員能夠完善其空值處理,以防止在執行時丟擲 NullPointerException。
此外,Spring 應用程式開發人員可以選擇標註其程式碼庫並使用構建外掛(如 NullAway)在構建時在應用程式級別強制執行空安全。
指南
本節的目的是分享一些建議,用於明確指定 Spring 相關庫或應用程式的空值性。
JSpecify
預設為非空
需要理解的關鍵一點是,Java 中型別的空值性預設是未知的,並且非空型別用法遠比可空用法更常見。為了保持程式碼庫的可讀性,我們通常希望預設情況下型別用法是非空的,除非在特定範圍內標記為可空。這正是 @NullMarked 的目的,它通常在 Spring 專案中透過 package-info.java 檔案在包級別設定,例如
@NullMarked
package org.springframework.core;
import org.jspecify.annotations.NullMarked;
顯式空值性
在 @NullMarked 程式碼中,可空型別用法使用 @Nullable 顯式定義。
JSpecify 的 @Nullable / @NonNull 註解與大多數其他變體的一個關鍵區別是,JSpecify 註解透過 @Target(ElementType.TYPE_USE) 進行元註解,因此它們僅適用於型別用法。這影響了這些註解的放置位置,無論是為了遵守 相關的 Java 規範 還是為了遵循程式碼風格最佳實踐。從風格角度來看,建議透過將這些註解放置在與被註解型別相同的行上並緊跟其前,來體現其型別用法的性質。
例如,對於欄位
private @Nullable String fileEncoding;
或對於方法引數和方法返回型別
public @Nullable String buildMessage(@Nullable String message,
@Nullable Throwable cause) {
// ...
}
|
當重寫方法時,JSpecify 註解不會從原始方法繼承。這意味著如果您想重寫實現並保持相同的空值性語義,JSpecify 註解應該複製到重寫方法中。 |
對於典型用例,@NonNull 和 @NullUnmarked 很少需要。
陣列和可變引數
對於陣列和可變引數,您需要能夠區分元素的空值性與陣列本身的空值性。請注意 Java 規範定義的語法,這最初可能會令人驚訝。例如,在 @NullMarked 程式碼中
-
@Nullable Object[] array表示單個元素可以是null,但陣列本身不能。 -
Object @Nullable [] array表示單個元素不能是null,但陣列本身可以是。 -
@Nullable Object @Nullable [] array表示單個元素和陣列都可以是null。
泛型
JSpecify 註解也適用於泛型。例如,在 @NullMarked 程式碼中
-
List<String>表示一個非空元素的列表(等同於List<@NonNull String>) -
List<@Nullable String>表示一個可空元素的列表
當宣告泛型型別或泛型方法時,情況會稍微複雜一些。有關更多詳細資訊,請參閱相關的 JSpecify 泛型文件。
| 泛型型別和泛型方法的空值性 尚未完全被 NullAway 支援。 |
NullAway
配置
推薦的配置是
-
NullAway:OnlyNullMarked=true,以便僅對用@NullMarked標註的包執行空值性檢查。 -
NullAway:CustomContractAnnotations=org.springframework.lang.Contract,這使得 NullAway 能夠識別org.springframework.lang包中的 @Contract 註解,該註解可用於表達補充語義,以避免程式碼庫中不相關的警告。
@Contract 宣告的一個很好的例子是 Assert.notNull(),它被註解為 @Contract("null, _ → fail")。有了這個契約宣告,NullAway 將理解在成功呼叫 Assert.notNull() 後,作為引數傳遞的值不能為 null。
可選地,可以將 NullAway:JSpecifyMode=true 設定為啟用 對完整 JSpecify 語義的檢查,包括陣列、可變引數和泛型上的註解。請注意,此模式 仍在開發中,需要 JDK 22 或更高版本(通常與 --release Java 編譯器標誌結合使用以配置預期的基線)。建議僅在第二步啟用 JSpecify 模式,即在確保程式碼庫在使用本節前面提到的推薦配置下不產生任何警告之後。
警告抑制
在一些有效的使用場景中,NullAway 會錯誤地檢測到空值問題。在這種情況下,建議抑制相關警告並記錄原因
-
在欄位、建構函式或類級別使用
@SuppressWarnings("NullAway.Init")可以避免由於欄位的延遲初始化而產生的多餘警告——例如,由於類實現了InitializingBean。 -
當 NullAway 資料流分析無法檢測到涉及空值問題的路徑永遠不會發生時,可以使用
@SuppressWarnings("NullAway") // Dataflow analysis limitation。 -
當 NullAway 不考慮在 lambda 外部為 lambda 內的程式碼路徑執行的斷言時,可以使用
@SuppressWarnings("NullAway") // Lambda。 -
對於某些已知會返回非空值(即使 API 無法表達)的反射操作,可以使用
@SuppressWarnings("NullAway") // Reflection。 -
當
Map#get呼叫使用已知存在的鍵並且之前已插入非空相關值時,可以使用@SuppressWarnings("NullAway") // Well-known map keys。 -
當超類未定義空值性時(通常當超類來自外部依賴時),可以使用
@SuppressWarnings("NullAway") // Overridden method does not define nullability。 -
當 NullAway 無法檢測泛型方法中的型別變數空值性時,可以使用
@SuppressWarnings("NullAway") // See github.com/uber/NullAway/issues/1075。
從 Spring 空安全註解遷移
Spring 空安全註解 @Nullable、@NonNull、@NonNullApi 和 @NonNullFields 在 Spring Framework 5 中引入,當時 JSpecify 尚不存在,最好的選擇是利用 JSR 305(一個休眠但廣泛使用的 JSR)的元註解。自 Spring Framework 7 起,它們已被棄用,轉而使用 JSpecify 註解,後者提供了顯著的增強,例如定義明確的規範、沒有拆分包問題的規範依賴、更好的工具、更好的 Kotlin 整合以及針對更多用例更精確地指定空值性的能力。
一個關鍵區別是,Spring 已棄用的空安全註解(遵循 JSR 305 語義)適用於欄位、引數和返回值;而 JSpecify 註解適用於型別用法。這種微妙的差異在實踐中非常重要,因為它允許開發人員區分元素和陣列/可變引數的空值性,以及定義泛型型別的空值性。
這意味著陣列和可變引數的空安全宣告必須更新以保持相同的語義。例如,帶有 Spring 註解的 @Nullable Object[] array 需要更改為帶有 JSpecify 註解的 Object @Nullable [] array。可變引數也適用相同規則。
還建議將欄位和返回值註解更靠近型別並放在同一行,例如
-
對於欄位,使用 Spring 註解時是
@Nullable private String field,而使用 JSpecify 註解時是private @Nullable String field。 -
對於方法返回型別,使用 Spring 註解時是
@Nullable public String method(),而使用 JSpecify 註解時是public @Nullable String method()。
此外,使用 JSpecify 時,在覆蓋超類方法中帶有 @Nullable 註解的型別用法時,您無需指定 @NonNull 來“取消”空值標記程式碼中的可空宣告。只需將其宣告為未註解,空值標記的預設值將適用(除非明確標註為可空,否則型別用法被視為非空)。