アプリ開発備忘録

PlayStationMobile、Android、UWPの開発備忘録

Javaで外部ライブラリなしでMockをする

Java/Kotlinにて、テストとかで使うのではなく、ちょっとした確認でMockを使用したかっただけでした。それくらいでMockライブラリを入れるのが面倒だったので、ライブラリ無しでMockできないかを検討してみました。

1. Mockライブラリを調べる

Mockライブラリの処理を追っていけば絶対にわかるはずです。自分は読み慣れたKotlinのMockライブラリということでMockKがいいかなとMockKを読みました。

しかしMockKはKotlin Multi Platform Projectに対応しているのでとても読みにくかったです。おすすめしない。

最終的に objenesis というライブラリを使用していることがわかりました。

mokkitoとかも objenesis を使用しているようです。

2. objenesisを調べる

objenesis.orgはとてもシンプルなライブラリです。

Mockを行う最小のコードはこの様になります。

data class Hoge(
        val value: String
)

fun main() {
    val hoge: Hoge = ObjenesisStd().getInstantiatorOf(Hoge::class.java)
            .newInstance()

    println("value is ${hoge.value}") // -> value is null
}

StdInstantiatorStrategy

ObjenesisStdではStdInstantiatorStrategyが使用されます。これは、どのようにクラスをインスタンス化するかを決定します。

どのようにインスタンス化をする とは、使用しているJVMごとにインスタンス化をする(リフレクションをする)方法が異なっているからです。

public class StdInstantiatorStrategy extends BaseInstantiatorStrategy {
    public StdInstantiatorStrategy() {
    }

    public <T> ObjectInstantiator<T> newInstantiatorOf(Class<T> type) {
        if (!PlatformDescription.isThisJVM("Java HotSpot") && !PlatformDescription.isThisJVM("OpenJDK")) {
            if (PlatformDescription.isThisJVM("Dalvik")) {
                if (PlatformDescription.isAndroidOpenJDK()) {
                    return new UnsafeFactoryInstantiator(type);
                } else if (PlatformDescription.ANDROID_VERSION <= 10) {
                    return new Android10Instantiator(type);
                } else {
                    return (ObjectInstantiator)(PlatformDescription.ANDROID_VERSION <= 17 ? new Android17Instantiator(type) : new Android18Instantiator(type));
                }
            } else if (PlatformDescription.isThisJVM("GNU libgcj")) {
                return new GCJInstantiator(type);
            } else {
                return (ObjectInstantiator)(PlatformDescription.isThisJVM("PERC") ? new PercInstantiator(type) : new UnsafeFactoryInstantiator(type));
            }
        } else if (PlatformDescription.isGoogleAppEngine() && PlatformDescription.SPECIFICATION_VERSION.equals("1.7")) {
            return (ObjectInstantiator)(Serializable.class.isAssignableFrom(type) ? new ObjectInputStreamInstantiator(type) : new AccessibleInstantiator(type));
        } else {
            return new SunReflectionFactoryInstantiator(type);
        }
    }
}

ObjectInstantiator

使用しているJVMごとの Mockのためのリフレクションの処理ObjectInstantiator<T> の派生クラスを確認しましょう。

JVMごとの処理

ここでは、AndroidとOpenJDKの処理を見ていきます。

少しここのコードはわかりにくいです。何故なら、Mock(リフレクション)をするためにリフレクションを使用している為です。

OpenJDKの処理

newInstance が目的の処理です。そこでは mungedConstructor が呼ばれています。

@Instantiator(Typology.STANDARD)
public class SunReflectionFactoryInstantiator<T> implements ObjectInstantiator<T> {
    private final Constructor<T> mungedConstructor;

    public SunReflectionFactoryInstantiator(Class<T> type) {
        Constructor<Object> javaLangObjectConstructor = getJavaLangObjectConstructor();
        this.mungedConstructor = SunReflectionFactoryHelper.newConstructorForSerialization(type, javaLangObjectConstructor);
        this.mungedConstructor.setAccessible(true);
    }

    public T newInstance() {
        try {
            return this.mungedConstructor.newInstance((Object[])null);
        } catch (Exception var2) {
            throw new ObjenesisException(var2);
        }
    }

    private static Constructor<Object> getJavaLangObjectConstructor() {
        try {
            return Object.class.getConstructor((Class[])null);
        } catch (NoSuchMethodException var1) {
            throw new ObjenesisException(var1);
        }
    }
}

Constructor を取得している SunReflectionFactoryHelper を見ていきます。ここが一番重要な部分です。

class SunReflectionFactoryHelper {
    SunReflectionFactoryHelper() {
    }

    public static <T> Constructor<T> newConstructorForSerialization(Class<T> type, Constructor<?> constructor) {
        Class<?> reflectionFactoryClass = getReflectionFactoryClass();
        Object reflectionFactory = createReflectionFactory(reflectionFactoryClass);
        Method newConstructorForSerializationMethod = getNewConstructorForSerializationMethod(reflectionFactoryClass);

        try {
            return (Constructor)newConstructorForSerializationMethod.invoke(reflectionFactory, type, constructor);
        } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException var6) {
            throw new ObjenesisException(var6);
        }
    }

    private static Class<?> getReflectionFactoryClass() {
        try {
            return Class.forName("sun.reflect.ReflectionFactory");
        } catch (ClassNotFoundException var1) {
            throw new ObjenesisException(var1);
        }
    }

    private static Object createReflectionFactory(Class<?> reflectionFactoryClass) {
        try {
            Method method = reflectionFactoryClass.getDeclaredMethod("getReflectionFactory");
            return method.invoke((Object)null);
        } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException | NoSuchMethodException var2) {
            throw new ObjenesisException(var2);
        }
    }

    private static Method getNewConstructorForSerializationMethod(Class<?> reflectionFactoryClass) {
        try {
            return reflectionFactoryClass.getDeclaredMethod("newConstructorForSerialization", Class.class, Constructor.class);
        } catch (NoSuchMethodException var2) {
            throw new ObjenesisException(var2);
        }
    }
}

このコードをリフレクションを使わずに、Kotlinで表すとこの様になります。これがOpenJDKでのMockの方法です。

ReflectionFactory.getReflectionFactory()
        .newConstructorForSerialization(Hoge::class.java, Any::class.java.constructors.first())

Mock(リフレクション)をするためにリフレクションをしているのは、ReflectionFactoryがAndroidに無い為です。OpenJDKだけのライブラリなら良いのですが、objenesisは汎用的に使えるライブラリである為、このようになっています。

Androidの処理

Androidには複数の分岐があるので自分でコードを追ってみてください。自分はAPI29で以下のUnsafeFactoryInstantiatorが処理をしていました。

@Instantiator(Typology.STANDARD)
public class UnsafeFactoryInstantiator<T> implements ObjectInstantiator<T> {
    private final Unsafe unsafe = UnsafeUtils.getUnsafe();
    private final Class<T> type;

    public UnsafeFactoryInstantiator(Class<T> type) {
        this.type = type;
    }

    public T newInstance() {
        try {
            return this.type.cast(this.unsafe.allocateInstance(this.type));
        } catch (InstantiationException var2) {
            throw new ObjenesisException(var2);
        }
    }
}

以下のUnsafe#allocateInstance()が処理を行っています。

package sun.misc;

public final class Unsafe {
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

    public native Object allocateInstance(Class<?> var1) throws InstantiationException;
    // ~略~
}

しかし、これはAndroidから直接参照できないためリフレクションを使うしかありませんでした。

val unsafeClazz = Class.forName("sun.misc.Unsafe")
        val unsafe = unsafeClazz.getDeclaredField("theUnsafe")
                .also { it.isAccessible = true }
                .let { it[null] }
        
val hoge = unsafeClazz
        .getDeclaredMethod("allocateInstance", Class::class.java)
        .invoke(unsafe, Hoge::class.java) as Hoge

OpenJDKからは直接Unsafeが参照できましたが、Unsafe.getUnsafe() はセキュリティエラーが出るのでリフレクションを使うしかないのでReflectionFactoryから行ったほうが楽です。

おわり

OpenJDKとAndroidについて調べましたが、Androidは少し面倒ですね。自分はOpenJDKで使うために調べたので良いのですが。