hidekatsu-izuno 日々の記録

プログラミング、経済政策など伊津野英克が興味あることについて適当に語ります(旧サイト:A.R.N [日記])

続・Javaのクラス内が依存しているフィールド、メソッドの一覧を表示する (Java11 対応版)

大昔、「Javaのクラス内が依存しているフィールド、メソッドの一覧を表示する」という記事を書いたのだが、久々に使ってみるとどうも一部のメソッド呼び出しが抜けている。いろいろ調べていたのだが、なるほど invoke dynamic の仕業だった。

なんせ前回の記事を書いたのが 2013 年だから 7 年前か。無駄に歳を重ねてしまったものだ。おそらく当時は Java 1.7 だった……と考えるとそれほど変わったわけでもないな。1.6 と 1.8 の時代が大変長かったし、世の中的にはまだまだ Java 8 が現役だ。

閑話休題invoke dynamic 命令自体は Java 1.7 の頃から存在はしたものの、コンパイラが生成したコードに含まれるようになったのは Java 8 からで、主にラムダ式に使われている。そのため、前回の記事の時点ではあのコードで十分だったのだが、Java 8 以降で使うと漏れが発生してしまうわけだ。

そういうわけで、Javassistinvoke dynamic を扱う方法を調べていたのだが、これが意外とやっかいで困ってしまった。invoke dynamic 命令自体は、クラスの属性として定義されている Bootstrap Methods 属性に定義されている Bootstrap Method 配列の番号しか持っていない。そして Bootstrap Method は、実際のメソッドの呼び出しをラッピングするメソッド ハンドルを持っている。結局、invoke dynamic 命令⇒Bootstrap Methods 属性⇒Bootstrap Method 配列のひとつ⇒メソッド ハンドル⇒実際のメソッドと4回たどる必要があった。

最終的なソースコードは以下の通り。Bootstrap Method を取得するところと invoke virtual の前処理を除けば前回から大きな違いはない。

ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get(クラス名);

BootstrapMethod[] bsms = null;
for (AttributeInfo attr : cc.getClassFile().getAttributes()) {
    if (attr instanceof BootstrapMethodsAttribute) {
        BootstrapMethodsAttribute bsma = (BootstrapMethodsAttribute)attr;
        bsms = bsma.getMethods();
    }
}

for (CtBehavior cb : cc.getDeclaredBehaviors()) {
    MethodInfo info = cb.getMethodInfo2();
    ConstPool pool = info.getConstPool();
    CodeAttribute code = info.getCodeAttribute();

    if (code == null)
        return;

    CodeIterator i = code.iterator();
    while (i.hasNext()) {
        int pos = i.next();
        int opecode = i.byteAt(pos);
        int dynIndex = -1;
        if (opecode == Opcode.INVOKEDYNAMIC) {
            int index = i.u16bitAt(pos + 1);
            int bootstrap = pool.getInvokeDynamicBootstrap(index);
            BootstrapMethod bm = bsms[bootstrap];

            for (int arg : bm.arguments) {
                int tag = pool.getTag(arg);
                if (tag == ConstPool.CONST_MethodHandle) {
                    switch (pool.getMethodHandleKind(arg)) {
                        case ConstPool.REF_invokeVirtual:
                            opecode = Opcode.INVOKEVIRTUAL;
                            dynIndex = pool.getMethodHandleIndex(arg);
                            break;
                        case ConstPool.REF_invokeSpecial:
                            opecode = Opcode.INVOKESPECIAL;
                            dynIndex = pool.getMethodHandleIndex(arg);
                            break;
                        case ConstPool.REF_invokeStatic:
                            opecode = Opcode.INVOKESTATIC;
                            dynIndex = pool.getMethodHandleIndex(arg);
                            break;
                        case ConstPool.REF_invokeInterface:
                            opecode = Opcode.INVOKEINTERFACE;
                            dynIndex = pool.getMethodHandleIndex(arg);
                            break;
                    }
                    break;
                }
            }
        }

        switch (opecode) {
        case Opcode.NEW:
        case Opcode.ANEWARRAY:
        case Opcode.MULTIANEWARRAY: {
            int index = i.u16bitAt(pos + 1);
            
            System.out.print(cc.getName());
            System.out.print('\t');
            System.out.print(cb.getLongName());
            System.out.print('\t');
            System.out.print("new");
            System.out.print('\t');
            
            String className = pool.getClassInfo(index);
            if (opecode == Opcode.ANEWARRAY || opecode == Opcode.MULTIANEWARRAY) {
                className = "new " + className + "[]";
            }
            
            System.out.print(className);
            System.out.println();
            break;
        }
        case Opcode.NEWARRAY: {
            System.out.print(cc.getName());
            System.out.print('\t');
            System.out.print(cb.getLongName());
            System.out.print('\t');
            System.out.print("new");
            System.out.print('\t');
            
            String className = null;
            switch (i.byteAt(pos + 1)) {
            case Opcode.T_BOOLEAN: className = "new boolean[]"; break;
            case Opcode.T_CHAR: className = "new char[]"; break;
            case Opcode.T_BYTE: className = "new byte[]"; break;
            case Opcode.T_SHORT: className = "new short[]"; break;
            case Opcode.T_INT: className = "new int[]"; break;
            case Opcode.T_LONG: className = "new long[]"; break;
            case Opcode.T_FLOAT: className = "new float[]"; break;
            case Opcode.T_DOUBLE: className = "new double[]"; break;
            }
            
            System.out.print(className);
            System.out.println();
            break;
        }
        case Opcode.CHECKCAST: {
            int index = i.u16bitAt(pos + 1);
            
            System.out.print(cc.getName());
            System.out.print('\t');
            System.out.print(cb.getLongName());
            System.out.print('\t');
            System.out.print("cast");
            System.out.print('\t');
            
            String className = "(" + pool.getClassInfo(index) + ")";
            
            System.out.print(className);
            System.out.println();
            break;
        }
        case Opcode.GETSTATIC:
        case Opcode.PUTSTATIC:
        case Opcode.GETFIELD:
        case Opcode.PUTFIELD: {
            int index = i.u16bitAt(pos + 1);
            
            System.out.print(cc.getName());
            System.out.print('\t');
            System.out.print(cb.getLongName());
            System.out.print('\t');
            System.out.print("field");
            
            String className = pool.getFieldrefClassName(index);
            String fieldName = pool.getFieldrefName(index);
            String desc = pool.getFieldrefType(index);
            String typeName = Descriptor.toClassName(desc);
            
            System.out.print('\t');
            System.out.print(typeName);
            System.out.print(' ');
            System.out.print(className);
            System.out.print('.');
            System.out.print(fieldName);
            System.out.println();
            
            break;
        }
        case Opcode.INVOKEVIRTUAL:
        case Opcode.INVOKESPECIAL:
        case Opcode.INVOKESTATIC:
        case Opcode.INVOKEINTERFACE: {
            int index = dynIndex == -1 ? i.u16bitAt(pos + 1) : dynIndex;
            
            System.out.print(cc.getName());
            System.out.print('\t');
            System.out.print(cb.getLongName());
            System.out.print('\t');
            System.out.print("method");
            
            String className;
            if (opecode == Opcode.INVOKEINTERFACE) {
                className = pool.getInterfaceMethodrefClassName(index);
            } else {
                className = pool.getMethodrefClassName(index);
            }
            
            String methodName;
            if (opecode == Opcode.INVOKEINTERFACE) {
                methodName = pool.getInterfaceMethodrefName(index);
            } else {
                methodName = pool.getMethodrefName(index);
            }
            
            String desc;
            if (opecode == Opcode.INVOKEINTERFACE) {
                desc = pool.getInterfaceMethodrefType(index);
            } else {
                desc = pool.getMethodrefType(index);
            }

            String ret = null;
            List<String> params = new ArrayList<String>();
            if (desc.charAt(0) == '(') {
                int start = 1;
                boolean inClass = false;
                for (int j = start; j < desc.length(); j++) {
                    char c = desc.charAt(j);
                    if (inClass) {
                        if (c == ';') {
                            params.add(Descriptor.toClassName(desc.substring(start, j + 1)));
                            start = j + 1;
                            inClass = false;
                        }
                    } else if (c == ')') {
                        ret = Descriptor.toClassName(desc.substring(j + 1));
                        break;
                    } else if (c == 'L') {
                        inClass = true;
                    } else if (c != '[') {
                        params.add(Descriptor.toClassName(desc.substring(start, j + 1)));
                        start = j + 1;
                    }
                }
            } else {
                ret = Descriptor.toClassName(desc);
            }
            
            System.out.print('\t');
            System.out.print(ret);
            System.out.print(' ');
            System.out.print(className);
            System.out.print('.');
            System.out.print(methodName);
            System.out.print('(');
            for (int j = 0; j < params.size(); j++) {
                if (j > 0) System.out.print(',');
                System.out.print(params.get(j));
            }
            System.out.println(')');
            break;
        }
        }
    }
}