java.lang.Classをインスタンス化します







コンストラクタjava.lang.Class



は、Java言語で最も保護されているエンティティの1つです。 仕様では、JVM自体のみがClass



型のオブジェクトを作成でき、ここでは何の関係もないと明確に述べていますが、本当にそうですか?







Reflection APIの深さ(だけでなく)を掘り下げて、そこにすべてを配置する方法と、既存の制限を回避することがどれほど難しいかを調べることを提案します。







デフォルト設定の64ビットJDK 1.8.0_151で実験を行っています。 Java 9については記事の最後になります。







レベル1.シンプル



最も素朴な試みから始めて、増やしていきましょう。 最初に敵に向かいましょう:







 private Class(ClassLoader loader) { classLoader = loader; }
      
      





このコンストラクタは特別なものではありません。 コンパイラーは例外を作成せず、コンストラクターもバイトコードに存在します。 したがって、他のクラスと同じことをしようとします。







 Constructor<Class> constructor = Class.class.getDeclaredConstructor(ClassLoader.class); constructor.setAccessible(true); Class<?> clazz = constructor.newInstance(ClassLoader.getSystemClassLoader());
      
      





このコードは機能せず、次のエラーが発生することが予想されます。







 Exception in thread "main" java.lang.SecurityException: Cannot make a java.lang.Class constructor accessible at java.lang.reflect.AccessibleObject.setAccessible0(...) at java.lang.reflect.AccessibleObject.setAccessible(...) at Sample.main(...)
      
      





最初の試行から、 setAccessible0



メソッドから最初の警告をsetAccessible0



java.lang.Class



クラスのコンストラクタ専用にハードコーディングされていjava.lang.Class









 private static void setAccessible0(AccessibleObject obj, boolean flag) throws SecurityException { if (obj instanceof Constructor && flag == true) { Constructor<?> c = (Constructor<?>) obj; if (c.getDeclaringClass() == Class.class) { throw new SecurityException("Cannot make a java.lang.Class" + " constructor accessible"); } } obj.override = flag; }
      
      





このメソッドのキー行は最後のものであるため、問題ではありません- override



フィールドをtrue



設定しoverride



。 これはブルートフォースを使用して簡単に実行できます。







 Field overrideConstructorField = AccessibleObject.class.getDeclaredField("override"); overrideConstructorField.setAccessible(true); overrideConstructorField.set(constructor, true);
      
      





レベル2.より難しい



当然、 override



フラグの設定が唯一の制限ではありませんが、 newInstance



メソッドの動作を少なくとももう少し進めることができます。 さらなる行動を計画するのに十分なほど。 今回は、エラーは次のようになります。







 Exception in thread "main" java.lang.InstantiationException: Can not instantiate java.lang.Class at sun.reflect.InstantiationExceptionConstructorAccessorImpl.newInstance(...) at java.lang.reflect.Constructor.newInstance(...) at Sample.main(...)
      
      





私たちはパッケージsun.reflect



クラスに直接連れて行かれ、そこで主な魔法が起こるはずであることを知っています。 newInstance



クラスの実装を調べて、そこにどのようにnewInstance



したかを調べます。







 public T newInstance(Object ... initargs) throws ... { ... ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }
      
      





実装から、 Constructor



がインスタンス化のすべての作業をConstructorAccessor



型の別のオブジェクトに委任することが明らかになります。 これは遅延方式で初期化され、それ以上変更されません。 acquireConstructorAccessor



メソッドの内部については説明しません。結果として、 newConstructorAccessor



クラスsun.reflect.ReflectionFactory



newConstructorAccessor



メソッドの呼び出しにつながるとしか言えません。 また、このメソッドがjava.lang.Class



クラスのコンストラクター(および抽象クラス)のためにInstantiationExceptionConstructorAccessorImpl



オブジェクトを返します。 彼は何かをインスタンス化する方法を知りませんが、彼へのあらゆる訴えに対して例外を投げます。 これはすべて、たった1つのことを意味します。正しいConstructorAccessor



を自分でインスタンス化する必要があります。







レベル3.ネイティブ



(上記のオブジェクトに加えて) ConstructorAccessor



オブジェクトのタイプを調べる時間:









したがって、最も一般的な状況で最も一般的なクラスの場合、オブジェクトを取得します

NativeConstructorAccessorImpl



NativeConstructorAccessorImpl



、手動で作成しようとします:







 Class<?> nativeCAClass = Class.forName("sun.reflect.NativeConstructorAccessorImpl"); Constructor<?> nativeCAConstructor = nativeCAClass.getDeclaredConstructor(Constructor.class); nativeCAConstructor.setAccessible(true); ConstructorAccessor constructorAccessor = (ConstructorAccessor) nativeCAConstructor.newInstance(constructor);
      
      





トリックはありません。オブジェクトは不必要な制限なしで作成され、残りのすべてはjava.lang.Class



をインスタンス化するためにそれを使用することです:







 Class<?> clazz = (Class<?>) constructorAccessor.newInstance( new Object[]{ClassLoader.getSystemClassLoader()});
      
      





しかし、ここで驚きが待っています:







 # # A fatal error has been detected by the Java Runtime Environment: # # SIGSEGV (0xb) at pc=0x00007f8698589ead, pid=20537, tid=0x00007f8699af3700 # # JRE version: Java(TM) SE Runtime Environment (8.0_151-b12) (build 1.8.0_151-b12) # ...
      
      





JVMは、特にすべての警告の後、そのような非論理的なアクションをユーザーに期待していないようです。 それにもかかわらず、この結果はsun.misc



ことながら成果と見なすことができますsun.misc



は失敗し、パッケージsun.misc



クラスを使用することはありません!







レベル4.マジック



ネイティブ呼び出しは機能しないため、 GeneratedConstructorAccessor



を処理する必要があります。







実際、これは単なるクラスではなく、クラスのファミリー全体です。 ランタイムのコンストラクタごとに、独自の実装が生成されます。 そのため、ネイティブ実装が主に使用されます。バイトコードを生成し、そこからクラスを作成するのは高価です。 クラス自体を生成するプロセスは、 sun.reflect.MethodAccessorGenerator



クラスのgenerateConstructor



メソッドに隠されています。 手動で呼び出すのは簡単です。







 Class<?> methodAccessorGeneratorClass = Class.forName("sun.reflect.MethodAccessorGenerator"); Constructor<?> methodAccessorGeneratorConstructor = methodAccessorGeneratorClass.getDeclaredConstructor(); methodAccessorGeneratorConstructor.setAccessible(true); Object methodAccessorGenerator = methodAccessorGeneratorConstructor.newInstance(); Method generateConstructor = methodAccessorGeneratorClass .getDeclaredMethod("generateConstructor", Class.class, Class[].class, Class[].class, int.class); generateConstructor.setAccessible(true); ConstructorAccessor constructorAccessor = (ConstructorAccessor) generateConstructor.invoke(methodAccessorGenerator, constructor.getDeclaringClass(), constructor.getParameterTypes(), constructor.getExceptionTypes(), constructor.getModifiers());
      
      





NativeConstructorAccessorImpl



の場合のNativeConstructorAccessorImpl



、落とし穴はありません-このコードは機能し、期待どおりに動作します。 しかし、少し考えてみましょう。さて、ある種のクラスを生成しました。プライベートコンストラクターを呼び出す権利はどこから来るのでしょうか。 これはすべきではないので、生成されたクラスをダンプし、そのコードを調べるだけです。 これを行うのは難しくありません-デバッガーをgenerateConstructor



メソッドに入れ、適切なタイミングで必要なバイト配列をファイルにダンプします。 逆コンパイルされたバージョンは次のとおりです(変数の名前を変更した後)。







 public class GeneratedConstructorAccessor1 extends ConstructorAccessorImpl { public Object newInstance(Object[] args) throws InvocationTargetException { Class clazz; ClassLoader classLoader; try { clazz = new Class; if (args.length != 1) { throw new IllegalArgumentException(); } classLoader = (ClassLoader) args[0]; } catch (NullPointerException | ClassCastException e) { throw new IllegalArgumentException(e.toString()); } try { clazz.<init>(classLoader); return clazz; } catch (Throwable e) { throw new InvocationTargetException(e); } } }
      
      





もちろん、このようなコードをコンパイルして戻すことはできません。これには2つの理由があります。









これらの命令は、異なるtryブロックに配置するために間隔が空いています。 なぜこれが行われるのか、私にはわかりません。 これはおそらく、例外が言語仕様に完全に準拠するように例外を処理する唯一の方法でした。







非定型の例外処理に目を向けると、このクラスの他のすべてはまったく正常ですが、彼が突然プライベートコンストラクターを呼び出す権利を得た場所はまだ明確ではありません。 すべてがスーパークラスにあることがわかります。







 abstract class ConstructorAccessorImpl extends MagicAccessorImpl implements ConstructorAccessor { public abstract Object newInstance(Object[] args) throws InstantiationException, IllegalArgumentException, InvocationTargetException; }
      
      





JVMにはsun.reflect.MagicAccessorImpl



と呼ばれる有名な松葉杖があります。 彼の相続人はそれぞれ、あらゆるクラスの個人データに無制限にアクセスできます。 これはまさにあなたが必要とするものです! クラスが魔法の場合、 java.lang.Class



インスタンスを取得するのに役立ちます。 私たちはチェックします:







 Class<?> clazz = (Class<?>) constructorAccessor.newInstance( new Object[]{ClassLoader.getSystemClassLoader()});
      
      





そして再び例外が発生します:







 Exception in thread "main" java.lang.IllegalAccessError: java.lang.Class at sun.reflect.GeneratedConstructorAccessor1.newInstance(...) at Sample.main(...)
      
      





これはすでに非常に興味深いものです。 どうやら、約束の魔法は起こらなかったようです。 または私は間違っていますか?







エラーをより慎重に検討し、 newInstance



メソッドの動作と比較する価値があります。 問題がclazz.<init>(classLoader)



場合、 InvocationTargetException



ます。 実際、 IllegalAccessError



があります。つまり、コンストラクターへの呼び出しにはIllegalAccessError



ませんでした。 NEW



命令はエラーとともに機能し、 java.lang.Class



オブジェクトにメモリを割り当てることができませんでした。 ここに私たちの力はすべてあります。







レベル5.モダン



リフレクションは問題の解決に役立ちませんでした。 たぶん、Reflectionは古くて弱いので、代わりに若くて強いMethodHandlesを使用する必要がありますか? そう思う。 少なくとも試してみる価値はあります。







そして、Reflectionが不要であると判断するとすぐに、すぐに役立ちました。 MethodHandlesはもちろん優れていますが、これを使用すると、アクセスできるデータのみを取得するのが一般的です。 また、プライベートコンストラクターが必要な場合は、昔ながらの方法を使用する必要があります。







そのため、 java.lang.Class



クラスへのプライベートアクセスを持つMethodHandles.Lookup



が必要MethodHandles.Lookup



。 この場合に非常に適したコンストラクターがあります。







 private Lookup(Class<?> lookupClass, int allowedModes) { ... }
      
      





それを呼ぶ:







 Constructor<MethodHandles.Lookup> lookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class); lookupConstructor.setAccessible(true); MethodHandles.Lookup lookup = lookupConstructor .newInstance(Class.class, MethodHandles.Lookup.PRIVATE);
      
      





lookup



を受け取ったら、必要なコンストラクタに対応するMethodHandle



オブジェクトを取得できます。







 MethodHandle handle = lookup.findConstructor(Class.class, MethodType.methodType(Class.class, ClassLoader.class));
      
      





このメソッドを開始した後、私は率直に驚いた- lookup



は、コンストラクターがクラスに確実に存在するものの、まったく存在しないふりをしている!







 Exception in thread "main" java.lang.NoSuchMethodException: no such constructor: java.lang.Class.<init>(ClassLoader)Class/newInvokeSpecial at java.lang.invoke.MemberName.makeAccessException(...) at java.lang.invoke.MemberName$Factory.resolveOrFail(...) at java.lang.invoke.MethodHandles$Lookup.resolveOrFail(...) at java.lang.invoke.MethodHandles$Lookup.findConstructor(...) at Sample.main(Sample.java:59) Caused by: java.lang.NoSuchFieldError: method resolution failed at java.lang.invoke.MethodHandleNatives.resolve(...) at java.lang.invoke.MemberName$Factory.resolve(...) at java.lang.invoke.MemberName$Factory.resolveOrFail(...) ... 3 more
      
      





奇妙なことは、例外の理由がNoSuchFieldError



です。 不思議なことに...







今回は間違っていたのは私でしたが、私はすぐにこれを理解しませんでした。 findConstructor



仕様では、 MethodType



結果が説明したとおりであるにもかかわらず、戻り値の型がvoid



であることが必要です(すべて、コンストラクターを担当する<init>



メソッドは、歴史的な理由でvoid



を返すため)

lookup



はコンストラクタを取得するための2番目のメソッドがあり、 unreflectConstructor



と呼ばれるため、 unreflectConstructor



方法で混乱を避けることができます。







 MethodHandle handle = lookup.unreflectConstructor(constructor);
      
      





このメソッドは確かに正しく動作し、必要なハンドルを返します。







真実の瞬間。 インスタンス化メソッドを実行します。







 Class<?> clazz = (Class<?>) handle. invoke(ClassLoader.getSystemClassLoader());
      
      





良いことは何も起こらないとすでに推測していると思いますが、少なくともエラーを見てみましょう。 これは新しいものです:







 Exception in thread "main" java.lang.IllegalAccessException: java.lang.Class at sun.misc.Unsafe.allocateInstance(...) at java.lang.invoke.DirectMethodHandle.allocateInstance(...) at java.lang.invoke.LambdaForm$DMH/925858445.newInvokeSpecial_L_L(...) at java.lang.invoke.LambdaForm$MH/523429237.invoke_MT(...) at Sample.main(...)
      
      





デフォルトでは、スタックトレースは短く表示されるため、追加しました

-XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames



起動パラメーターの-XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames



。 これにより、私たちがどのような奇妙な場所にいるかを理解しやすくなります。







MethodHandles



生成するクラスMethodHandles



は詳しく説明しませんが、これは重要ではありません。 別のことが重要です-最終的にsun.misc.Unsafe



を使用するsun.misc.Unsafe



になり、彼でさえjava.lang.Class



オブジェクトを作成できません。







allocaeInstance



メソッドallocaeInstance



、オブジェクトを作成したいが、そのコンストラクターを呼び出さない場所で使用されます。 これは、たとえばオブジェクトを逆シリアル化するときに役立ちます。 実際、これは同じNEW



命令ですが、アクセス制御チェックの負担はかかりません。 今見たように、 ほとんど負担はありません。







Unsafe



でもできなかったので、悲しい結論に達することができるだけです。新しいjava.lang.Class



オブジェクトを割り当てることは不可能です。 興味深いことに、コンストラクタは禁止されていて、割り当ては禁止されていると思いました! これを回避してみましょう。







レベル6.安全でない



空のオブジェクトを作成し、それが何で構成されているかを確認することを提案します。 これを行うには、 Unsafe



を使用して、新しいjava.lang.Object



を割り当てjava.lang.Object









 Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafeField.get(null); Object object = unsafe.allocateInstance(Object.class);
      
      





現在のJVMでは、結果は次のような12バイトのメモリ領域になります。













ここに表示されるのは「オブジェクトヘッダー」です。 概して、それは2つの部分で構成されています-関心のない8バイトのマークワードと、重要な4バイトのクラスワードです。







JVMはどのようにオブジェクトクラスを認識しますか? これは、クラスを記述する内部JVM構造へのポインターを格納するクラスワード領域を読み取ることでこれを行います。 したがって、この場所に別の値を書き込むと、オブジェクトのクラスが変更されます!







次のコードは非常に悪いので、絶対にしないでください。







 System.out.println(object.getClass()); unsafe.putInt(object, 8L, unsafe.getInt(Object.class, 8L)); System.out.println(object.getClass());
      
      





Object.class



のクラスワードを読み取り、それをobject



クラスワードに書き込みました。 作業の結果は次のとおりです。







 class java.lang.Object class java.lang.Class
      
      





一気に、 java.lang.Class



を割り当てたと仮定できjava.lang.Class



。 よくできました! 次に、コンストラクターを呼び出す必要があります。 笑うこともできますが、ASMを使用して、目的のコンストラクターを呼び出すことができるクラスを生成します。 当然、 MagicAccessorImpl



から継承する必要があります。







これがクラス作成の始まりです(定数は静的にインポートされるため、より短くなります)。







 ClassWriter cw = new ClassWriter(COMPUTE_FRAMES | COMPUTE_MAXS); cw.visit(V1_8, ACC_PUBLIC, "sun/reflect/MyConstructorInvocator", null, "sun/reflect/MagicAccessorImpl", null);
      
      





そこで彼はコンストラクターを作成します:







 MethodVisitor init = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); init.visitCode(); init.visitVarInsn(ALOAD, 0); init.visitMethodInsn(INVOKESPECIAL, "sun/reflect/MagicAccessorImpl", "<init>", "()V", false); init.visitInsn(RETURN); init.visitMaxs(-1, -1); init.visitEnd();
      
      





そして、 void construct(Class<?>, ClassLoader)



メソッドが作成されvoid construct(Class<?>, ClassLoader)



、内部でClass<?>



オブジェクトのコンストラクタを呼び出します:







 MethodVisitor construct = cw.visitMethod(ACC_PUBLIC, "construct", "(Ljava/lang/Class;Ljava/lang/ClassLoader;)V", null, null); construct.visitCode(); construct.visitVarInsn(ALOAD, 1); construct.visitVarInsn(ALOAD, 2); construct.visitMethodInsn(INVOKESPECIAL, "java/lang/Class", "<init>", "(Ljava/lang/ClassLoader;)V", false); construct.visitInsn(RETURN); construct.visitMaxs(-1, -1); construct.visitEnd();
      
      





クラスの準備ができました。 目的のメソッドをダウンロードし、インスタンス化し、呼び出します。







 byte[] bytes = cw.toByteArray(); Class<?> myCustomInvocator = unsafe.defineClass(null, bytes, 0, bytes.length, ClassLoader.getSystemClassLoader(), null); Object ci = myCustomInvocator.newInstance(); Method constructMethod = myCustomInvocator.getDeclaredMethod("construct", Class.class, ClassLoader.class); Class<?> clazz = (Class<?>) object; constructMethod.invoke(ci, clazz, ClassLoader.getSystemClassLoader());
      
      





そしてそれは動作します! より正確には、それが機能することは幸運です。 次のコードを実行して確認できます。







 System.out.println(clazz.getClassLoader());
      
      





出力は次のようになります。







 sun.misc.Launcher$AppClassLoader@18b4aac2
      
      





このClassLoader



場所と、後で読み込まれた場所について、私は巧みに黙秘します。 そして、予想どおり、このオブジェクトで他のほとんどすべてのメソッドを呼び出すと、すぐにJVMがクラッシュします。 そして残り-目標は完了しました!







Java 9には何がありますか?



Java 9では、ほとんど同じです。 同じことができますが、いくつか注意点があります。









また、 sun.reflect



移動し、 MyConstructorInvocator



クラスをjdk.internal.reflect



と同じローダーでロードする必要があることも検討する価値があります。

ClassLoader.getSystemClassLoader()



なくなり、アクセスできなくなります。







また、 NoSuchFieldError



奇妙なバグも修正しました。現在はNoSuchMethodError



が代わりにあり、そこにあるはずです。 些細なことですが、素晴らしい。







一般に、Java 9では、たとえそれが主な目標であったとしても、自分の足で撃つためにもっと一生懸命努力する必要があります。 これは最高だと思います。







結論:





あまりにも真剣に説明されたすべてをとるべきではありません。 java.lang.Class



インスタンス化タスク自体は完全に無意味です。 ここでは、それを解決する過程で得られた知識が重要です。







ご清聴ありがとうございました!








All Articles