ASM系列四 利用Method 组件动态注入方法逻辑 ASM字节码技术JVM动态代理AOP
这篇继续结合例子来深入了解下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 操作,也就是需要变成如下的方法:
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文件分析一遍,再进行编码。