Java 8 や 11 で動いていたアプリを Java 17 で起動すると、次の例外で落ちることがあります。
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field
transient java.lang.Object[] java.util.ArrayList.elementData accessible:
module java.base does not "opens java.util" to unnamed module @32993f6f
コピペ検索用の表記ゆれです。パッケージ名の部分が違うだけで、原因はどれも同じです。
module java.base does not "opens java.util" to unnamed modulemodule java.base does not "opens java.lang" to unnamed moduleUnable to make field ... accessibleUnable to make protected ... accessible
末尾の @32993f6f は実行のたびに変わります。検索するときは外してください。
Java 16 から、JDK の内部へのリフレクション(setAccessible(true))が既定で拒否されるようになりました。同じコードが、8 では黙って通り、11 では警告つきで通り、16 以降で例外になります。
直し方の要点を先に
実行する JVM に --add-opens を渡します。 コードは変えません。
java --add-opens java.base/java.util=ALL-UNNAMED -jar app.jar
java.util の部分は、エラーメッセージが does not "opens ..." と名指ししているパッケージに置き換えます。
そのうえで、次の 2 つを間違えると直りません。どちらも検索で見つかる書き方です。
| 書き方 | 結果 |
|---|---|
--illegal-access=permit | Java 17 では無視される(例外は同じまま) |
--add-exports java.base/java.util=ALL-UNNAMED | 直らない(exports と opens は別物) |
なぜ 17 で初めて落ちるのか
Java 9 のモジュールシステム導入以降、JDK 内部へのリフレクションは段階的に締められてきました。同じコードの結果が版で変わります。
| JDK | 結果 |
|---|---|
| 8 | 成功する。警告も出ない |
| 9〜15 | 成功するが、WARNING: An illegal reflective access operation has occurred が出る(実測は 11。9〜15 は既定が permit だったため・JEP 396) |
| 16 | InaccessibleObjectException(JEP 396 で既定が拒否に変わった) |
| 17 / 21 | InaccessibleObjectException |
境界は 16 です。 9〜15 では警告だけで動いてしまいます。「Java 11 に上げたとき警告は出たが動いていたから、たいした問題ではない」と判断したまま 17 へ進むと、そこで初めて落ちます。9〜15 で出ていた WARNING: An illegal reflective access operation has occurred が、この例外の予告でした。
--illegal-access=permit は Java 17 では効きません
検索すると、この指定を勧める回答が上位に残っています。
java --illegal-access=permit -jar app.jar
Java 17 では無視されます。 実行すると、次の 1 行が出たうえで、元とまったく同じ例外が出ます。
OpenJDK 64-Bit Server VM warning: Ignoring option --illegal-access=permit; support was removed in 17.0
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: ...
例外の文言が変わらないので、Ignoring option の 1 行を見落とすと、**「フラグを足したのに直らない」**という状態で止まります。
つまり、この移行には境界が 2 段あります。
- Java 16:既定が拒否に変わった。ただし
--illegal-access=permitで従来どおりに戻せた。 - Java 17:JEP 403 で
--illegal-accessが廃止された。オプションは今も受け付けられる(起動は成功する)が、指定しても警告だけ出して黙殺される。戻す手段がもう無い。
Java 16 でだけ通用した回避策が、そのまま 17 に持ち込まれて動かなくなっている、というのが多くの記事の状態です。
綴りを間違えた存在しないオプション(--bogus など)なら、Unrecognized option で起動に失敗します。--illegal-access は実在するオプションとして受理され、警告を 1 行出して無視されるだけで、JVM は起動します。起動ログのその 1 行を読み飛ばすと、フラグが効いていない状態のまま先へ進むことになります。
--add-exports では直りません
もう 1 つの取り違えがこれです。名前が似ているので混ざります。
--add-exports… その package の public な型に触れてよい、という許可。--add-opens… 深いリフレクション(setAccessible)を許す、という許可。
--add-exports java.base/java.util=ALL-UNNAMED を渡しても、InaccessibleObjectException は文言まで含めてそのまま出ます。必要なのは opens のほうです。
手がかりはエラーメッセージ自身にあります。module java.base does not "opens java.util" と、JVM が opens と言っています。ここを読めば、渡すべきフラグは --add-opens だと分かります。
ビルドツールから渡す
--add-opens は JVM の引数です。アプリケーションの引数ではないので、渡す場所を間違えると届きません。
Maven(テスト実行時):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>--add-opens java.base/java.util=ALL-UNNAMED</argLine>
</configuration>
</plugin>
Gradle(テスト実行時):
tasks.withType(Test).configureEach {
jvmArgs '--add-opens', 'java.base/java.util=ALL-UNNAMED'
}
実行可能 jar なら、マニフェストに書いておく方法もあります(起動コマンドを変えられないとき)。
Add-Opens: java.base/java.util
--add-opens を足したあとに残るもの
--add-opens を足しても、JDK 内部の実装に依存し続けることに変わりはありません。内部フィールドの名前や型は、将来の JDK で予告なく変わります。
そのリフレクションを書いているのが自分のコードなら、内部を覗くのをやめるのが本筋です。多くの場合、公開 API で同じことができます。
ライブラリが内部で行っているなら、まずそのライブラリの changelog を見て、この JDK 移行に対応した版があるかを確認してください(どの版で解消するかは、この記事では検証していません)。対応版があるなら、古い版に --add-opens を足して延命するより、ライブラリを上げるほうが、次の JDK 移行で同じ作業を繰り返さずに済みます。 ただし上げ先が存在しないライブラリもあります。その場合は --add-opens が残る手になります。
--add-opens が要るのは、ライブラリを上げられない事情があるときです。その場合は、なぜ必要なのかをコメントに残してください。フラグだけが残って理由が失われると、次の移行で誰も外せなくなります。
切り分け
- フラグを足したのに、例外が 1 文字も変わらない →
--illegal-access=permitを渡しています。Java 17 では無視されるので、--add-opensに置き換えてください。起動ログにIgnoring optionの行が出ているはずです。 --add-opensを足したのに直らない → 渡す先が違う可能性があります。--add-opensは JVM の引数なので、java -jar app.jar --add-opens ...のようにアプリケーション側に付けても届きません。javaの直後に置いてください。ビルドツール経由なら、上記のargLine/jvmArgsに入れます。- パッケージ名が
java.utilではない → メッセージが名指ししているパッケージに合わせます。java.langを覗くライブラリなら--add-opens java.base/java.lang=ALL-UNNAMEDです。複数必要なら、--add-opensを並べて指定します。 Unable to make ... accessibleではなくIllegalAccessException→ こちらはモジュールの開放ではなく、privateなどの通常のアクセス制御です。setAccessible(true)の呼び忘れを疑ってください。- 手元では動くのに CI でだけ落ちる → 手元と CI の JDK の版が違います。9〜15 なら警告だけで通り、16 以降で例外になります。両方で
java -versionを確認してください。 - エラーが
NoClassDefFoundError: javax/xml/bind/...→ 別件です。こちらは Java 11 で JDK から削除されたパッケージの話で、フラグではなく依存を足して直します。NoClassDefFoundError: javax/xml/bind/JAXBException の直し方 を参照してください。
検証環境
eclipse-temurin:17-jdk、ネットワーク無し- 再現:
ArrayListの内部フィールドelementDataにsetAccessible(true)するクラスを Java 17 で実行し、does not "opens java.util" to unnamed moduleが出て終了コード 1 - 修正:同一のソースを同じようにコンパイルし、
--add-opens java.base/java.util=ALL-UNNAMEDを付けて実行するだけで終了コード 0・シグネチャ消滅。reproduce/とfix/の.javaに差分はありません(変えたのは JVM 引数だけです) - 版の表(8 は無警告で成功/11 は警告つきで成功/16・17・21 は例外)は、
eclipse-temurinの 8・11・16・17・21 でそれぞれ実行して確認しました - 「Java 16 では
--illegal-access=permitが通る」「Java 17 ではIgnoring optionと出て無視される」「--add-exportsでは直らない」も、同じく個別に実行して確認しています
Maven Surefire の argLine / Gradle の jvmArgs / 実行可能 jar の Add-Opens マニフェストは、機構が同じ(JVM に --add-opens が届くか)ことを根拠にした対処であり、ビルドツール経由での再現は通していません。Jackson・Spring・Lombok の特定の版でこの問題が解消するかどうかも確認していません。