LoginSignup
7

More than 5 years have passed since last update.

defineClassする

Last updated at Posted at 2017-03-12

Javaにおいてバイトコードをクラスローダに強制的に読み込ませるには、以下のようにリフレクションを利用してClassLoader#defineClassを呼びだします。

Main.java
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#setAccessibleInaccessibleObjectExceptionが発生しています。
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を使う方法があります。

Main.java
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#defineClassJigsawのjake forestに追加されました
Hibernate ORMMockito等で利用されているバイトコード操作ライブラリのByte Buddyでは、このメソッドを利用するClassLoadingStrategy用意されています

以上です。

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
7