defineClassする

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

```java: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#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`を使う方法があります。

```java: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`指定されている](http://hg.openjdk.java.net/jdk9/jdk9/jdk/file/tip/src/jdk.unsupported/share/classes/module-info.java)ため、リフレクションによる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](http://openjdk.java.net/jeps/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に追加されました](http://hg.openjdk.java.net/jigsaw/jake/jdk/rev/51eac7fb1150#l4.66)。
[Hibernate ORM](http://hibernate.org/orm/)や[Mockito](http://site.mockito.org/)等で利用されているバイトコード操作ライブラリの[Byte Buddy](http://bytebuddy.net)では、このメソッドを利用する`ClassLoadingStrategy`が[用意されています](https://github.com/raphw/byte-buddy/commit/f2294ec134b6a02e3bf287d5726ae10515c57ec5)。

以上です。