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で使うために調べたので良いのですが。