Java11でMyBatisが動かない→Unsafeの仕様が変わったため

お久しぶりです。生きてます。

最近忙しすぎてサボってました。書くネタはそれなりにあるのですが…

さて、Java11がリリースされましたね。僕も早速検証中ですが、 すぐに壁にぶち当たりました。MyBatisのlazy loadingが動きません。


結論から言うと、org.apache.ibatis.javassist.util.proxy.DefineClassHelperがsun.mics.Unsafeを使っているところを、jdk.internal.misc.Unsafeを使うように切り替える必要があります。 ただこれ、当然ながらソースコードにハードコーディングされているわけでして、なんとかしようと思うと

  1. 毎度おなじみJavassistで無理やり書き換える
  2. ソースをコピーして書き換える

のどちらかになるかと。ただ、(1)はあまりお勧めできません。sun.misc.Unsafeとjdk.internal.misc.Unsafeは共通のインタフェースがあるわけではなくて完全に別クラスになり、 するとフィールドをJavassistで強引に入れ替えるだけでは済まず、メソッド定義も全面的に書き換える必要があります。要するにコードの書き直しに等しい。


よって、とりあえず今回はめんどくさいので(2)の作戦を採用します。 適当なソースフォルダにDefineClassHelperのソースをコピーして、以下の箇所を書き換えます。

package org.apache.ibatis.javassist.util.proxy;

...

import jdk.internal.misc.Unsafe; // sun.misc.Unsafe→jdk.internal.misc.Unsafeへ置換

...

public class DefineClassHelper {

    // ...略

    static {
        if (ClassFile.MAJOR_VERSION < 53) {
            try {
                final Class<?> cl = Class.forName("java.lang.ClassLoader");
                defineClass1 = SecurityActions.getDeclaredMethod(cl, "defineClass",
                    new Class[] { String.class, byte[].class, Integer.TYPE, Integer.TYPE });
                defineClass2 = SecurityActions.getDeclaredMethod(cl, "defineClass",
                    new Class[] { String.class, byte[].class, Integer.TYPE, Integer.TYPE, ProtectionDomain.class });
            } catch (final Exception e) {
                throw new RuntimeException("cannot initialize", e);
            }
        } else {
            try {
                // 元のコード
                // final Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
                // theUnsafe.setAccessible(true);
                // sunMiscUnsafe = (Unsafe) theUnsafe.get((Object) null);

                // このようにする。もはやsunMiscUnsafeというフィールド名は不適切だがめんどくさいのでそのまま
                sunMiscUnsafe = Unsafe.getUnsafe();
            } catch (final Throwable e) {
            }
        }
    }
}

とりあえずJava11だけで動けばいいならば、必要最小限だけ残すと、

public class DefineClassHelper {

    private static final Unsafe theUnsafe;

    public static Class<?> toClass(final String className, final ClassLoader loader, final ProtectionDomain domain,
        final byte[] bcode) throws CannotCompileException {
        try {
            return theUnsafe.defineClass(className, bcode, 0, bcode.length, loader, domain);
        } catch (final Throwable e) {
            throw new CannotCompileException(e);
        }
    }

    static Class<?> toPublicClass(final String className, final byte[] bcode) throws CannotCompileException {
        try {
            Lookup lookup = MethodHandles.lookup();
            lookup = lookup.dropLookupMode(2);
            return lookup.defineClass(bcode);
        } catch (final Throwable e) {
            throw new CannotCompileException(e);
        }
    }

    static {
        try {
            theUnsafe = Unsafe.getUnsafe();
        } catch (final Throwable e) {
            throw new ExceptionInInitializerError(e);
        }
    }
}

です。


そして、jdkの内部クラスを無理やり使うために

--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED

というオプションをjavaおよびjavacに与える必要があります。

現場からは以上です。