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 {
}
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;
}
}
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());
第一引数にモジュール名、第二引数にパッケージを指定することにより、任意のモジュール、パッケージをオープンすることができます。
動的プロキシ生成後ならモジュール名もパッケージもわかるので、このようにしてしまえば良いというわけです。
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のカプセル化」という思想から明確に逆行しています。
どう考えても推奨されるものではなく、現在は何か違う方法を模索中です。
(そもそもプロキシインスタンスにリフレクションアクセスというのが微妙な気がするので、ここをリファクタリングするのがいいのかなあと考えています)
思いついたはいいですが日の目を見ることもなさそうなので、供養も兼ねてここに残しておきます。