欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ASM系列八 利用TreeApi动态注入方法逻辑 ASMTreeApi动态代理字节码技术更改方法字节码 

程序员文章站 2022-05-07 10:29:17
...

 一、转换方法的字节码

 

        利用Tree Api转化方法字节码,其实也就是对MethodNode对象的InsnList的操作。通过获取InsnList的迭代器,可以直接add 或者remove方法的指令。如果需要添加比较多的指令集,那么可以把指令集分开成不同的InsnList(临时的指令集对象)再将这些子集合并。具体的代码块如下:

InsnList il = new InsnList();
il.add(...);
...
il.add(...);
mn.instructions.insert(i, il);

         下面通过一个例子来看一下。这个例子是和之前CoreApi 中介绍方法转换的例子 http://yunshen0909.iteye.com/blog/2223935相同。对比一下两种Api的方法转换实现方式的不同。

         这个例子中,我们还是对于一个Class的所有方法(除了构造器方法)注入一段计时的逻辑。整个Class我们需要先添加一个属性timer。这时候就可以堆ClassNodefields属性进行add操作。代码块如下:

int acc = Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC;
cn.fields.add(new FieldNode(acc, "timer", "J", null, null));

         我们通过AddTimerTransformer类中的transform方法来实现,对ClassNode以及其MethodNode集合的操作。AddTimerTransformer  中的注入字节码逻辑实现如下:

 

package asm.tree.method;

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

import java.util.Iterator;
import java.util.List;

/**
 * Created by yunshen.ljy on 2015/7/30.
 */
public class AddTimerTransformer  {

    public void transform(ClassNode cn) {
        for (MethodNode mn : (List<MethodNode>) cn.methods) {
            if ("<init>".equals(mn.name) || "<clinit>".equals(mn.name)) {
                continue;
            }
            InsnList insns = mn.instructions;
            if (insns.size() == 0) {
                continue;
            }
            Iterator<AbstractInsnNode> j = insns.iterator();
            while (j.hasNext()) {
                AbstractInsnNode in = j.next();
                int op = in.getOpcode();
                if ((op >= Opcodes.IRETURN && op <= Opcodes.RETURN) || op == Opcodes.ATHROW) {
                    InsnList il = new InsnList();
                    il.add(new FieldInsnNode(Opcodes.GETSTATIC, cn.name, "timer", "J"));
                    il.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J",
                            false));
                    il.add(new InsnNode(Opcodes.LADD));
                    il.add(new FieldInsnNode(Opcodes.PUTSTATIC, cn.name, "timer", "J"));
                    insns.insert(in.getPrevious(), il);
                }
            }
            InsnList il = new InsnList();
            il.add(new FieldInsnNode(Opcodes.GETSTATIC, cn.name, "timer", "J"));
            il.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false));
            il.add(new InsnNode(Opcodes.LSUB));
            il.add(new FieldInsnNode(Opcodes.PUTSTATIC, cn.name, "timer", "J"));
            insns.insert(il);
            mn.maxStack += 4;
        }
        int acc = Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC;
        cn.fields.add(new FieldNode(acc, "timer", "J", null, null));
    }
}

       注入了timer逻辑后的Class文件反编译后如下:

package asm.core.methord;

public class Time {
    public static long timer;

    public Time() {
    }

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

    public static void myMethod(int a) {
        timer -= System.currentTimeMillis();
        System.out.println(a + 0);
        timer += System.currentTimeMillis();
    }
}

 

 

         对比CoreApi 示例中的AddTimerMethodAdapter的实现,TreeApi从流式的操作字节码转换成了对于字节码集合,也就是方法字节码链表元素的操作。并且这种操作是可以非按照字节码实际偏移量来编码的,因为通过遍历所有字节码list之后对于特定字节码(return等)的逻辑注入可以不受其他字节码子集的编码位置影响(例子中我们先插入了timer += System.currentTimeMillis();在遍历结束后再插入对于下面字节码指令的实现timer -= System.currentTimeMillis();)。然后通过mn.maxStack += 4;操作maxStack属性的值,代替了像Core中需要覆盖visitMax方法(mv.visitMaxs(maxStack + 4, maxLocals);)去操作栈空间的变化。当然,整体看下来,TreeApi的操作更加便利,但代码量上来看,两种Api差距并不大。只是TreeApi更加面向对象,对开发者更加友好。

二、全局转换

        之前介绍的方法转换,迁移或者注入字节码指令都需要关注和知道字节码指令的位置。字节码指令位置关系如果写错了,那么生成的指令解析和验证就会出现问题,正如前面介绍的CoreApi的实现方式,实现起来也相当复杂。但是TreeApi 提供了任意位置来注入指令的实现方法。

 

        下面举例来看一下。还是引用之前的一个Coffee类的一段代码为例。原来的代码片段如下:

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

         这段代码编译后,用javap分析的字节码指令集如下:

public void addEspresso(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: iload_1
         1: iflt          13
         4: aload_0
         5: iload_1
         6: i2l
         7: putfield      #2                  // Field f:J
        10: goto          21
        13: new           #3                  // class java/lang/IllegalArgumentException
        16: dup
        17: invokespecial #4                  // Method java/lang/IllegalArgumentException."<init>":()V
        20: athrow
        21: return
      LineNumberTable:
        line 56: 0
        line 57: 4
        line 59: 13
        line 61: 21
      StackMapTable: number_of_entries = 2
        frame_type = 13 /* same */
        frame_type = 7 /* same */

         字节码偏移位置10那一行,goto 21 直接跳转到return指令执行。这里我们把goto 21 直接替换成return 指令。通过TreeApi我们可以对指令的相对位置进行标记和转换,也就是可以通过操作指令对象的方式来update指令。实现代码如下:

 

package asm.tree.method;

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

import java.util.Iterator;

/**
 * 将GOTO label 替换成label实际跳转到的指令-RETURN
 * Created by yunshen.ljy on 2015/8/14.
 */
public class OptimizeJumpTransformer {

    public void transform(MethodNode mn) {
        InsnList insns = mn.instructions;
        Iterator<AbstractInsnNode> i = insns.iterator();
        while (i.hasNext()) {
            AbstractInsnNode in = i.next();
            if (in instanceof JumpInsnNode) {
                // 初始化label
                LabelNode label = ((JumpInsnNode) in).label;
                AbstractInsnNode target;
                // 循环调用,将goto XX 中的XX跳转地址记录在label变量中
                while (true) {
                    target = label;   // 跳转过滤掉FrameNode 和LabelNode
                    while (target != null && target.getOpcode() < 0) {
                        target = target.getNext();
                    }
                    if (target != null && target.getOpcode() == Opcodes.GOTO) {
                        label = ((JumpInsnNode) target).label;
                    } else {
                        break;
                    }
                }
                // 更新替换label的值(实际跳转地址)
                ((JumpInsnNode) in).label = label;
                // 如果指令是goto ,并且新的跳转的目标指令是ARETURN 指令,那么就将当前的指令替换成这个return指令的一个clone对象
                if (in.getOpcode() == Opcodes.GOTO && target != null) {
                    int op = target.getOpcode();
                    if ((op >= Opcodes.IRETURN && op <= Opcodes.RETURN) || op == Opcodes.ATHROW) {
                        // replace ’in’ with clone of ’target’
                        insns.set(in, target.clone(null));
                    }
                }
            }
        }
    }
}

 

         测试方法的代码片段如下:

ClassReader cr = new ClassReader("bytecode.Coffee");
        ClassNode cn = new ClassNode();
        cr.accept(cn, 0);
        OptimizeJumpTransformer at = new OptimizeJumpTransformer();
        List<MethodNode> methodNodes = cn.methods;
        for(MethodNode mn :methodNodes){
            if(mn.name.equals("addEspresso")){
                at.transform(mn);
            }
        }

        这时候可以对比一下CoreApi 的实现方式,我们不再需要关注字节码指令的绝对位置,也不再需要处理JVM的栈图表。转换后字节码指令如下:

  public void addEspresso(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: iload_1
         1: iflt          11
         4: aload_0
         5: iload_1
         6: i2l
         7: putfield      #21                 // Field f:J
        10: return
        11: new           #23                 // class java/lang/IllegalArgumentException
        14: dup
        15: invokespecial #24                 // Method java/lang/IllegalArgumentException."<init>":()V
        18: athrow
        19: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      20     0  this   Lbytecode/Coffee;
            0      20     1     f   I
      LineNumberTable:
        line 56: 0
        line 57: 4
        line 59: 11
        line 61: 19
      StackMapTable: number_of_entries = 2
        frame_type = 11 /* same */
        frame_type = 7 /* same */

 三、MethodNode 源码解析

 

        TreeApi其实在ASM中不是独立的接口,通过和CoreApi的接口和组件结合,提供了更加友好的实现。这里以MethodNode为例。可以看到源码中MethodNode 继承于MethodVisitor。并且提供了两个accept方法,分别接受ClassVisitor以及MethodVisitor参数。accept方法处理了给予MethodNodefileds的一组事件。MethodNode本身就成为了事件的接收方。

 

       Accept方法源码如下:

/**
     * Makes the given method visitor visit this method.
     * 
     * @param mv
     *            a method visitor.
     */
    public void accept(final MethodVisitor mv) {
        // visits the method parameters
        int i, j, n;
        n = parameters == null ? 0 : parameters.size();
        for (i = 0; i < n; i++) {
            ParameterNode parameter = parameters.get(i);
            mv.visitParameter(parameter.name, parameter.access);
        }
        // visits the method attributes
        if (annotationDefault != null) {
            AnnotationVisitor av = mv.visitAnnotationDefault();
            AnnotationNode.accept(av, null, annotationDefault);
            if (av != null) {
                av.visitEnd();
            }
        }
        n = visibleAnnotations == null ? 0 : visibleAnnotations.size();
        for (i = 0; i < n; ++i) {
            AnnotationNode an = visibleAnnotations.get(i);
            an.accept(mv.visitAnnotation(an.desc, true));
        }
        n = invisibleAnnotations == null ? 0 : invisibleAnnotations.size();
        for (i = 0; i < n; ++i) {
            AnnotationNode an = invisibleAnnotations.get(i);
            an.accept(mv.visitAnnotation(an.desc, false));
        }
        n = visibleTypeAnnotations == null ? 0 : visibleTypeAnnotations.size();
        for (i = 0; i < n; ++i) {
            TypeAnnotationNode an = visibleTypeAnnotations.get(i);
            an.accept(mv.visitTypeAnnotation(an.typeRef, an.typePath, an.desc,
                    true));
        }
        n = invisibleTypeAnnotations == null ? 0 : invisibleTypeAnnotations
                .size();
        for (i = 0; i < n; ++i) {
            TypeAnnotationNode an = invisibleTypeAnnotations.get(i);
            an.accept(mv.visitTypeAnnotation(an.typeRef, an.typePath, an.desc,
                    false));
        }
        n = visibleParameterAnnotations == null ? 0
                : visibleParameterAnnotations.length;
        for (i = 0; i < n; ++i) {
            List<?> l = visibleParameterAnnotations[i];
            if (l == null) {
                continue;
            }
            for (j = 0; j < l.size(); ++j) {
                AnnotationNode an = (AnnotationNode) l.get(j);
                an.accept(mv.visitParameterAnnotation(i, an.desc, true));
            }
        }
        n = invisibleParameterAnnotations == null ? 0
                : invisibleParameterAnnotations.length;
        for (i = 0; i < n; ++i) {
            List<?> l = invisibleParameterAnnotations[i];
            if (l == null) {
                continue;
            }
            for (j = 0; j < l.size(); ++j) {
                AnnotationNode an = (AnnotationNode) l.get(j);
                an.accept(mv.visitParameterAnnotation(i, an.desc, false));
            }
        }
        if (visited) {
            instructions.resetLabels();
        }
        n = attrs == null ? 0 : attrs.size();
        for (i = 0; i < n; ++i) {
            mv.visitAttribute(attrs.get(i));
        }
        // visits the method's code
        if (instructions.size() > 0) {
            mv.visitCode();
            // visits try catch blocks
            n = tryCatchBlocks == null ? 0 : tryCatchBlocks.size();
            for (i = 0; i < n; ++i) {
                tryCatchBlocks.get(i).updateIndex(i);
                tryCatchBlocks.get(i).accept(mv);
            }
            // visits instructions
            instructions.accept(mv);
            // visits local variables
            n = localVariables == null ? 0 : localVariables.size();
            for (i = 0; i < n; ++i) {
                localVariables.get(i).accept(mv);
            }
            // visits local variable annotations
            n = visibleLocalVariableAnnotations == null ? 0
                    : visibleLocalVariableAnnotations.size();
            for (i = 0; i < n; ++i) {
                visibleLocalVariableAnnotations.get(i).accept(mv, true);
            }
            n = invisibleLocalVariableAnnotations == null ? 0
                    : invisibleLocalVariableAnnotations.size();
            for (i = 0; i < n; ++i) {
                invisibleLocalVariableAnnotations.get(i).accept(mv, false);
            }
            // visits maxs
            mv.visitMaxs(maxStack, maxLocals);
            visited = true;
        }
        mv.visitEnd();
    }

         这样我们就可以将CoreApiTreeApi 结合起来,用CoreApi处理Class,TreeApi处理Method。这样在我们自己的ClassVisitorAdapter中就可以用下面的方式来处理method中的指令对象集合:

  public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (name.startsWith("is")) {
//            System.out.println(" start with is method: " + name + desc);
        }
        return new MethodNode(Opcodes.ASM5, access, name, desc, signature, exceptions)
        {
            @Override public void visitEnd() {
                accept(cv);
            }
        };
    }