InaccessibleObjectException の発生原因とその対処方

f:id:Naotsugu:20201229073519p:plain



リフレクションで発生する InaccessibleObjectException

Java9 で導入された JavaPlatform Module System によりリフレクションを利用するフレームワークで以下のような InaccessibleObjectException が発生する場合がある。

java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.util.ArrayList jdk.internal.loader.URLClassPath.loaders accessible: module java.base does not "opens jdk.internal.loader" to unnamed module @7ba4f24f
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:349)
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:289)
    at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:174)
    at java.base/java.lang.reflect.Field.setAccessible(Field.java:168)
    at com.sun.appserv.ClassLoaderUtil.getField(ClassLoaderUtil.java:308)
    at com.sun.appserv.ClassLoaderUtil.initForClosingJars(ClassLoaderUtil.java:290)
    at com.sun.appserv.ClassLoaderUtil.init(ClassLoaderUtil.java:263)
    at com.sun.appserv.ClassLoaderUtil.releaseLoader(ClassLoaderUtil.java:139)
    at com.sun.appserv.ClassLoaderUtil.releaseLoader(ClassLoaderUtil.java:111)
    at org.glassfish.web.loader.WebappClassLoader.stop(WebappClassLoader.java:1956)
    at org.glassfish.web.loader.WebappClassLoader.preDestroy(WebappClassLoader.java:1917)
    at org.glassfish.deployment.common.DeploymentContextImpl.getClassLoader(DeploymentContextImpl.java:289)
    at org.glassfish.deployment.common.DeploymentContextImpl.getClassLoader(DeploymentContextImpl.java:231)
    at com.sun.enterprise.v3.server.ApplicationLifecycle.prepare(ApplicationLifecycle.java:561)
    at org.glassfish.deployment.admin.DeployCommand.execute(DeployCommand.java:579)
    at com.sun.enterprise.v3.admin.CommandRunnerImpl$2$1.run(CommandRunnerImpl.java:556)
    at com.sun.enterprise.v3.admin.CommandRunnerImpl$2$1.run(CommandRunnerImpl.java:552)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
    at java.base/javax.security.auth.Subject.doAs(Subject.java:363)
    at com.sun.enterprise.v3.admin.CommandRunnerImpl$2.execute(CommandRunnerImpl.java:551)
    at com.sun.enterprise.v3.admin.CommandRunnerImpl$3.run(CommandRunnerImpl.java:582)
    at com.sun.enterprise.v3.admin.CommandRunnerImpl$3.run(CommandRunnerImpl.java:574)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
    at java.base/javax.security.auth.Subject.doAs(Subject.java:363)
    at com.sun.enterprise.v3.admin.CommandRunnerImpl.doCommand(CommandRunnerImpl.java:573)
    at com.sun.enterprise.v3.admin.CommandRunnerImpl.doCommand(CommandRunnerImpl.java:1497)
    at com.sun.enterprise.v3.admin.CommandRunnerImpl.access$1300(CommandRunnerImpl.java:120)
    at com.sun.enterprise.v3.admin.CommandRunnerImpl$ExecutionContext.execute(CommandRunnerImpl.java:1879)
    at com.sun.enterprise.v3.admin.CommandRunnerImpl$ExecutionContext.execute(CommandRunnerImpl.java:1755)
    at com.sun.enterprise.admin.cli.embeddable.DeployerImpl.deploy(DeployerImpl.java:137)
    // ...

上記例外は、JavaEE(Payara) へ war をデプロイする処理で発生したもの。


war のデプロイの前に、WebappClassLoader の開放処理があり、その中で、JDK の内部クラスである URLClassPath のフィールドをリフレクションにより取得している。

以下のような実装になっている。

private static void initForClosingJars() throws NoSuchFieldException {
    // ...        
    loadersField = getField(ucpCLass, URLCLASSPATH_LOADERS_FIELD_NAME);

}

ucpCLassjdk.internal.loader.URLClassPath であり、URLCLASSPATH_LOADERS_FIELD_NAME"loaders" というフィールドを指す。

つまり、以下のフィールドをリフレクションにより取得している。

package jdk.internal.loader;

public class URLClassPath {
    // ...
    private final ArrayList<URLClassPath.Loader> loaders;

}

その後、取得した Loader が保持している JarFile のクローズを行う前処理である。


さて、jdk.internal.loader は、java.base モジュールに含まれており、module-info.java では以下のようなエクスポート定義となっている。

module java.base {
    // ...
    exports jdk.internal.loader to
        java.instrument,
        java.logging;
}

つまり、jdk.internal.loader パッケージは、java.instrument モジュールと java.logging モジュールにしか公開されていない。

そのため、jdk.internal.loader.URLClassPath を外部からリフレクションでアクセスすることが出来ずに InaccessibleObjectException が発生する。


Automatic Module(自動モジュール) や Unnamed Module(無名モジュール) によるモジュールロードが行われることで、対象のモジュールは全て exports 扱い(java.base モジュールを exports しなくとも使える)になるものの、そのモジュール内で、jdk.internal.loader パッケージが限定公開となっているため如何ともし難い。

JavaPlatform Module System と Automatic Module(自動モジュール) / Unnamed Module(無名モジュール)については以下を参照。

blog1.mammb.com


リフレクションを許可する

--add-opens オプションを利用してアプリケーションを実行することでリフレクションを許可することができる。

--add-opens オプションの構文は次のとおり。

--add-opens <module>/<package>=<target-module>(,target-module)*

このオプションにより起動することで <module> にある <package><target-module> へ公開することを許可することができる。

<target-module>ALL-UNNAMED を使用すれば、すべての Unnamed Module(無名モジュール) に対して公開されることになる。


つまり今回のケースでは以下のようなコマンドライン引数を指定すれば良い(今回はモジュールシステムを使っていないので)。

java --add-opens java.base/jdk.internal.loader=ALL-UNNAMED

例えば Gradle のアプリケーションプラグインで実行する場合には以下のようになる。

application {
    mainClass = 'xxx'
    applicationDefaultJvmArgs = ['--add-opens', 'java.base/jdk.internal.loader=ALL-UNNAMED']
}

Gradle Kotlin DSL の場合は以下。

application {
    mainClass.set("xxx")
    applicationDefaultJvmArgs = listOf("--add-opens", "java.base/jdk.internal.loader=ALL-UNNAMED")
}


これにて InaccessibleObjectException は発生しなくなる。


なお、アプリケーション側のモジュール定義の問題であれば、自身のコードの module-info.javaopens などでリフレクションに対して可視設定することは言うまでもない。

以上。