ASM(四) 利用Method 组件动态注入方法逻辑

      这篇继续结合样例来深入了解下Method组件动态变更方法字节码的实现。通过前面一篇,知道ClassVisitor 的visitMethod()方法能够返回一个MethodVisitor的实例。

那么我们也基本能够知道,同ClassVisitor改变类成员一样,MethodVIsistor假设须要改变方法成员,注入逻辑,也能够通过继承MethodVisitor,来编写一个MethodXXXAdapter来实现对于方法逻辑的注入。通过以下的两个样例来介绍下无状态注入和有状态注入方法逻辑的实现。

样例主要參考官方文档介绍,大家依照这个思路能够扩展很多其它种场景的应用。

    一、无状态注入

        先看一个样例,也是比較常见的一种场景,我们须要给以下这个类的全部方法注入一个计时的逻辑。

        源代码例如以下:

package asm.core.methord;

/**
 * Created by yunshen.ljy on 2015/6/29.
 */
public class Time {
    public void myCount() throws Exception {
        int i = 5;
        int j = 10;
        System.out.println(j - i);
    }

    public void myDeal() {
        try {
            int[] myInt = { 1, 2, 3, 4, 5 };
            int f = myInt[10];
            System.out.println(f);
        } catch (ArrayIndexOutOfBoundsException e) {
            e.printStackTrace();
        }
    }
}
 
      我们目标的class 字节码例如以下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package asm.core.methord;

public class Time {
    public static long timer;

    public Time() {
    }

    public void myCount() throws Exception {
        timer -= System.currentTimeMillis();
        byte i = 5;
        byte j = 10;
        System.out.println(j - i);
        timer += System.currentTimeMillis();
    }

    public void myDeal() {
        timer -= System.currentTimeMillis();

        try {
            int[] e = new int[]{1, 2, 3, 4, 5};
            int f = e[10];
            System.out.println(f);
        } catch (ArrayIndexOutOfBoundsException var3) {
            var3.printStackTrace();
        }

        timer += System.currentTimeMillis();
    }
}
    

      通过查看字节码结构能够知道,首先我们须要添加一个field给Time类。然后在除了构造器以外的方法注入计时逻辑的字节码。我们先以第一个方法myCount()为例,用javap工具查看字节码信息例如以下:

public void myCount() throws java.lang.Exception;
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=7, locals=3, args_size=1
         0: getstatic     #18                 // Field timer:J
         3: invokestatic  #24                 // Method java/lang/System.currentTimeMillis:()J
         6: lsub
         7: putstatic     #18                 // Field timer:J
        10: iconst_5
        11: istore_1
        12: bipush        10
        14: istore_2
        15: getstatic     #28                 // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_2
        19: iload_1
        20: isub
        21: invokevirtual #34                 // Method java/io/PrintStream.println:(I)V
        24: getstatic     #18                 // Field timer:J
        27: invokestatic  #24                 // Method java/lang/System.currentTimeMillis:()J
        30: ladd
        31: putstatic     #18                 // Field timer:J
        34: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           10      25     0  this   Lasm/core/methord/Time;
           12      23     1     i   I
           15      20     2     j   I
      LineNumberTable:
        line 8: 10
        line 9: 12
        line 10: 15
        line 11: 24
    Exceptions:
      throws java.lang.Exception
    

       从方法的偏移量0 到 7 是我们的  timer -=System.currentTimeMillis();相应的字节码实现。24 到31 是timer += System.currentTimeMillis();的字节码实现。

基本能够判定,我们须要再方法刚进入的时候先生成timer -= System.currentTimeMillis();的字节码。然后在方法返回return 指令或者是athrow指令之前生成timer+= System.currentTimeMillis()的字节码。

      timer +=System.currentTimeMillis()我们能够通过visitCode(方法開始是通过此方法的调用)方法中加入ASM提供的字节码指令生成的几个方法来实现:

@Override
        public void visitCode() {
            mv.visitCode();
            mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitInsn(Opcodes.LSUB);
            mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
        }

    

      timer -=System.currentTimeMillis()须要通过visitInsn(int opcode)方法来完毕,遍历全部的操作码来推断我们当前的指令是否是return 或者athrow 。假设是那么前插入我们须要的指令。再继续调用下一层mv.visitInsn(opcode)。

代码例如以下:

@Override
        public void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
                mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
                mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                mv.visitInsn(Opcodes.LADD);
                mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
            }
            mv.visitInsn(opcode);
        }

        那么最后还剩下。须要在class中生成一个timer的属性,如前面ClassVisitor的介绍一样,须要在ClassVisitor 的适配子类中的visitEnd()方法中插入我们的FieldVisitor。

@Override
    public void visitEnd() {
        if (!isInterface) {
            FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer", "J", null, null);
            if (fv != null) {
                fv.visitEnd();
            }
        }
        cv.visitEnd();
    }

      至此。我们的字节码已经创建和生成完成,为了健壮性考虑。我们仅仅要再加上是否是Interface的推断。由于接口是没有方法实现体的。而且还要推断,构造器方法中不加入timer计时逻辑。这里我们把须要注入逻辑的Class的name通过參数owner传递给MethodVisitor。总体Adapter方法例如以下:

package asm.core.methord;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * Created by yunshen.ljy on 2015/6/29.
 */
public class AddTimerAdapter extends ClassVisitor {
    private String owner;
    private boolean isInterface;

    public AddTimerAdapter(ClassVisitor cv) {
        super(Opcodes.ASM4, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
        owner = name;
        isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if (!isInterface && mv != null && !name.equals("<init>")) {
            mv = new AddTimerMethodAdapter(mv);
        }
        return mv;
    }

    @Override
    public void visitEnd() {
        if (!isInterface) {
            FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer", "J", null, null);
            if (fv != null) {
                fv.visitEnd();
            }
        }
        cv.visitEnd();
    }

    class AddTimerMethodAdapter extends MethodVisitor {
        public AddTimerMethodAdapter(MethodVisitor mv) {
            super(Opcodes.ASM4, mv);
        }

        @Override
        public void visitCode() {
//            mv.visitCode();
            mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitInsn(Opcodes.LSUB);
            mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
        }

        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
                mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
                mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                mv.visitInsn(Opcodes.LADD);
                mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
            }
            mv.visitInsn(opcode);
        }

        @Override
        public void visitMaxs(int maxStack, int maxLocals) {
            // 手动挡须要计算栈空间,这里两个long型变量的操作须要4个slot
            mv.visitMaxs(maxStack + 4, maxLocals);
        }

    }
}
    

二、有状态注入

        这里的有状态是相对于无状态来说的。刚才的样例中是对于方法绝对偏移量的一种逻辑注入。

简单来说就是注入的逻辑不依赖前一个指令的操作或者指令的參数。

对于Class文件的全部方法都是同样的逻辑注入。可是假设考虑一种情况,那就是当前须要注入的字节码指令依赖于前面指令的运行结果状态。那么我们就必须存储前面这个指令的状态。

       以下这个样例来源于自官方文档中的举例。考虑例如以下方法:

public void myCount(){
        int i = 5;
        int j = 10;
        System.out.println(j - i);
        System.out.println(j + i);
        System.out.println(j + 0);
          }
      这里我们知道j+0 或者j-0 的输出结果都是j。那么假设我们要让上面的代码去掉+0 以及-0这两种操作。也就是须要变成例如以下的方法:

public void myCount(){
        byte i = 5;
        byte j = 10;
        System.out.println(j - i);
        System.out.println(j + i);
        System.out.println(j);
          }
        通过查看原方法的字节码信息例如以下:


  0: iconst_5
         1: istore_1
         2: bipush        10
         4: istore_2
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: iload_2
         9: iload_1
        10: isub
        11: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        14: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        17: iload_2
        18: iload_1
        19: iadd
        20: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        23: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        26: iload_2
        27: iconst_0
        28: iadd
        29: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        32: return
        能够发现iadd 指令是iconst_0 的后置指令。可是我们不能单纯得推断当前字节码指令时iadd或者iconst_0 就直接remove。当然remove的实现方式MethodVisitor 同ClassVisitor的适配器实现方式相近,都是通过不继续调用mv.visitInsn(opcode);方法的方式。但这里我们须要标记iconst_0指令的状态。iconst_0指令运行时标记一个状态,在下一条指令运行的时候推断状态值,假设下一条命令是iadd那么就直接return掉方法来移除指令。官方实现很的优雅,这里加了一些凝视。方便理解实现。
  
package asm.core.methord;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * Created by yunshen.ljy on 2015/7/1.
 */
public class RemoveAddZeroAdapter extends MethodVisitor {
    private static int SEEN_ICONST_0 = 1;
    protected final static int SEEN_NOTHING = 0;
    protected int state;
    public RemoveAddZeroAdapter(MethodVisitor mv) {
        super(Opcodes.ASM4, mv);
    }

    @Override
    public void visitInsn(int opcode) {
        // 是否检測到前一个指令是ICONST_0
        if (state == SEEN_ICONST_0) {
            // 而且当前指令时iadd
            if (opcode == Opcodes.IADD) {
                // 又一次初始化指令状态
                state = SEEN_NOTHING;
                // 移除指令序列
                return;
            }
        }
        visitInsn();
        // 假设当前指令是ICONST_0 记录指令状态。而且直接返回(移除)
        if (opcode == Opcodes.ICONST_0) {
            state = SEEN_ICONST_0;
            return;
        }
        // 继续訪问下一条指令
        mv.visitInsn(opcode);
    }

    protected void visitInsn() {
        // 假设最后訪问的是SEEN_ICONST_0指令,那么还原指令(由于刚才被移除了)
        if (state == SEEN_ICONST_0) {
            mv.visitInsn(Opcodes.ICONST_0);
        }
        state = SEEN_NOTHING;
    }

}

      这里再补充一下,我们不须要处理StacckMapFrame以及像前一部分须要计算局部变量表和操作数栈的size,那是由于我们没有添加额外的属性,而且演示样例中也没有无条件跳转语句等,须要验证的操作。但假设我们要实现更复杂的情况,还须要覆盖visitMaxs方法、visitFrame visitLable方法等。

(保证移除指令不会影响其它指令的正常跳转。须要调用visitInsn()方法)

      事实上我个人认为。处理有状态的字节码指令移除、加入、转移还是须要注意各种字节码指令的情况。

字节码指令的顺序,上下文,栈信息对于编写一段健壮的ASM 逻辑注入代码都很关键。

有时候事实上还是建议先去把注入前后情况的class文件分析一遍,再进行编码。

原文地址:https://www.cnblogs.com/yjbjingcha/p/7112550.html