Error [does not "opens java.util" to unnamed module]

InaccessibleObjectException: module java.base does not "opens java.util" の直し方

FIX SUMMARY verified
Applies when
eclipse-temurin:17-jdksetAccessible(true) on a JDK-internal field; strong encapsulation became the default in Java 16 (JEP 396) and --illegal-access was removed in 17 (JEP 403)

Verified: reproduced in eclipse-temurin:17-jdk, then the does not "opens java.util" to unnamed module signature was gone after the fix (exit 0).

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 module
  • module java.base does not "opens java.lang" to unnamed module
  • Unable to make field ... accessible
  • Unable 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=permitJava 17 では無視される(例外は同じまま)
--add-exports java.base/java.util=ALL-UNNAMED直らないexportsopens は別物)

なぜ 17 で初めて落ちるのか

Java 9 のモジュールシステム導入以降、JDK 内部へのリフレクションは段階的に締められてきました。同じコードの結果が版で変わります。

JDK結果
8成功する。警告も出ない
9〜15成功するが、WARNING: An illegal reflective access operation has occurred が出る(実測は 11。9〜15 は既定が permit だったため・JEP 396
16InaccessibleObjectExceptionJEP 396 で既定が拒否に変わった)
17 / 21InaccessibleObjectException

境界は 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 17JEP 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-opensJVM の引数です。アプリケーションの引数ではないので、渡す場所を間違えると届きません。

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 の内部フィールド elementDatasetAccessible(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 の特定の版でこの問題が解消するかどうかも確認していません。

検証(machine-verified)

この修正は eclipse-temurin:17-jdk のバージョン固定コンテナ内で再現し、修正後に does not "opens java.util" to unnamed module のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs jvm/inaccessible-object-exception
● reproduce does not "opens java.util" to unnamed module present ✓
● apply fix exit 0
● re-run does not "opens java.util" to unnamed module gone ✓
PASS verified · eclipse-temurin:17-jdk · signature gone