ASM核心API-方法

AMS4使用指南
实战java虚拟机

方法结构-示例

类文件如下:

public class Bean {
    private int f;

    public int getF() {
        return f;
    }

    public void setF(int f) {
        this.f = f;
    }
}

使用javap -verbose xx/xx/Bean,查看字节码指令,观察方法区:

构造方法
如果程序没有显示的定义构造函、器,编译器会自动生成一个默认的构造器Bean(){super();}

0: aload_0                   #this
1: invokespecial #1                  // Method java/lang/Object."<init>":()V
4: return

getF()方法
Xreturn,ireturn表示int, 如果是引用类型为areturn ,void则为return;

0: aload_0
1: getfield      #2                  // Field f:I
4: ireturn                  ###ireturn == return int ;

setF()方法

0: aload_0                  #### this
1: iload_1                  ### 入参 f;
2: putfield      #2                  // Field f:I
5: return     ###return void

抛出异常
增加一个稍微复杂的方法,抛出异常:

    public void checkAndSetF(int f) {
        if (f >= 0) {
            this.f = f;
        } else {
            throw new IllegalArgumentException();
        }
    }

字节码指令如下:

0: iload_1
1: iflt          12     ###如果栈顶值<=0,则跳转至label标记指定的指令,否则顺序执行
4: aload_0
5: iload_1
6: putfield      #2     // Field f:I
9: goto          20     ####无条件跳转
###创建一个异常对象,并压入栈顶。
12: new          #3     // class java/lang/IllegalArgumentException
15: dup                 ####栈顶值再入栈一次,此时栈顶前2位都是同一个值
###invokespecial 弹出栈顶元素,调用其构造函数,此时栈顶值仍然是异常对象
16: invokespecial #4    // Method java/lang/IllegalArgumentException."<init>":()V  
19: athrow    ###弹出剩下的异常的副本,
20: return

异常处理try-catch
增加try-catch方法:

public static void sleep(long d) {
    try {
        Thread.sleep(d);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

字节码指令如下:

0: lload_0
1: invokestatic  #5      // Method java/lang/Thread.sleep:(J)V
4: goto          20      ####正常执行至末尾return
7: astore_2
8: aload_2
9: invokevirtual #7     // Method java/lang/InterruptedException.printStackTrace:()V
12: goto          20
15: astore_2
16: aload_2
17: invokevirtual #9    // Method java/lang/Exception.printStackTrace:()V
20: return
Exception table:
###from-to偏移区间 出现type的异常,跳转至target
from    to  target type
  0     4     7   Class java/lang/InterruptedException
  0     4    15   Class java/lang/Exception

不存在用于捕获异常的字节代码: 而是将一个方法的字节代码与一个异常处理器列表关联在一起,这个列表规定了在某方法中一给定部分抛出异常时必须执行的代码。



MethodVisitor

用于生成和变转已编译方法的都是基于 MethodVisitor 抽象类的,它由 ClassVisitor.visitMethod()方法返回。

public abstract class MethodVisitor {
    AnnotationVisitor visitAnnotationDefault();
    AnnotationVisitor visitAnnotation(String desc, boolean visible);
    AnnotationVisitor visitParameterAnnotation(int parameter,String desc, boolean visible);
    void visitAttribute(Attribute attr);
    //..................
    void visitCode(); //方法字节码开始
//  void visitXXXInsn(int opcode);

    void visitLabel(Label label);
    void visitTryCatchBlock(Label start, Label end, Label handler,String type);

    void visitMaxs(int maxStack, int maxLocals); //设置局部变量,栈帧大小
    void visitEnd(); //方法字节码结束
}

使用MethodVisitor .visitXXXInsn()来填充函数,添加方法实现的字节码
- visitVarInsn(int opcode, int var) :带有参数的字节码指令
- visitInsn(int opcode) : 无参数的字节码指令
- visitLdcInsn(Object cst): LDC专用指令。LDC_W,LDC2_W已被废弃
- visitTypeInsn(int opcode, String type) :带有引用类型参数的字节码指令
- visitMethodInsn(int opcode, String owner, String name,String desc):调用方法
- visitFieldInsn(int opcode, String owner, String name, String desc):操作变量

MethodVisitor 类的方法必须按以下顺序调用(在这个类的 Javadoc 中规定):

visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )* ---对于非抽象方法,如果存在注释和属性的话,必须首先访问
( visitCode
   ( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |visitLocalVariable | visitLineNumber )*
visitMaxs ---visitCode与visitMaxs成对出现,出现?次。
)?
visitEnd

ClassWriter 选项
MethodVisitor.visitMaxs(int ,int)为一个方法计算局部变量和栈帧大小并不是一件容易的事。好在在创建ClassWriter 时,通过设置选项,使ASM为我们完成这些计算。

  • 在使用 new ClassWriter(0)时,不会自动计算任何东西。必须自行计算帧、局部变量与操作数栈的大小。
  • 在使用 new ClassWriter(ClassWriter.COMPUTE_MAXS)时,将为你计算局部变量与操作数栈部分的大小。还是必须调用 visitMaxs,但可以使用任何参数:它们将被忽略并重新计算。使用这一选项时,仍然必须自行计算这些帧。
  • 在使用 new ClassWriter(ClassWriter.COMPUTE_FRAMES)时,一切都是自动计算。不再需要调用 visitFrame,但仍然必须调用 visitMaxs(参数将被忽略并重新计算)。

这些选项的使用很方便,但有一个代价: COMPUTE_MAXS 选项使 ClassWriter 的速度降低 10%,而使用 COMPUTE_FRAMES 选项则使其降低一半。


生成方法

普通方法

  • 普通方法的入参中第一个参数是this。也就是说它的args_size最小是1,而静态方法是0
  • 定义静态方法必须要ACC_STATIC
  • 调用普通方法INVOKEVIRTUAL,调用static方法:INVOKESTATIC
//普通方法 empty()
mv = cw.visitMethod(ACC_PRIVATE, "empty", "()V", null, null);

//静态static emptyStatic()
mv = cw.visitMethod(ACC_PRIVATE|ACC_STATIC, "emptyStatic", "()V", null, null);

//implements java.lang.AutoCloseable.close()
mv = cw.visitMethod(ACC_INTERFACE, "close", "()V", null, new String[]{"java/lang/Exception"});

运行结果:

private void empty() {
}

private static void emptyStatic() {
}

void close() throws Exception {
}

构造方法

调用构造函数时,需要调用父类的构造函数或者类内部其他构造函数。

如果没有明显指定调用父类自定义的构造方法,那么编译器会调用默认的父类构造方法super()。
但是如果要调用父类自定义的构造方法,要在子类的构造方法中明确指定。

无参构造方法

MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD,0); //visitVarInsn获取变量 this
mv.visitMethodInsn(INVOKESPECIAL,"java/lang/Thread","<init>","()V");//自动添加:调用父类的构造函数
mv.visitInsn(RETURN);
mv.visitMaxs(1,1);
mv.visitEnd();

有参构造方法
可以使用super()或者this():因为上面的无参的构造函数中调用了super()

mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/lang/String;I)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD,0); // this
mv.visitMethodInsn(INVOKESPECIAL,clazzPath,"<init>","()V"); //调用自身的无参的构造函数,
mv.visitVarInsn(ALOAD,0); // this
mv.visitVarInsn(ALOAD,1); //获取入参 name
mv.visitFieldInsn(PUTFIELD,clazzPath,"name","Ljava/lang/String;"); 
mv.visitVarInsn(ALOAD,0); //
mv.visitVarInsn(ILOAD,2); //获取入参 age
mv.visitFieldInsn(PUTFIELD,clazzPath,"age","I");
mv.visitInsn(RETURN);
mv.visitMaxs(2,3);
mv.visitEnd();

调用结果

public User() {
    //如果没有显式的调用父类构造方法,会默认调用父类无参的构造函数super()
}

public User(String var1, int var2) {
    this(); //间接调用 无参构造函数 中的 super();
    this.name = var1;
    this.age = var2;
}

静态构造方法<clinit>

即静态语句块static {}

mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
mv.visitLdcInsn(1234L);
mv.visitMethodInsn(INVOKESTATIC,"java/lang/Long","valueOf","(J)Ljava/lang/Long;"); //Long.valueOf(long);自动装箱
mv.visitFieldInsn(PUTSTATIC,clazzPath,"clazzId","Ljava/lang/Long;");
mv.visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
mv.visitLdcInsn("static{} invoked...");
mv.visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V");
mv.visitInsn(RETURN);
mv.visitMaxs(2,0); //long 占用2单位slot
mv.visitEnd();

期望运行结果

static {
    clazzId = 1234L;
    System.out.println("static{} invoked...");
}

实际运行结果:

public static Long clazzId = Long.valueOf(1234L);
static {
     System.out.println("static{} invoked...");
 }


转换方法

方法可以像类一样进行转换,也就是使用一个方法适配器将它收到的方法调用转发出去,并进行一些修改:

  • 改变参数可用于改变各具体指令;
  • 不转发某一收到的调用将删除一条指令;
  • 在接收到的调用之间插入调用,将增加新的指令。

例:删除方法中的 NOP指令(因为它们不做任何事情,所以删除它们没有任何问题)
a).RemoveNopAdapter继承MethodVisitor

public class RemoveNopAdapter extends MethodVisitor {
    public RemoveNopAdapter(MethodVisitor mv) {
        super(ASM5,mv);
    }

    @Override
    public void visitInsn(int opcode) {
        if(opcode == NOP){
            return ;
        }
        super.visitInsn(opcode);
    }
}

b).RemoveNopClassAdapter继承ClassVisitor

public class RemoveNopClassAdapter extends ClassVisitor {
    public RemoveNopClassAdapter(ClassVisitor classVisitor) {
        super(ASM5, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        if (mv != null) {
            //调用MethodAdapter
            mv = new RemoveNopAdapter(mv);
        }
        return mv;
    }
}

此时,调用程序图如下:
这里写图片描述



例1:为类增加安全控制

public class Account {
    public void operation() {
        System.out.println("operation.....");
    }
}

目前Accout.operation()没有任何控制手段,现希望给operation()增加一些安全校验,判断这个方法是否有权限执行这个方法,如果有,则执行该方法;如果没有,直接退出。
增加安全校验方法:

public class SecurityChecker {
    public static boolean checkSecurity() {
        System.out.println("SecurityChecker.checkSecurity.....");
        /**
         * 简单模拟“安全校验”
         * 当前系统时间,尾数如果是偶数,则结果是1,如果是奇数则结果是0
         */
        if ((System.currentTimeMillis() & 0x1) == 0) {
            return false;
        } else {
            return true;
        }
    }
}

即将原有的Account转换为如下形式:

public class Account {
    public void operation() throws InterruptedException{
        if(SecurityChecker.checkSecurity()){
            System.out.println("operation.....");
        }   
    }
}

方法适配器AddSecurityCheckMethodAdapter

class AddSecurityCheckMethodAdapter extends MethodVisitor {

    public AddSecurityCheckMethodAdapter(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }


    @Override
    public void visitCode() {
        Label continueLabel = new Label();
        visitMethodInsn(INVOKESTATIC,"asm/security/SecurityChecker","checkSecurity","()Z");
        //IFNE i != 0 时跳转,即true的时候跳转;visitLabel-->continueLabel处,继续执行
        visitJumpInsn(IFNE,continueLabel); //如果checkSecurity=false,则不发生跳转,顺序执行,下一条指令直接返回
        visitInsn(RETURN);
        visitLabel(continueLabel);
        super.visitCode();
    }
}

类适配器TimeStatClassAdapter

class AddSecurityCheckClassAdapter extends ClassVisitor {

    public AddSecurityCheckClassAdapter(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        MethodVisitor wrappedMv = mv;
        //当遇到operation方法时,
        if(mv != null && name.equals("operation")){
            wrappedMv = new AddSecurityCheckMethodAdapter(mv);
        }
        return wrappedMv;
    }
}

例2:统计函数执行时间

统计Accout.operation()方法的执行时间:

public class Account {
    public void operation() throws InterruptedException{
        System.out.println("operation.....");
        Thread.sleep(new Random().nextInt(1000));
    }
}

我们需要统计每个方法花费的时间,需要加入如下统计功能:

public class TimeStat {
    static ThreadLocal<Long> t = new ThreadLocal<>();

    public static void start() {
        t.set(System.currentTimeMillis());
    }

    public static void end(){
        long end = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getStackTrace()+" spend:" +(end - t.get()));
    }
}

即将原有的Account转换为如下形式:

public class Account {
    public void operation() throws InterruptedException{
        TimeStat.start();
        System.out.println("operation.....");
        Thread.sleep(new Random().nextInt(1000));
        TimeStat.end();
    }
}

为了了解可以如何在 ASM 中实现它,可以编译这两个类,并针对这两个版本比较TraceClassVisitor 的输出(或者是使用默认的 Textifier 后端,或者是使用 ASMifier后端)。
TraceClassVisitor tcv = new TraceClassVisitor(cw, new ASMifier(),new PrintWriter(System.out));

方法适配器TimeStatMethodAdapter

class TimeStatMethodAdapter extends MethodVisitor {

    public TimeStatMethodAdapter(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }


    @Override
    public void visitCode() {
        //方法执行开始,增加TimeStat.start();
        visitMethodInsn(INVOKESTATIC, "asm/timer/TimeStat", "start", "()V");
        super.visitCode();
    }

    @Override
    public void visitInsn(int opcode) {
    /*  int IRETURN = 172; // visitInsn
        int LRETURN = 173; // -
        int FRETURN = 174; // -
        int DRETURN = 175; // -
        int ARETURN = 176; // -
        int RETURN = 177; // -*/
        //方法返回前,增加TimeStat.end();
        if (opcode >= IRETURN && opcode <= RETURN) {
            visitMethodInsn(INVOKESTATIC, "asm/timer/TimeStat", "end", "()V");
        }
        super.visitInsn(opcode);
    }
}

类适配器TimeStatClassAdapter

class TimeStatClassAdapter extends ClassVisitor {

    public TimeStatClassAdapter(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        MethodVisitor wrappedMv = mv;
        //判断,如果是operation方法时,使用适配方法
        if (mv != null && name.equals("operation")) {
            wrappedMv = new TimeStatMethodAdapter(mv);
        }
        return wrappedMv;
    }
}

工具

  • Type:t.getOpcode(IMUL),若 t 等于 Type.FLOAT_TYPE,则返回 FMUL
  • TraceClassVisitor它打印它所访问类的文本表示, 包括类的方法的文本表示,可以使用TraceMethodVisitor代替TraceClassVisitor
public MethodVisitor visitMethod(int access, String name,String desc, String signature, String[] exceptions) {
    MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);
    if (debug && mv != null && ...) { // 如果必须跟踪此方法
        Printer p = new Textifier(ASM5) {
            @Override 
            public void visitMethodEnd() {
                print(aPrintWriter); // 在其被访问后输出它
            }
        };
        mv = new TraceMethodVisitor(mv, p);
    }
    return new MyMethodAdapter(mv);
}
  • CheckClassAdapter来检验ClassVisitor 方法的调用顺序是否适当,参数是否有效,与之类似的,CheckMethodAdapter来检查一个方法,而不是检查整个类。
  • AnalyzerAdapter:这个方法适配器根据 visitFrame 中访问的帧,计算每条指令之前的栈映射帧。

LocalVariablesSorter
这个方法适配器将一个方法中使用的局部变量按照它们在这个方法中的出现顺序重新进行编号。在向一个方法中插入新的局部变量时,这个适配器很有用。没有这个适配器,就需要在所有已有局部变量之后添加新的局部变量,但遗憾的是,在 visitMaxs 中,要直到方法的末尾处才能知道这些局部变量的编号。
假设需要一个这样局部变量来记录方法的执行时间:

public class Account  {
        public static long timer;
    public void operation() throws Exception {
        long t = System.currentTimeMillis();
        System.out.println("operation.....");
        Thread.sleep(100);
        timer += System.currentTimeMillis() - t;
    }
}

那么MethodAdapter

class TimeStatMethodAdapter2 extends MethodVisitor {
    private LocalVariablesSorter lvs;
    private AnalyzerAdapter aa;
    private String owner;
    private int time;
    private int maxStack;

    public TimeStatMethodAdapter2(String owner,MethodVisitor mv,LocalVariablesSorter lvs , AnalyzerAdapter aa) {
        super(ASM5, mv);
        this.owner = owner;
        this.lvs = lvs;//由于java是单继承,所以这里set
        this.aa = aa;
    }
    @Override public void visitCode() {
        mv.visitCode();
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System","currentTimeMillis", "()J");
        time = lvs.newLocal(Type.LONG_TYPE); //newLocal()
        mv.visitVarInsn(LSTORE, time);
        maxStack = 4;
    }

    @Override public void visitInsn(int opcode) {
        if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System","currentTimeMillis", "()J");
            mv.visitVarInsn(LLOAD, time);
            mv.visitInsn(LSUB); //sub
            mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
            mv.visitInsn(LADD);
            mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
            maxStack = Math.max(aa.stack.size() + 4, maxStack);
        }
        mv.visitInsn(opcode);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        mv.visitMaxs(Math.max(this.maxStack, maxStack), maxLocals);
    }
}

AdviceAdapter
这个方法适配器是一个抽象类, 可用于在一个方法的开头以及恰在任意 RETURN 或 ATHROW指令之前插入代码。

public abstract class AdviceAdapter extends GeneratorAdapter implements Opcodes {
    protected void onMethodEnter(){}

    protected void onMethodExit(int opcode) {};
}

猜你喜欢

转载自blog.csdn.net/it_freshman/article/details/81156106