オーバーライドされたメソッドの元のメソッドを呼び出す実験

お題:

Overrideされた、元の(スーパークラスの)メソッドを
呼び出す方法ってないですかね?

Overrideされた元のメソッドを呼び出す方法 - 谷本 心 in せろ部屋

方針

JavaHouse-Brewers36709 Re super クラスのメソッドの呼び出し方によるとinvokespecialを使えば(オーバーライドされていても)指定したクラスに定義されているメソッドを直接呼べるようだ。そこで、ASM(http://asm.objectweb.org/download/index.html)を使ってinvokespecialを実行するクラスを動的に生成し、呼べるようにしてみた。


処理の流れとしては、invokespecialを実行するメソッドを含むクラスの定義(バイト列)をASMで生成→カスタムのクラスローダでクラスを生成→インスタンス化して実行、という感じ。


ちなみにASMを使ったのは今回初めて。次のページ辺りを参考にした。

ソースコード

メソッドを実行する為のインタフェースInvokerを定義

package sample.invoke;

public interface Invoker {
    Object invoke(Object target, Object[] args);
}


Methodを渡すと、そのMethodのdeclaringClassをinvokespecialで呼び出すInvokerを戻すユーティリティクラス

package sample.invoke;

import static org.objectweb.asm.Opcodes.*;

import java.lang.reflect.Method;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

public class AncestorInvokerUtils {
    /**
     * Invokerの実装クラスを生成する。
     * @param targetMethod Invokerで呼び出すメソッド
     * @return 生成したInvokerの実装クラス
     */
    public static Invoker createInvoker(Method targetMethod) {
        String className = "TemporaryInvoker"; // ダミーのクラス名
        // クラス定義を含むバイト列を生成
        byte[] classRep = createClassRep(className, targetMethod);
        // テンポラリのクラスローダを使ってクラスを生成
        ClassLoader classLoader = new TempClassLoader(className, classRep);
        Invoker invoker = null;
        try {
            Class<?> tempClass = classLoader.loadClass(className);
            // インスタンス化
            invoker = (Invoker) tempClass.newInstance();
        } catch (Exception e) {
            throw new RuntimeException("Failed to create invoker", e);
        }
        return invoker;
    }

    /**
     * クラス定義を含むバイト列を生成する。
     * クラスはInvokerの実装クラスとし、classNameで指定したクラス名で生成する。
     * invoke()メソッドは、targetMethodのdeclaringClassに対してtargetMethodのメソッドを
     * invokespecialで実行する
     */
    private static byte[] createClassRep(String className, Method targetMethod) {
        String internalClassName = className.replace('.', '/');
        ClassWriter classWriter = new ClassWriter(0);

        classWriter.visit(V1_5, 
                ACC_PUBLIC + ACC_SUPER, 
                internalClassName,  // name
                null,       // signature (Generics使用時でなければnull)
                Type.getInternalName(Object.class), // superclass
                new String[] { Type.getInternalName(Invoker.class) }); // interfaces

        // デフォルトコンストラクタを定義
        MethodVisitor constructorVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        constructorVisitor.visitCode();
        constructorVisitor.visitVarInsn(ALOAD, 0);
        constructorVisitor.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(Object.class), "<init>", "()V");
        constructorVisitor.visitInsn(RETURN);
        constructorVisitor.visitMaxs(1, 1);
        constructorVisitor.visitEnd();

        // 問題のメソッドを追加
        MethodVisitor methodVisitor = classWriter.visitMethod(
                ACC_PUBLIC, 
                "invoke", 
                "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;",
                null, null);
        methodVisitor.visitCode();
       // 第一引数を今から呼び出すメソッドのレシーバとする
        methodVisitor.visitVarInsn(ALOAD, 1); 
        methodVisitor.visitTypeInsn(CHECKCAST, 
                Type.getInternalName(targetMethod.getDeclaringClass()));
       // 第二引数の中身を今から呼び出すメソッドの引数として処理する
        Class<?>[] paramTypes = targetMethod.getParameterTypes();
        for (int i = 0; i < paramTypes.length; i++) {
            methodVisitor.visitVarInsn(ALOAD, 2);   // 第二引数の
            methodVisitor.visitIntInsn(BIPUSH, i);  // i番目の
            methodVisitor.visitInsn(AALOAD);        // 配列の中身を取り出す
            methodVisitor.visitTypeInsn(CHECKCAST, Type.getInternalName(paramTypes[i]));
        }
        // INVOKESPECIALを指定する事でまさにそのクラスのメソッドを実行
        methodVisitor.visitMethodInsn(INVOKESPECIAL, 
                Type.getInternalName(targetMethod.getDeclaringClass()),
                targetMethod.getName(),
                Type.getMethodDescriptor(targetMethod));
        methodVisitor.visitInsn(ARETURN); // 呼び出したメソッドの戻り値を戻す
        methodVisitor.visitMaxs(1 + paramTypes.length, 3); // 引数にあわせてstackのサイズを確保
        methodVisitor.visitEnd();

        classWriter.visitEnd();

        return classWriter.toByteArray();
    }

    /** バイト列からクラスを生成する為のクラスローダ */
    private static class TempClassLoader extends ClassLoader {
        private String className;
        private byte[] classRep;
        /**
         * クラス名とクラスの定義を含むバイト列を指定してインスタンスを生成
         * @param className クラス名
         * @param classRep クラスの定義を含むバイト列
         */
        public TempClassLoader(String className, byte[] classRep) {
            super();
            if (className == null) {
                throw new IllegalArgumentException("className is null.");
            }
            if (classRep == null) {
                throw new IllegalArgumentException("classRep is null.");
            }
            this.className = className;
            this.classRep = classRep.clone();
        }

        @Override
        protected Class<?> findClass(String className) throws ClassNotFoundException {
            // クラス名が合致する場合だけバイト列からクラス生成
            if (!this.className.equals(className)) {
                return super.findClass(className);
            }
            Class<?> aClass = defineClass(className, this.classRep, 0, this.classRep.length);
            return aClass;
        }
    }
}

クラスローダの使い方が若干怪しいかもです。あと例外は真面目に処理してません。

使用例

次のようなBaseクラスとDelivedクラスがあるとする

class Base {
  public void aMethod() {
    System.out.println("Base#aMethod called.");
  }
}

class Delived extends Base {
  public void aMethod() {
    System.out.println("Delived#aMethod called.");
  }
}

Delivedのインスタンスに対してBaseのaMethod()を呼び出すように指定

    public static void main(String[] args) {
        try {
            Method targetMethod = Base.class.getMethod("aMethod", new Class[]{});
            Invoker invoker = AncestorInvokerUtils.createInvoker(targetMethod);
            invoker.invoke(new Delived(), new Object[]{});
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

実行。通常の実行方法だとVerifyErrorとなってしまうので、オプションに-noverifyを付ける。

terazzo% java -noverify -classpath "./lib/asm-3.1.jar:./bin" sample.invoke.AncestorInvokerSample
Base#aMethod called.

ちゃんとBaseの方が呼ばれている。

-noverifyを付けないとこうなる

terazzo% java -classpath "./lib/asm-3.1.jar:./bin" sample.invoke.AncestorInvokerSample
Exception in thread "main" java.lang.VerifyError: (class: TemporaryInvoker, method: invoke signature: (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;) Illegal use of nonvirtual function call
        at java.lang.Class.getDeclaredConstructors0(Native Method)
        at java.lang.Class.privateGetDeclaredConstructors(Class.java:2328)
        at java.lang.Class.getConstructor0(Class.java:2640)
        at java.lang.Class.newInstance0(Class.java:321)
        at java.lang.Class.newInstance(Class.java:303)
        at sample.invoke.AncestorInvokerUtils.createInvoker(AncestorInvokerUtils.java:115)
        at sample.invoke.AncestorInvokerSample.main(AncestorInvokerSample.java:9)


Integerに対してObjectのtoString()を実行した場合

    public static void main(String[] args) {
        try {
            Method targetMethod = Object.class.getMethod("toString", new Class[]{});
            Invoker invoker = AncestorInvokerUtils.createInvoker(targetMethod);
            System.out.println("0 = " + invoker.invoke(new Integer(0), new Object[]{}));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

実行結果

0 = java.lang.Integer@0

動作環境

感想

-noverifyを付けなければ行けない時点で黒魔術決定だと思う。自分ではあまり使いたくない。

追記

戻り値がプリミティブの場合うまくいかない。(例えばhashCode()など)
試しにInvokerのinvokeの戻り型をint(シグネチャも変更)にしてARETURNをIRETURNに変えれば上手くいった。
戻りの型を見てObjectに変換する処理を入れないとダメかも。