JSpecify とは?
JSpecify とは、Java の nullability アノテーションを標準化するためのオープンソースプロジェクトです。
Java エコシステムでは従来、@Nullable や @NonNull といったアノテーションが JetBrains、FindBugs、Checker Framework など複数のライブラリでバラバラに定義されており、互換性の問題がありました。JSpecify はこれを統一し、ツール間で共通して使える仕様を提供することを目的としています。
2006 年頃に提案された Java のソフトウェア欠陥検出用アノテーションの JCP 標準化仕様に JSR‑305(事実上廃止)がありましたが、JSpecify は型引数やジェネリクスに対する nullability の扱いなど、JSR‑305 では不十分だった領域をカバーしています。
org.jspecify.annotations パッケージ配下に @Nullable、@NonNull、@NullMarked などを定義しており、静的解析ツールや IDE がこれらを認識することで、より正確な null チェックが可能になります。
Spring Framework 7 は JSpecify に対応し、null を返すメソッドや null を渡してもよい引数には明示的に @Nullable をつけるようになりました。それ以外では null を返さず、null の設定も許容しません。
詳しくは こちらのブログ記事 を参照してください。
JSpecify のアノテーションを使用することで、適切な null チェックが行われていないコードに対して、IDE は警告を表示できます。
しかし、これ単体ではコンパイルエラーを発生させることはありません。
NullAway とは?
NullAway は、Java の NullPointerException を静的解析で検出するツールです。Error Prone プラグインとして動作し、ビルド時に null 安全性の違反を警告・エラーとして報告します。
JSpecify や JSR‑305 のアノテーションを認識し、「@Nullable が付いていない参照は non-null とみなす」というオプトアウト方式を採用しています。これにより既存コードベースへの段階的な導入がしやすい設計になっています。
NullAway と JSpecify を組み合わせることによって、適切な null チェックが行われていないコードに対して、コンパイルエラーを発生させることができるようになります。
例えば、次のコードを NullAway が有効な状態でコンパイルします。
package com.example.foo;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@Component
public class Foo {
private final RestClient restClient;
public Foo(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.build();
}
public String getFoo() {
return this.restClient.get().uri("http://example.com/foo").retrieve().body(String.class);
}
}
すると、次のコンパイルエラーが発生します。
[ERROR] /Users/toshiaki/.../Foo.java:[16,2] error: [NullAway] returning @Nullable expression from method with @NonNull return type
RestClient の body メソッドには次のように @Nullable アノテーションがついており、null を返す可能性があるのに、何もアノテーションやチェックを行わずに return しているからです。
/**
* Extract the body as an object of the given type.
* @param bodyType the type of return value
* @param <T> the body type
* @return the body, or {@code null} if no response body was available
* @throws RestClientResponseException by default when receiving a
* response with a status code of 4xx or 5xx. Use
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
* handling.
*/
<T> @Nullable T body(Class<T> bodyType);
次のように null の場合に対処するコードを書く、あるいはメソッドに @Nullable をつけて、null を返す可能性を伝播させることでコンパイルエラーを取り除くことができます。
package com.example.foo;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import java.util.Objects;
@Component
public class Foo {
private final RestClient restClient;
public Foo(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.build();
}
public String getFoo() {
return Objects.requireNonNull(this.restClient.get().uri("http://example.com").retrieve().body(String.class),
"Response body must not be null");
}
}
なお、何もアノテーションを指定しない場合に non-null として扱うようにするには、クラスまたはパッケージに @NullMarked アノテーションをつける必要があります。
上記の例では次のような package-info.java を用意してあります。
@NullMarked
package com.example.foo;
import org.jspecify.annotations.NullMarked;
JSpecify/NullAway を Maven で使う
ここまでで JSpecify/NullAway を使うことで、強制的に null チェックができるようになることがわかりました。さて、これを Maven に組み込むにはどうすればよいでしょうか。
次の maven-compiler-plugin に次のような設定が必要です(要: JDK 22+)。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.47.0</version>
</path>
<path>
<groupId>com.uber.nullaway</groupId>
<artifactId>nullaway</artifactId>
<version>0.13.1</version>
</path>
</annotationProcessorPaths>
<fork>true</fork>
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<arg>--should-stop=ifError=FLOW</arg>
<!-- @formatter:off -->
<arg>-Xplugin:ErrorProne -XepDisableAllChecks -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:JSpecifyMode=true -Xep:NullAway:ERROR -XepExcludedPaths:(.*/test/java/.*|.*/target/generated-sources/.*)</arg>
<!-- @formatter:on -->
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
</compilerArgs>
</configuration>
</plugin>
コピー&ペーストすればよいのですが、Maven プロジェクトの量が多いと管理が大変になります…
Nullability Maven Plugin の導入
Nullability Maven Plugin はこの pom.xml のボイラープレートを取り除いてくれる Maven プラグインです。
https://github.com/making/nullability-maven-plugin
前述の設定は Nullability Maven Plugin を使うと、次のように簡略化されます。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
</plugin>
<plugin>
<groupId>am.ik.maven</groupId>
<artifactId>nullability-maven-plugin</artifactId>
<version>0.3.0</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>configure</goal>
</goals>
</execution>
</executions>
</plugin>
maven-compiler-plugin に自身で設定した内容があれば自動でマージされます。
Tip
mvn help:effective-pom でマージされた内容を確認できます。
このプラグイン自体の制約ではありませんが、NullAway の JSpecify Mode を使用するには JDK 22 以上でコンパイルする必要がありますが、ターゲットは 17 でも構いません。どうしても古いバージョンの JDK(17 または 21)でコンパイルしたい場合は、こちら を参照してください。
package-info.java の自動生成
JSpecify を使う上で面倒なのが、package-info.java の作成です。@NullMarked のついた package-info.java は各サブパッケージに作成する必要があり、つい作成を忘れがちです。
Nullability Maven Plugin はデフォルトで、@NullMarked がついていないクラス/パッケージがあればエラーにします。
また、次の設定で package-info.java をビルド時に自動生成させることもできます。
<plugin>
<groupId>am.ik.maven</groupId>
<artifactId>nullability-maven-plugin</artifactId>
<version>0.3.0</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>configure</goal>
<goal>generate-package-info</goal><!-- 追加 -->
</goals>
</execution>
</executions>
</plugin>
生成させるディレクトリの設定は こちら を参照してください。
Nullability Maven Plugin で Maven ユーザーが簡単に JSpecify / NullAway を導入できる方法を紹介しました。ぜひ試してください。