null セーフ

Java では型システムで null 可能性マーカーを表現できませんが、Spring Framework のコードベースには JSpecify (英語) アノテーションが付与されており、API、フィールド、関連する型の使用における null 可能性を宣言しています。これらのアノテーションとセマンティクスに慣れるために、JSpecify ユーザーガイド (英語) を読むことを強くお勧めします。

この null 安全性の仕組みの主な目的は、ビルド時のチェックによって実行時に NullPointerException がスローされるのを防ぎ、明示的な null 可能性を用いて値が存在しない可能性を表現することです。これは、NullAway [GitHub] (英語) などの null 可能性チェッカーや、IntelliJ IDEA や Eclipse などの JSpecify アノテーションをサポートする IDE(後者は手動で設定が必要)を活用することで Java で役立ちます。Kotlin では、JSpecify アノテーションは自動的に Kotlin の null セーフティ (英語) に変換されます。

Nullness Spring API は、実行時に型の使用箇所、フィールド、メソッドの戻り値の型、パラメーターの null 性を検出するために使用できます。JSpecify アノテーション、Kotlin の null 安全性、Java プリミティブ型を完全にサポートし、パッケージに関係なく、あらゆる @Nullable アノテーションの実用的なチェックも提供します。

JSpecify アノテーションによるライブラリへのアノテーション

Spring Framework 7 以降、Spring Framework のコードベースは JSpecify アノテーションを活用して null 安全な API を公開し、ビルド時に NullAway [GitHub] (英語) との null 可能性宣言の整合性をチェックしています。Spring Framework および Spring ポートフォリオプロジェクトに依存する各ライブラリ、および Spring エコシステムに関連するその他のライブラリ(Reactor、Micrometer、Spring コミュニティプロジェクト)にも同様の実装が推奨されます。

Spring アプリケーションで JSpecify アノテーションを活用する

null 性アノテーションをサポートする IDE を使用してアプリケーションを開発すると、null 可能性契約が遵守されていない場合に Java で警告が、Kotlin でエラーが提供されるため、Spring アプリケーション開発者は null 処理を改善して実行時に NullPointerException がスローされるのを防ぐことができます。

オプションとして、Spring アプリケーション開発者はコードベースにアノテーションを付け、NullAway [GitHub] (英語) などのビルドプラグインを使用して、ビルド時にアプリケーションレベルで null 安全性を強制できます。

ガイドライン

このセクションの目的は、Spring 関連のライブラリまたはアプリケーションの null 可能性を明示的に指定するためのいくつかのガイドラインの提案を共有することです。

JSpecify

デフォルトは null 以外

理解すべき重要な点は、Java では型の null 性はデフォルトでは不明であり、null 非許容型の使用は null 許容型の使用よりもはるかに多いということです。コードベースの可読性を維持するためには、特定のスコープで null 許容としてマークされていない限り、型の使用はデフォルトで null 非許容型であると定義するのが一般的です。これはまさに @NullMarked (英語) の目的であり、Spring プロジェクトでは通常、package-info.java ファイルを介してパッケージレベルで設定されます。例:

@NullMarked
package org.springframework.core;

import org.jspecify.annotations.NullMarked;

明示的な null 可能性

@NullMarked コードでは、null 許容型の使用は @Nullable (英語) で明示的に定義されます。

JSpecify @Nullable / @NonNull アノテーションと他のほとんどのバリアントとの主な違いは、JSpecify アノテーションは @Target(ElementType.TYPE_USE) でメタアノテーションされているため、型の使用にのみ適用されることです。これは、関連する Java 仕様 [Oracle] (英語) に準拠するか、コードスタイルのベストプラクティスに従うかという点で、これらのアノテーションを配置する場所に影響します。スタイルの観点からは、これらのアノテーションをアノテーション対象の型と同じ行の直前に配置することで、型の使用という性質を受け入れることが推奨されます。

例: フィールドの場合:

private @Nullable String fileEncoding;

または、メソッドのパラメーターとメソッドの戻り値の型の場合:

public @Nullable String buildMessage(@Nullable String message,
                                     @Nullable Throwable cause) {
    // ...
}

メソッドをオーバーライドする場合、JSpecify アノテーションは元のメソッドから継承されません。つまり、実装をオーバーライドして同じ null 許容セマンティクスを維持したい場合は、JSpecify アノテーションをオーバーライド元のメソッドにコピーする必要があります。

一般的な使用例では、@NonNull (英語) @NullUnmarked (英語) が必要になることはほとんどありません。

配列と可変引数

配列と可変長引数を使用する場合、要素の null 値と配列自体の null 値を区別する必要があります。Java 仕様で定義されている [Oracle] (英語) 構文に注意してください。最初は驚くかもしれませんが、@NullMarked コードでは次のようになります。

  • @Nullable Object[] array は、個々の要素は null にできるが、配列自体は null にできないことを意味します。

  • Object @Nullable[] array は、個々の要素は null にできないが、配列自体は null にできることを意味します。

  • @Nullable Object @Nullable[] array は個々の要素と配列の両方が null になることを意味します。

汎用

JSpecify アノテーションはジェネリクスにも適用されます。例: @NullMarked コードの場合:

  • List<String> は非 NULL 要素のリストを意味する (List<@NonNull String> に相当する)

  • List<@Nullable String> は null 可能な要素のリストを意味します

ジェネリクス型やジェネリクスメソッドを宣言する場合は、少し複雑になります。詳細については、関連する JSpecify ジェネリクスドキュメント (英語) を参照してください。

ジェネリクス型とジェネリクスメソッドの null 可能性は、NullAway ではまだ完全にはサポートされていません [GitHub] (英語)

ネストされた型と完全修飾型

Java 仕様では、JSpecify の @Nullable アノテーションと同様に、@Target(ElementType.TYPE_USE) で定義されたアノテーションは、内部または完全修飾型名内の最後のドット (.) の後に宣言する必要があることも規定されています。

  • Cache.@Nullable ValueWrapper

  • jakarta.validation.@Nullable Validator

NullAway

構成

推奨される構成は次のとおりです。

  •  @NullMarked でアノテーションが付けられたパッケージに対してのみ、null 可能性チェックを実行するために NullAway:OnlyNullMarked=true を使用します。

  • NullAway:CustomContractAnnotations=org.springframework.lang.Contract は、NullAway に org.springframework.lang パッケージ内の @Contract (Javadoc) アノテーションを認識させ、コードベース内の無関係な警告を回避するために補完的なセマンティクスを表現するために使用できます。

@Contract 宣言の利点を示す良い例は、@Contract("null, _ → fail") でアノテーションされた Assert.notNull() (Javadoc) です。この契約宣言により、NullAway は Assert.notNull() の呼び出しが成功した後、パラメーターとして渡された値が null であってはならないことを理解します。

オプションとして、NullAway:JSpecifyMode=true を設定することで、配列、可変引数、ジェネリクスへのアノテーションを含む、JSpecify セマンティクス全体のチェック [GitHub] (英語) を有効にできます。このモードはまだ開発中 [GitHub] (英語) であり、JDK 22 以降が必要です(通常、期待されるベースラインを設定するために --release Java コンパイラーフラグと組み合わせて使用します)。このセクションで前述した推奨構成でコードベースが警告を生成しないことを確認した後、2 番目のステップとしてのみ JSpecify モードを有効にすることをお勧めします。

警告の抑制

NullAway が nullability の問題を誤って検出する有効なユースケースがいくつかあります。そのような場合は、関連する警告を抑制し、その理由をドキュメント化することをお勧めします。

  • フィールド、コンストラクター、クラスレベルでの @SuppressWarnings("NullAway.Init") を使用すると、フィールドの遅延初期化 (たとえば、InitializingBean (Javadoc) を実装するクラスによる) による不要な警告を回避できます。

  • @SuppressWarnings("NullAway") // Dataflow analysis limitation は、NullAway データフロー解析で、null 可能性の問題を含むパスが決して発生しないことを検出できない場合に使用できます。

  • @SuppressWarnings("NullAway") // Lambda は、NullAway がラムダ内のコードパスに対してラムダ外で実行されたアサーションを考慮しない場合に使用できます。

  • @SuppressWarnings("NullAway") // Reflection は、API で表現できない場合でも null 以外の値を返すことが知られている一部のリフレクション操作に使用できます。

  • @SuppressWarnings("NullAway") // Well-known map keys は、存在することが分かっているキーを使用して Map#get 呼び出しが実行され、以前に null 以外の関連値が挿入されている場合に使用できます。

  • @SuppressWarnings("NullAway") // Overridden method does not define nullability は、スーパークラスが null 可能性を定義していない場合 (通常、スーパークラスが外部依存関係から取得される場合) に使用できます。

  • @SuppressWarnings("NullAway") // See github.com/uber/NullAway/issues/1075 (英語)  は、NullAway がジェネリクスメソッド内の型変数の null を検出できない場合に使用できます。

Spring null-safety アノテーションからの移行

org.springframework.lang パッケージに含まれる Spring null 安全性アノテーション @Nullable (Javadoc) @NonNull (Javadoc) @NonNullApi (Javadoc) @NonNullFields (Javadoc) は、JSpecify が存在しなかった時代の Spring Framework 5 で導入されました。当時の最善の選択肢は、JSR 305(現在は休止状態ですが広く使用されている JSR)のメタアノテーションを活用することでした。これらのアノテーションは Spring Framework 7 以降非推奨となり、代わりに JSpecify (英語) アノテーションが採用されています。JSpecify (英語) アノテーションは、適切に定義された仕様、分割パッケージの課題のない標準的な依存関係、ツールの改善、Kotlin との統合強化、より多くのユースケースで null 可能性をより正確に指定する機能など、重要な機能強化を提供します。

重要な違いは、Spring の非推奨の null 安全性アノテーション(JSR 305 のセマンティクスに準拠)はフィールド、パラメーター、戻り値に適用されるのに対し、JSpecify アノテーションは型の使用に適用される点です。この微妙な違いは、開発者が要素の null 性と配列 / 可変長引数の null 性を区別し、ジェネリクス型の null 性を定義できるようになるため、実用上非常に重要です。

つまり、配列と可変長引数の null 安全性宣言は、同じセマンティクスを維持するために更新する必要があります。たとえば、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 アノテーションが付与された型使用をオーバーライドする際に、null マークされたコード内の nullable 宣言を「元に戻す」ために @NonNull を指定する必要はありません。アノテーションなしで宣言するだけで、null マークされたデフォルトが適用されます(明示的に nullable としてアノテーションが付与されていない限り、型使用は null ではないとみなされます)。