Java11にしてIntellijからGradleユニットテストを実行するとcompileTestGroovyでClassNotFoundExceptionが発生する

Java8からJava11にアップデートをしたところ、Intellijからテスト実行ができなくなった。

Git Bashから ./gradlew test したり、コマンドプロンプトから gradlew.bat test すると問題なく実行できるので、Intellij起因の問題だろうと思って調べたが、かなりしょっぱい原因だったのでメモ。

環境

Windows 10 Pro 64bit 21H1, Intellij IDEA Ultimate 2020.3.4, Java Openjdk 11.0.12.7, Gradle 6.6.1

問題

JDKのバージョンを8から11に変更し、Intellij IDEAからユニットテストを実行すると、以下の例外で compileTestGroovy タスクが失敗する。

[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] FAILURE: Build failed with an exception.
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] 
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] * What went wrong:
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] Execution failed for task ':compileTestGroovy'.
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] > Failed to run Gradle Worker Daemon
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter]    > Process 'Gradle Worker Daemon 1' finished with non-zero exit value 1

--debug オプションを付けて実行してみると、実際のエラーが発生している個所は以下の模様。

# 日本語
[ERROR] [system.err] エラー: メイン・クラスworker.org.gradle.process.internal.worker.GradleWorkerMainを検出およびロードできませんでした
[ERROR] [system.err] 原因: java.lang.ClassNotFoundException: worker.org.gradle.process.internal.worker.GradleWorkerMain

# 英語
[ERROR] [system.err] Error: Could not find or load main class worker.org.gradle.process.internal.worker.GradleWorkerMain
[ERROR] [system.err] Caused by: java.lang.ClassNotFoundException: worker.org.gradle.process.internal.worker.GradleWorkerMain

このエラーが発生するのはIntellijからの実行の場合のみで、直接 ./gradlew test などのコマンドを実行した場合は発生しない。

調査

gradle Intellij ClassNotFoundException GradleWorkerMain あたりで検索すると色々出てくる。

intellij idea - Gradle build failing after update - Stack Overflowを見て、./gradlew --stop.gradle の削除、また --refresh-dependencies オプション付きでの実行など行ったが効果なし。

続いて、Intellijのissueトラッカーを見てみると、issue214762およびissue193219にて、IntellijbootRun 時に引数を追加していたという情報を得た。

状況としては似ているので、Intellijから実行した場合と、直接コマンドで実行した場合でどのような引数が渡されているかを見比べてみた。

コマンド引数の違い

--debug オプションを付けて実行すると、Gradle Worker 起動時のコマンドが Starting process 'Gradle Worker Daemon 1'. Working directory: %GRADLE_USER_HOME%\workers Command: ... として出力される。

Java8にてIntellijから実行した際のコマンドは以下。長いので折り返しているが、実際は1行で出力される。

%PATH_TO_JAVA8_BIN%\java.exe ^
  -Djava.security.manager=worker.org.gradle.process.internal.worker.child.BootstrapSecurityManager ^
  -Xmx512m ^
  -Dfile.encoding=UTF-8 ^
  -Duser.country=JP ^
  -Duser.language=ja ^
  -Duser.variant ^
  -cp %GRADLE_USER_HOME%\caches\6.6.1\workerMain\gradle-worker.jar ^
  worker.org.gradle.process.internal.worker.GradleWorkerMain 'Gradle Worker Daemon 1'

続いてJava11にてIntellijから実行した際のコマンド。

%PATH_TO_JAVA11_BIN%\java.exe ^
  --add-opens java.base/java.lang=ALL-UNNAMED ^
  --add-opens java.base/java.lang.invoke=ALL-UNNAMED ^
  --add-opens java.prefs/java.util.prefs=ALL-UNNAMED ^
  @C:\tools\idea_temp\gradle-worker-classpath%20桁の数値%txt ^
  -Xmx512m ^
  -Dfile.encoding=UTF-8 ^
  -Duser.country=JP ^
  -Duser.language=ja ^
  -Duser.variant ^
  worker.org.gradle.process.internal.worker.GradleWorkerMain 'Gradle Worker Daemon 1'

最後に、コマンドを直接実行した場合。

%PATH_TO_JAVA11_BIN%\java.exe ^
  --add-opens java.base/java.lang=ALL-UNNAMED ^
  --add-opens java.base/java.lang.invoke=ALL-UNNAMED ^
  --add-opens java.prefs/java.util.prefs=ALL-UNNAMED ^
  @%USERPROFILE%\AppData\Local\Temp\gradle-worker-classpath%20桁の数値%txt ^
  -Xmx512m ^
  -Dfile.encoding=windows-31j ^
  -Duser.country=JP ^
  -Duser.language=ja ^
  -Duser.variant ^
  worker.org.gradle.process.internal.worker.GradleWorkerMain 'Gradle Worker Daemon 1'

Java11にした場合、 java.security.manager が消え、モジュール関連のオプションが追加され、クラスパス指定が -cp から @argument files に変更されている。

@argument files 記法は、Java9から追加された。

従来のWindowsには、コマンドプロンプトの引数の文字列長に制限があるため、クラスパスが極端に長くなる場合、クラスパスだけを指定したマニフェストJARを作成し、それを読み込んでやる必要があった。Java9以降であれば、Javaコマンドのオプションをファイルに記述し、 @ファイルパス を指定することで読み込ませることができるようになっている。

実際のファイルの内容は、Intellijから実行した場合も、コマンドで実行した場合も変わらず、以下のような形式。先頭に gradle-worker.jar 、その後ろに依存JARのパスが含まれる。

-cp
%GRADLE_USER_HOME%\\caches\\6.6.1\\workerMain\\gradle-worker.jar;%GRADLE_USER_HOME%\\wrapper\\dists\\gradle-6.6.1-bin\\%25桁の英数字%\\gradle-6.6.1\\lib\\gradle-core-6.6.1.jar;...

Java11での実行の場合、Intellijからの実行では -Dfile.encoding=UTF-8 に対し、コマンドからの実行では -Dfile.encoding=windows-31j が指定されている。

@argument files で指定されたファイルの文字コードを見てみると、Intellijからの実行ではUTF-8、コマンドからの実行ではShift_JIS(CP932)になっていた。現時点では @argument files文字コードを指定する方法はなさそうなので、OSのデフォルトエンコーディングでファイルの内容を記述する必要があると思われる。だが、Intellijから実行した場合、Javaファイルなどのエンコーディングを指定する -Dfile.encoding と同じく、UTF-8で出力されてしまっていた。

さらに、実装環境は、Active Directoryにてユーザー名に漢字が使われているため、 GRADLE_USER_HOME のデフォルトである %USERPROFILE%\.gradle に漢字が含まれていた。漢字を含んだパスがUTF-8で書き出され、Shift_JISとして読み込まれたため、適切に解釈できていなかった模様。

試しに、Intellijから実行した場合のファイルをShift_JISに変換し、コマンドを手動で実行したところ、Gradle Workerが立ち上がった。

対応

GRADLE_USER_HOME に非ASCII文字が含まれなければUTF-8でもShift_JISでも読み込むことができるため、 GRADLE_USER_HOME を適当な半角英字のみのパスに設定したところ、解消した。

振り返り

原因が分かれば大したことはないが、IDE起因かGradle起因かJava起因か、また原因の特定に4時間くらいかかった。似たような問題が起こっている人もちらほらいるようだが、同じ原因の人もいるのでは。パスにマルチバイト文字は入れないのが無難だろう。

以前は bootRun するときにマニフェストJARを作るプラグインを入れたりしていたが、 @argument files のおかげでタスクの変更でできるようになった模様。

Java9以上でファイル生成をよしなにしてくれるプラグインもあった。後々使いたくなるかもしれないので、URLを残しておく。

github.com