LoginSignup
3
3

More than 1 year has passed since last update.

JEP 403を無理やり突破してみた

Posted at

JEP 403とは

JEP 403: Strongly Encapsulate JDK Internalsは、JDK9から続く内部APIカプセル化をより強固なものにすべく、JDK17から追加された仕様です。
このあたりの経緯についてはこことかが分かりやすいと思います。

ちょっと強引な例ですが、スレッドからリフレクション経由でコンテキストクラスローダーを取得するシチュエーションを考えてみます。

import java.lang.reflect.Field;

public class Main {

    public static void main(String[] args) throws NoSuchFieldException {
        Field field = Thread.class.getDeclaredField("contextClassLoader");
        field.setAccessible(true);
        System.out.println(field.getName());
    }
}

このコードをJDK17で動かすと、以下のような例外をスローします。

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.ClassLoader java.lang.Thread.contextClassLoader accessible: module java.base does not "opens java.lang" to unnamed module @27973e9b
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178)
	at java.base/java.lang.reflect.Field.setAccessible(Field.java:172)
	at com.example.jdk17_test.Main.main(Main.java:9)

私が用意したMainクラスはunnamed moduleなので、JEP403の仕様に則って内部APIへのリフレクションがはじかれていることがわかります。

ですがこれについては先のリンクを見ればわかる通り、VMオプションに--add-opens=java.base/java.lang=ALL-UNNAMEDを追加することで回避することができます。

contextClassLoader

プロセスは終了コード 0 で終了しました

このように大抵の場合は必要に応じて--add-opensしていけば何とかなる、、、はずなのですが、今回は何とかならなかったというお話しです。

背景

JDK8で動いている10年もののWebアプリを試しにJDK17で動かしてみようと思い立ち、JDKをアップデートしてAPサーバを起動。
案外すんなりいくかもなーなどと呑気に構えていた私の目に、以下のようなスタックトレースが飛び込んできたのでした。

java.lang.reflect.InaccessibleObjectException: Unable to make field private static final java.lang.reflect.Method jdk.proxy1.$Proxy36.m0 accessible: module jdk.proxy1 does not "opens jdk.proxy1" to unnamed module 
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178)
	at java.base/java.lang.reflect.Field.setAccessible(Field.java:172)

(後略)

module jdk.proxy1 does not "opens jdk.proxy1" to unnamed moduleということで、どうやらjdk.proxy1というモジュールがunnamed moduleに対してオープンになっていない様子。
ならばということでVMオプションに--add-opens=jdk.proxy1/jdk.proxy1=ALL-UNNAMEDを追加して再起動しましたが、相変わらず同じ例外がスローされています。
それだけでなく、サーバの起動ログに以下のような警告が表示されてしまったのでした。

WARNING: Unknown module: jdk.proxy1 specified to --add-opens

jdk.proxy1などというモジュールは存在しないとのこと。

どういうことだってばよ。。。

何が起こっていたのか

jdk.proxy1というモジュールは、Javaの動的プロキシという仕組みで生成されたものです。
動的プロキシについてはこちらの記事が大変参考になりました。
(AOPとかで使ってる仕組だと思いますが、間違ってたらすみません。。。)

動的プロキシは内部API扱いのようで、先ほどお見せした例外はそれに対するリフレクションがはじかれたものでした。
ここまでは冒頭で述べた内部APIカプセル化の仕様通りですが、問題はjdk.proxy1が「動的に生成されたもの」だということです。

動的プロキシは処理を実行するごとに生成されるため、それより前のタイミングでは存在しません。
そのためVMオプションに--add-opens=jdk.proxy1/jdk.proxy1=ALL-UNNAMEDを追加しても、そんなモジュールはないと怒られてしまったわけです。

ならどうする

内部APIゆえにデフォルトではリフレクションできない。
モジュールが動的に生成されるゆえにVMオプションでオープンにすることもできない。

しばらくの間「もう無理やろこれ。。。」と頭を悩ませましたが、ある日ついに閃きました。
動的に生成されるなら、動的にオープンすればいいじゃない。

解決法

jdk.internal.module.Modulesというクラスがあります。
Modulesという名前の通り、モジュールに対して様々な操作を行うことができます。
その中のaddOpensToAllUnnamedといういかにもな名前のメソッドを使うことで、Javaコード上で--add-opensと同じことができるのです。

まずは以下のようなクラスを用意します。

テスト用ののインタフェース
public interface TestInterface {
}
テスト用のInvocationHandler
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class TestInvocationHandler implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        return null;
    }
}
Mainクラス
public class Main {

    public static void main(String[] args) {
        TestInvocationHandler testInvocationHandler = new TestInvocationHandler();

        TestInterface proxy = (TestInterface) Proxy.newProxyInstance(TestInterface.class.getClassLoader(),
                new Class[] { TestInterface.class },
                testInvocationHandler);

        Field[] fields = proxy.getClass().getDeclaredFields();

        for (Field field : fields) {
            field.setAccessible(true);
            System.out.println(field.getName());
        }
    }
}

今回は動的プロキシのインスタンスにリフレクションでアクセスしたいだけなので、interfaceとInvocationHandlerは空っぽです。
mainメソッドではこれらを使って動的プロキシのインスタンスを取得し、リフレクションによるフィールドへのアクセスを試みています。

このコードはこのままの状態で実行すると、先述した例と同じように例外をスローします。

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private static final java.lang.reflect.Method jdk.proxy1.$Proxy0.m0 accessible: module jdk.proxy1 does not "opens jdk.proxy1" to unnamed module @7c3df479
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178)
	at java.base/java.lang.reflect.Field.setAccessible(Field.java:172)
	at com.example.jdk17_test.Main.main(Main.java:20)

ここで、mainメソッドに以下の一行を書き加えてみましょう。
Modules.addOpensToAllUnnamed(proxy.getClass().getModule(), proxy.getClass().getPackageName());
第一引数にモジュール名、第二引数にパッケージを指定することにより、任意のモジュール、パッケージをオープンすることができます。
動的プロキシ生成後ならモジュール名もパッケージもわかるので、このようにしてしまえば良いというわけです。

Mainクラス
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;

import jdk.internal.module.Modules;

public class Main {

    public static void main(String[] args) {
        TestInvocationHandler testInvocationHandler = new TestInvocationHandler();

        TestInterface proxy = (TestInterface) Proxy.newProxyInstance(TestInterface.class.getClassLoader(),
                new Class[] { TestInterface.class },
                testInvocationHandler);

        Modules.addOpensToAllUnnamed(proxy.getClass().getModule(), proxy.getClass().getPackageName());

        Field[] fields = proxy.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            System.out.println(field.getName());
        }
    }
}

早速実行。。。と行きたいところですが、もう一つ忘れてはならないのが、jdk.internal.module.Modules自体が内部APIであるということです。
今回はリフレクションではなく通常の呼び出しなので、--add-exports=java.base/jdk.internal.module=ALL-UNNAMEDをVMオプションに追加してあげましょう。

これで実行すると、、、

m0
m1
m2

プロセスは終了コード 0 で終了しました

エラーなしで通りましたね。
プロキシインスタンスに含まれるなんだかよくわからないフィールドの名前が出力されました。

まとめ

どう見ても黒魔術です。本当にありがとうございました。

なんでもかんでも問答無用でオープンするこの手法は「内部APIのカプセル化」という思想から明確に逆行しています。
どう考えても推奨されるものではなく、現在は何か違う方法を模索中です。
(そもそもプロキシインスタンスにリフレクションアクセスというのが微妙な気がするので、ここをリファクタリングするのがいいのかなあと考えています)

思いついたはいいですが日の目を見ることもなさそうなので、供養も兼ねてここに残しておきます。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3