本文属于Java ASM系列一:Core API当中的一篇。


对于《Java ASM系列一:Core API》有配套的视频讲解,可以点击这里这里进行查看;同时,也可以点击这里查看源码资料。


对于AdviceAdapter类来说,能够很容易的实现在“方法进入”和“方法退出”时添加代码。AdviceAdapter类的特点:引入了onMethodEnter()方法和onMethodExit()方法。

1. AdviceAdapter类

1.1 class info

第一个部分,AdviceAdapter类是一个抽象的(abstract)、特殊的MethodVisitor类。

具体来说,AdviceAdapter类继承自GeneratorAdapter类,而GeneratorAdapter类继承自LocalVariablesSorter类,LocalVariablesSorter类继承自MethodVisitor类。

  • org.objectweb.asm.MethodVisitor
    • org.objectweb.asm.commons.LocalVariablesSorter
      • org.objectweb.asm.commons.GeneratorAdapter
        • org.objectweb.asm.commons.AdviceAdapter

由于AdviceAdapter类是抽象类(abstract),如果我们想使用这个类,那么就需要实现一个具体的子类。

public abstract class AdviceAdapter extends GeneratorAdapter implements Opcodes {
}

1.2 fields

第二个部分,AdviceAdapter类定义的字段有哪些。其中,isConstructor字段是判断当前方法是不是构造方法。如果当前方法是构造方法,在“方法进入”时添加代码,需要特殊处理。

public abstract class AdviceAdapter extends GeneratorAdapter implements Opcodes {
    protected int methodAccess;
    protected String methodDesc;

    private final boolean isConstructor;
}

1.3 constructors

第三个部分,AdviceAdapter类定义的构造方法有哪些。需要注意的是,AdviceAdapter的构造方法是用protected修饰,因此这个构造方法只能在子类当中访问。换句话说,在外界不能用new关键字来创建对象。

public abstract class AdviceAdapter extends GeneratorAdapter implements Opcodes {
    protected AdviceAdapter(final int api, final MethodVisitor methodVisitor,
                            final int access, final String name, final String descriptor) {
        super(api, methodVisitor, access, name, descriptor);
        methodAccess = access;
        methodDesc = descriptor;
        isConstructor = "<init>".equals(name);
    }
}

1.4 methods

第四个部分,AdviceAdapter类定义的方法有哪些。

AdviceAdapter类的方法中,定义了两个重要的方法:onMethodEnter()方法和onMethodExit()方法。

public abstract class AdviceAdapter extends GeneratorAdapter implements Opcodes {
    // Generates the "before" advice for the visited method.
    // The default implementation of this method does nothing.
    // Subclasses can use or change all the local variables, but should not change state of the stack.
    // This method is called at the beginning of the method or
    // after super class constructor has been called (in constructors).
    protected void onMethodEnter() {}

    // Generates the "after" advice for the visited method.
    // The default implementation of this method does nothing.
    // Subclasses can use or change all the local variables, but should not change state of the stack.
    // This method is called at the end of the method, just before return and athrow instructions.
    // The top element on the stack contains the return value or the exception instance.
    protected void onMethodExit(final int opcode) {}
}

对于onMethodEnter()onMethodExit()这两个方法,我们从三个角度来把握它们:

  • 第一个角度,应用场景。
    • onMethodEnter()方法:在“方法进入”的时候,添加一些代码逻辑。
    • onMethodExit()方法:在“方法退出”的时候,添加一些代码逻辑。
  • 第二个角度,注意事项。
    • 第一点,对于onMethodEnter()onMethodExit()这两个方法,都要注意Subclasses can use or change all the local variables, but should not change state of the stack。也就是说,要保持operand stack在修改前和修改后是一致的。
    • 第二点,对于onMethodExit()方法,要注意The top element on the stack contains the return value or the exception instance。也就是说,“方法退出”的时候,operand stack上有返回值或异常对象,不要忘记处理,不要弄丢了它们。
  • 第三个角度,工作原理。
    • 对于onMethodEnter()方法,它是借助于visitCode()方法来实现的。使用onMethodEnter()方法的优势在于,它能够处理&lt;init&gt;()的复杂情况,而直接使用visitCode()方法则可能导致&lt;init&gt;()方法出现错误。
    • 对于onMethodExit()方法,它是借助于visitInsn(int opcode)方法来实现的。

2. 示例: 打印方法参数和返回值

2.1 预期目标

假如有一个HelloWorld类,代码如下:

public class HelloWorld {
    private String name;
    private int age;

    public HelloWorld(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void test(long idCard, Object obj) {
        int hashCode = 0;
        hashCode += name.hashCode();
        hashCode += age;
        hashCode += (int) (idCard % Integer.MAX_VALUE);
        hashCode += obj.hashCode();
        hashCode = Math.abs(hashCode);
        System.out.println("Hash Code is " + hashCode);
        if (hashCode % 2 == 1) {
            throw new RuntimeException("illegal");
        }
    }
}

我们想实现的预期目标:打印出构造方法(&lt;init&gt;())和test()的参数和返回值。

2.2 编码实现

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

public class ParameterUtils {
    private static final DateFormat fm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void printValueOnStack(Object value) {
        if (value == null) {
            System.out.println("    " + value);
        }
        else if (value instanceof String) {
            System.out.println("    " + value);
        }
        else if (value instanceof Date) {
            System.out.println("    " + fm.format(value));
        }
        else if (value instanceof char[]) {
            System.out.println("    " + Arrays.toString((char[])value));
        }
        else {
            System.out.println("    " + value.getClass() + ": " + value.toString());
        }
    }

    public static void printText(String str) {
        System.out.println(str);
    }
}
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;

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

public class ClassPrintParameterVisitor extends ClassVisitor {
    public ClassPrintParameterVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (mv != null) {
            boolean isAbstractMethod = (access & ACC_ABSTRACT) != 0;
            boolean isNativeMethod = (access & ACC_NATIVE) != 0;
            if (!isAbstractMethod && !isNativeMethod) {
                mv = new MethodPrintParameterAdapter(api, mv, access, name, descriptor);
            }
        }
        return mv;
    }

    public static class MethodPrintParameterAdapter extends AdviceAdapter {
        public MethodPrintParameterAdapter(int api, MethodVisitor mv, int access, String name, String descriptor) {
            super(api, mv, access, name, descriptor);
        }

        @Override
        protected void onMethodEnter() {
            printMessage("Method Enter: " + getName() + methodDesc);

            Type[] argumentTypes = getArgumentTypes();
            for (int i = 0; i < argumentTypes.length; i++) {
                Type t = argumentTypes[i];
                loadArg(i);
                box(t);
                printValueOnStack("(Ljava/lang/Object;)V");
            }
        }

        @Override
        protected void onMethodExit(int opcode) {
            printMessage("Method Exit: " + getName() + methodDesc);

            if (opcode == ATHROW) {
                super.visitLdcInsn("abnormal return");
            }
            else if (opcode == RETURN) {
                super.visitLdcInsn("return void");
            }
            else if (opcode == ARETURN) {
                dup();
            }
            else {
                if (opcode == LRETURN || opcode == DRETURN) {
                    dup2();
                }
                else {
                    dup();
                }
                box(getReturnType());
            }
            printValueOnStack("(Ljava/lang/Object;)V");
        }

        private void printMessage(String str) {
            super.visitLdcInsn(str);
            super.visitMethodInsn(INVOKESTATIC, "sample/ParameterUtils", "printText", "(Ljava/lang/String;)V", false);
        }

        private void printValueOnStack(String descriptor) {
            super.visitMethodInsn(INVOKESTATIC, "sample/ParameterUtils", "printValueOnStack", descriptor, false);
        }
    }
}

2.3 进行转换

import lsieun.utils.FileUtils;
import org.objectweb.asm.Cla***eader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

public class HelloWorldTransformCore {
    public static void main(String[] args) {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);
        byte[] bytes1 = FileUtils.readBytes(filepath);

        //(1)构建Cla***eader
        Cla***eader cr = new Cla***eader(bytes1);

        //(2)构建ClassWriter
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        //(3)串连ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv = new ClassPrintParameterVisitor(api, cw);

        //(4)结合Cla***eader和ClassVisitor
        int parsingOptions = Cla***eader.SKIP_DEBUG | Cla***eader.SKIP_FRAMES;
        cr.accept(cv, parsingOptions);

        //(5)生成byte[]
        byte[] bytes2 = cw.toByteArray();

        FileUtils.writeBytes(filepath, bytes2);
    }
}

2.4 验证结果

import java.util.Date;

public class HelloWorldRun {
    public static void main(String[] args) {
        HelloWorld instance = new HelloWorld("tomcat", 10);
        instance.test(441622197605122816L, new Date());
    }
}

3. 总结

本文对AdviceAdapter类进行介绍,内容总结如下:

  • 第一点,在AdviceAdapter类当中,有两个关键的方法,即onMethodEnter()onMethodExit()方法。我们可以从三个角度来把握这两个方法:
    • 从使用场景的角度来说,AdviceAdapter类能够很容易的在“方法进入”时和“方法退出”时添加一些代码。
    • 从注意事项的角度来说,Subclasses can use or change all the local variables, but should not change state of the stack.
    • 从工作原理的角度来说,onMethodEnter()方法是借助于visitCode()方法来实现的;onMethodExit()方法是借助于visitInsn(int opcode)方法来实现的。
  • 第二点,特殊的情况的处理。有些时候,使用AdviceAdapter类的onMethodEnter()onMethodExit()方法是不能正常工作的。比如说,一些代码经过混淆(obfuscate)之后,ByteCode的内容就会变得复杂,就会出现处理不了的情况。这个时候,我们还是应该回归到visitCode()visitInsn(opcode)方法来解决问题。