Javaにおいてバイトコードをクラスローダに強制的に読み込ませるには、以下のようにリフレクションを利用してClassLoader#defineClass
を呼びだします。
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.security.ProtectionDomain;
public class Main {
public static void main(String[] args) throws Exception {
ClassLoader loader = new ClassLoader() {
};
String className = C.class.getName();
byte[] bytes = readBytes(className);
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass",
String.class,
byte[].class,
int.class,
int.class,
ProtectionDomain.class);
defineClass.setAccessible(true);
Class<?> c = (Class<?>) defineClass.invoke(loader, className, bytes, 0, bytes.length, null);
System.out.println(c.getName());
System.out.println(c.getClassLoader());
}
private static byte[] readBytes(String className) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (InputStream in = Main.class.getResourceAsStream('/' + className.replace('.', '/') + ".class")) {
byte[] buffer = new byte[16384];
for (int l; (l = in.read(buffer)) != -1; ) {
out.write(buffer, 0, l);
}
}
return out.toByteArray();
}
private static class C {
}
}
これを実行すると、以下のようになります。
$ java Main
Main$C
Main$1@4aa298b7
しかし、上記のコードはJava 9では動作しません。
$ java -version
java version "9-ea"
Java(TM) SE Runtime Environment (build 9-ea+157)
Java HotSpot(TM) 64-Bit Server VM (build 9-ea+157, mixed mode)
$
$ java Main
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @31b7dea0
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:335)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:278)
at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:196)
at java.base/java.lang.reflect.Method.setAccessible(Method.java:190)
at Main.main(Main.java:20)
Method#setAccessible
でInaccessibleObjectException
が発生しています。
Jigsawの導入により、Java APIのnon-publicメンバへのアクセスが許可されなくなったためです。
上記のコードをJava 9で動作させるには、add-opens
オプションを使います。
$ java --add-opens=java.base/java.lang=ALL-UNNAMED Main
Main$C
Main$1@282ba1e
Java 9に対応させる
add-opens
オプションなしで動作させる方法として、sun.misc.Unsafe
を使う方法があります。
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.ProtectionDomain;
import sun.misc.Unsafe;
public class Main {
public static void main(String[] args) throws Exception {
ClassLoader loader = new ClassLoader() {
};
String className = C.class.getName();
byte[] bytes = readBytes(className);
Class<?> c;
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass",
String.class,
byte[].class,
int.class,
int.class,
ProtectionDomain.class);
try {
defineClass.setAccessible(true);
c = (Class<?>) defineClass.invoke(loader, className, bytes, 0, bytes.length, null);
System.out.println("Called ClassLoader#defineClass via reflection");
} catch (RuntimeException e) {
if (!e.getClass().getName().equals("java.lang.reflect.InaccessibleObjectException")) {
throw e;
}
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
c = unsafe.defineClass(className, bytes, 0, bytes.length, loader, null);
} catch (Throwable t) {
throw e;
}
System.out.println("Called Unsafe#defineClass");
}
System.out.println(c.getName());
System.out.println(c.getClassLoader());
}
private static byte[] readBytes(String className) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (InputStream in = Main.class.getResourceAsStream('/' + className.replace('.', '/') + ".class")) {
byte[] buffer = new byte[16384];
for (int l; (l = in.read(buffer)) != -1; ) {
out.write(buffer, 0, l);
}
}
return out.toByteArray();
}
private static class C {
}
}
main
メソッドを修正しました。
最初にリフレクションでのアクセスを試してInaccessibleObjectException
が発生した場合には、Unsafe
のインスタンスを取得後、Unsafe#defineClass
を実行しています。
Unsafe
インスタンスの取得にリフレクションを利用していますが、sun.misc
パッケージのクラスはmodule-infoでopens
指定されているため、リフレクションによるprivateメンバへのアクセスが可能です。
最初にリクレクションでのアクセスを試したり、Unsafe
使用箇所をtry-catchで囲んだりしているのは、Unsafe
が存在しないJava実装に(ちょっとだけ)配慮してのことですが、ここではそれらの実装については検証しません。
リフレクションとUnsafe
のどちらが呼ばれたかわかるように、それぞれの処理の最後にprintln
を入れています。
これをJava 8のjavacでコンパイルします。
$ javac -version
javac 1.8.0_121
$
$ javac Main.java
Main.java:8: warning: Unsafe is internal proprietary API and may be removed in a future release
import sun.misc.Unsafe;
^
Main.java:33: warning: Unsafe is internal proprietary API and may be removed in a future release
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
^
Main.java:35: warning: Unsafe is internal proprietary API and may be removed in a future release
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
^
Main.java:35: warning: Unsafe is internal proprietary API and may be removed in a future release
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
^
4 warnings
Unsafe
が将来的に削除されるかもしれない旨の警告が出ますが、JEP 260によると
The critical internal APIs proposed to remain accessible in JDK 9 are:
- sun.misc.{Signal,SignalHandler}
- sun.misc.Unsafe (The functionality of many of the methods in this class is now available via variable handles (JEP 193).)
- sun.reflect.Reflection::getCallerClass(int) (The functionality of this method may be provided in a standard form via JEP 259.)
- sun.reflect.ReflectionFactory.newConstructorForSerialization
とあり、(今のところ)Java 9でUnsafe
を使うことはできそうです。
まずは、Java 8で実行します。
$ java -version
openjdk version "1.8.0_121"
OpenJDK Runtime Environment (build 1.8.0_121-8u121-b13-0ubuntu1.16.10.2-b13)
OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)
$
$ java Main
Called ClassLoader#defineClass via reflection
Main$C
Main$1@4aa298b7
リフレクションでClassLoader#defineClass
が呼びだされています。
次に、Java 9で実行します。
$ java -version
java version "9-ea"
Java(TM) SE Runtime Environment (build 9-ea+157)
Java HotSpot(TM) 64-Bit Server VM (build 9-ea+157, mixed mode)
$
$ java Main
Called Unsafe#defineClass
Main$C
Main$1@1bc6a36e
Unsafe
が使われています。
ちなみに、つい最近(Sat, 04 Mar 2017 16:02:31 +0000)、MethodHandles.Lookup#defineClass
がJigsawのjake forestに追加されました。
Hibernate ORMやMockito等で利用されているバイトコード操作ライブラリのByte Buddyでは、このメソッドを利用するClassLoadingStrategy
が用意されています。
以上です。