使用ASM为方法插入埋点
使用ASM为方法插入埋点
前面我们已经完成为Instrumentation注册类转换器,在类转换器中调用ClassInstrumentationFactory的modifyClass方法修改类的字节码。现在我们继续完成ClassInstrumentationFactory的modifyClass方法,在modifyClass方法中改写类的字节码,将修改后的字节码返回给类转换器。
我们使用ASM修改类,为类的所有需要监控的方法插入埋点:
◆在方法执行之前调用一个埋点方法,将方法所在的类名、方法名、方法描述符、方法参数等传给埋点方法处理。方法开始执行时间可在埋点方法中获取,即获取当前系统时间。
◆在方法执行异常时,将异常信息传给埋点方法处理;
◆在方法返回之前,获取方法返回值传给埋点方法处理。
因此,我们需要定义三个埋点方法,如代码清单7-5所示。
代码清单7-5 CallLogAspect类
public class CallLogAspect { // 方法执行之前调用的埋点方法 public static void before(String className, String methodName, String descriptor, Object[] params) { System.out.println("方法开始执行时间:" + System.currentTimeMillis()); System.out.println("类名:" + className); System.out.println("方法名:" + methodName); System.out.println("方法描述符:" + descriptor); System.out.println("参数:" + JSON.toJSONString(params)); } // 方法执行异常时调用的埋点方法 public static void error(String className, String methodName, String descriptor, Throwable throwable) { System.out.println("方法执行出现异常时间:" + System.currentTimeMillis()); System.out.println("类名:" + className); System.out.println("方法名:" + methodName); System.out.println("方法描述符:" + descriptor); System.out.println("异常信息:" + throwable.getMessage()); } // 方法返回之前调用的埋点方法 public static void after(String className, String methodName, String descriptor, Object returnValue) { System.out.println("方法执行完成时间:" + System.currentTimeMillis()); System.out.println("类名:" + className); System.out.println("方法名:" + methodName); System.out.println("方法描述符:" + descriptor); System.out.println("返回值:" + JSON.toJSONString(returnValue)); } }
现在我们希望在每个需要被监控的方法中都插入埋点,调用这三个方法。伪代码描述为:
调用CallLogAspect 的before方法; try{ [原来的方法代码] 调用CallLogAspect 的after方法; [原来的return指令] }catch(Throwable throwable){ 调用CallLogAspect 的error方法; } }
伪代码描述after埋点不是很正确,因为一个方法编译后可能生成多条return指令,我们需要在每一条return指令之前都插入一个after埋点。
modifyClass方法如代码清单7-6所示。
代码清单7-6 modifyClass方法
public static byte[] modifyClass(byte[] classfileBuffer) { ClassReader classReader = new ClassReader(classfileBuffer); // 过滤接口 if ((classReader.getAccess() & ACC_INTERFACE) == ACC_INTERFACE) { return null; } ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); // 使用继承ClassWriter的MyClassAdapter // 复写父类ClassWriter的visitMethod方法,实现改写方法 MyClassAdapter classAdapter = new MyClassAdapter( classReader.getClassName(),classWriter); classReader.accept(classAdapter, 0); return classWriter.toByteArray(); }
使用ClassWriter无法实现修改方法,所以我们定义一个适配器MyClassAdapter,用于重写visitMethod方法,在visitMethod方法判断一个方法是否需要插入埋点。同样的,我们也需要自定义一个MethodVisitor适配器,用于改写方法,如果方法需要插入埋点,则返回一个MyClassAdapter。
MyClassAdapter适配器如代码清单7-7所示。
代码清单7-7 MyClassAdapter适配器
public class MyClassAdapter extends ClassVisitor { private String className; public MyClassAdapter(String className, ClassWriter classWriter) { super(ASM6, classWriter); this.className = className; } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { // 不对私有方法注入 if ((access & ACC_PRIVATE) != 0) { return super.visitMethod(access, name, descriptor, signature, exceptions); } // 不对抽象、native等方法注入 if ((access & ACC_ABSTRACT) != 0 || (access & ACC_NATIVE) != 0 || (access & ACC_BRIDGE) != 0 || (access & ACC_SYNTHETIC) != 0) { return super.visitMethod(access, name, descriptor, signature, exceptions); } // 不对类实例初始化方法注入 if ("<init>".equals(name) || "<clinit>".equals(name)) { return super.visitMethod(access, name, descriptor, signature, exceptions); } // 先调用父类的visitMethod方法创建一个MethodVisitor MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); // 使用方法适配器修改方法 return new MyMethodAdapter(className, access, name, descriptor, mv); } }
MyClassAdapter覆写了父类ClassVisitor的visitMethod方法。当调用ClassReader实例的accept方法时,ClassReader会为每个方法调用一次MyClassAdapter实例的visitMethod方法,因为调用accept方法时我们传入的是MyClassAdapter这个类访问器。
由于MyClassAdapter覆写了visitMethod方法,因此,如果MyClassAdapter的visitMethod方法返回null,则这个方法会被抹去,如果交给父类处理,则最终会调用到创建MyClassAdapter对象时构造方法传入的ClassWriter的visitMethod方法。如果我们想要改写方法,也需要将ClassWriter返回的MethodVisitor包装为MyMethodAdapter,通过在MyMethodAdapter中覆写一些父类的方法实现。MyMethodAdapter如代码清单7-8所示。
代码清单7-8 MyMethodAdapter类
public class MyMethodAdapter extends MethodVisitor { private String className; // 方法是否是静态方法 private boolean isStaticMethod = false; private String methodName; private String descriptor; private String[] paramDescriptors; // 这三个用于为方法添加try-catch private Label from = new Label(), to = new Label(), target = new Label(); public MyMethodAdapter(String className, int access, String methodName, String descriptor, MethodVisitor methodVisitor) { super(ASM6, methodVisitor); // 根据方法的访问标志判断是否是静态方法 if ((access & ACC_STATIC) == ACC_STATIC) { isStaticMethod = true; } this.className = className; this.methodName = methodName; this.descriptor = descriptor; // 根据方法描述符获取参数类型描述符 this.paramDescriptors = ByteCodeUtils.getParamDescriptors(descriptor); } }
在MyMethodAdapter的构造方法中,判断方法是否是静态方法,用于确定方法的第一个参数是在局部变量表索引为0的Slot还是索引为1的Slot,在插入埋点代码时会用到。调用ByteCodeUtils工具类的getParamDescriptors方法是根据方法描述符获取参数的类型描述符。我们在插入调用埋点方法的字节码指令时,需要根据参数类型描述符将基本数据类型的参数转为引用类型,并将所有参数包装为一个数组传给埋点方法。
ByteCodeUtils工具类的getParamDescriptors方法的实现如代码清单7-9所示。
代码清单7-9 ByteCodeUtils的getParamDescriptors方法
public static String[] getParamDescriptors(String methodDescriptor) { List<String> paramDescriptors = new ArrayList<>(); Matcher matcher = Pattern // 使用正则匹配获取方法的参数类型描述符 .compile("(L.*?;|\\[{0,2}L.*?;|[ZCBSIFJD]|\\[{0,2}[ZCBSIFJD]{1})") .matcher(methodDescriptor.substring(0, methodDescriptor.lastIndexOf(')') + 1)); while (matcher.find()) { paramDescriptors.add(matcher.group(1)); } // 无参数直接返回null if (paramDescriptors.size() == 0) { return null; } return paramDescriptors.toArray(new String[0]); }
在方法执行之前插入埋点
实现在方法开始执行前插入埋点可通过重写MethodVisitor的visitCode方法实现,因为visitCode方法是MethodVisitor首个会被ASM调用的方法,在操作方法字节码指令之前被调用。
我们重写visitCode方法为改写的方法插入调用CallLogAspect的before静态方法的埋点代码。MyMethodAdapter重写的visitCode方法如代码清单7-10所示。
代码清单7-10 MyMethodAdapter的visitCode方法
public class MyMethodAdapter extends MethodVisitor { //...... @Override public void visitCode() { super.visitCode(); // 插入埋点代码,调用CallLogAspect的before方法 this.visitLdcInsn(this.className); this.visitLdcInsn(this.methodName); this.visitLdcInsn(this.descriptor); // 方法没有参数则传null if (paramDescriptors == null) { this.visitInsn(ACONST_NULL); } else { // 数组的大小为参数的个数 if (paramDescriptors.length >= 4) { // 大于3的立即数入栈需要使用bipush指令 mv.visitVarInsn(BIPUSH, paramDescriptors.length); } else { switch (paramDescriptors.length) { case 1: mv.visitInsn(ICONST_1); break; case 2: mv.visitInsn(ICONST_2); break; case 3: mv.visitInsn(ICONST_3); break; default: mv.visitInsn(ICONST_0); } } // 创建Object数组 mv.visitTypeInsn(ANEWARRAY, Type.getDescriptor(Object.class)); // 方法第一个参数在局部变量表中的位置 // 非静态方法排除this,即从1开始 int localIndex = isStaticMethod ? 0 : 1; // 2. 给数组赋值 for (int i = 0; i < paramDescriptors.length; i++) { // dup一份数组引用 mv.visitInsn(DUP); // 访问数组的索引 switch (i) { case 0: mv.visitInsn(ICONST_0); break; case 1: mv.visitInsn(ICONST_1); break; case 2: mv.visitInsn(ICONST_2); break; case 3: mv.visitInsn(ICONST_3); break; default: mv.visitVarInsn(BIPUSH, i); } // 将基本数据类型转为Object类型 // 调用基本数据类型对应的包装类型的valueOf静态方法 String type = paramDescriptors[i]; if ("Z".equals(type)) { mv.visitVarInsn(ILOAD, localIndex++); mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Boolean.class), "valueOf", "(Z)Ljava/lang/Boolean;", false); } else if ("C".equals(type)) { mv.visitVarInsn(ILOAD, localIndex++); mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Character.class), "valueOf", "(C)Ljava/lang/Character;", false); } else if ("B".equals(type)) { mv.visitVarInsn(ILOAD, localIndex++); mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Byte.class), "valueOf", "(B)Ljava/lang/Byte;", false); } else if ("S".equals(type)) { mv.visitVarInsn(ILOAD, localIndex++); mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Short.class), "valueOf", "(S)Ljava/lang/Short;", false); } else if ("I".equals(type)) { mv.visitVarInsn(ILOAD, localIndex++); mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Integer.class), "valueOf", "(I)Ljava/lang/Integer;", false); } else if ("F".equals(type)) { mv.visitVarInsn(FLOAD, localIndex++); mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Float.class), "valueOf", "(F)Ljava/lang/Float;", false); } else if ("J".equals(type)) { // long类型占两个slot mv.visitVarInsn(LLOAD, localIndex); localIndex += 2; mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Long.class), "valueOf", "(J)Ljava/lang/Long;", false); } else if ("D".equals(type)) { // double类型占两个slot localIndex += 2; mv.visitVarInsn(DLOAD, localIndex); mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Double.class), "valueOf", "(D)Ljava/lang/Double;", false); } else { // 数组或对象 mv.visitVarInsn(ALOAD, localIndex++); } // 给数组指定下标元素赋值 mv.visitInsn(AASTORE); } } this.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CallLogAspect.class), "before", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V", false); // 设置try代码块的开始 this.visitLabel(from); } }
在方法返回前插入埋点
实现在所有返回指令之前插入调用CallLogAspect的after埋点,可通过重写MethodVisitor的visitInsn方法实现。判断操作码是否是几条返回指令之中的一条,如果是再插入埋点。
注意:
1、是先插入埋点代码再调用父类的visitInsn方法;
2、在重写visitInsn方法时,注意不要造成死递归调用。
MyMethodAdapter的visitInsn方法如代码清单7-11所示。
代码清单7-11 MyMethodAdapter的visitInsn方法
public class MyMethodAdapter extends MethodVisitor { // ....... @Override public void visitInsn(int opcode) { // 获取最后一个局部变量的后面一个位置 int li = nextLocalIndex; switch (opcode) { case RETURN: // 方法无返回值 this.visitLdcInsn(this.className); this.visitLdcInsn(this.methodName); this.visitLdcInsn(this.descriptor); // null入栈 this.visitInsn(ACONST_NULL); this.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CallLogAspect.class), "after", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", false); break; case IRETURN: this.visitInsn(DUP); this.visitVarInsn(ISTORE, li); this.visitLdcInsn(this.className); this.visitLdcInsn(this.methodName); this.visitLdcInsn(this.descriptor); // 将返回值由int类型转为Integer类型 this.visitVarInsn(ILOAD, li); this.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Integer.class), "valueOf", "(J)Ljava/lang/Integer;", false); // 调用埋点方法 this.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CallLogAspect.class), "after", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", false); break; case FRETURN: this.visitInsn(DUP); this.visitVarInsn(FSTORE, li); this.visitLdcInsn(this.className); this.visitLdcInsn(this.methodName); this.visitLdcInsn(this.descriptor); // 将返回值由float类型转为Float类型 this.visitVarInsn(FLOAD, li); this.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Float.class), "valueOf", "(J)Ljava/lang/Float;", false); // 调用埋点方法 this.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CallLogAspect.class), "after", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", false); break; case LRETURN: // long占两个slot this.visitInsn(DUP2); this.visitVarInsn(LSTORE, li); this.visitLdcInsn(this.className); this.visitLdcInsn(this.methodName); this.visitLdcInsn(this.descriptor); // 将返回值由long类型转为Long类型 this.visitVarInsn(LLOAD, li); this.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Long.class), "valueOf", "(J)Ljava/lang/Long;", false); // 调用埋点方法 this.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CallLogAspect.class), "after", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", false); break; case DRETURN: // double占两个slot this.visitInsn(DUP2); this.visitVarInsn(DSTORE, li); this.visitLdcInsn(this.className); this.visitLdcInsn(this.methodName); this.visitLdcInsn(this.descriptor); // 将返回值由double类型转为Double类型 this.visitVarInsn(DLOAD, li); this.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Double.class), "valueOf", "(J)Ljava/lang/Double;", false); // 调用埋点方法 this.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CallLogAspect.class), "after", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", false); break; case ARETURN: // 先将返回值dup出一份,将一份保存到局部变量表 // 还有一份在操作数栈,留给ARETURN指令的 this.visitInsn(DUP); this.visitVarInsn(ASTORE, li); // 插入埋点代码,调用CallLogAspect的after方法 this.visitLdcInsn(this.className); this.visitLdcInsn(this.methodName); this.visitLdcInsn(this.descriptor); // 从局部变量表加载返回值 this.visitVarInsn(ALOAD, li); this.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CallLogAspect.class), "after", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", false); break; } super.visitInsn(opcode); } }
我们在visitInsn方法中,判断操作码是否是返回指令,在调用父类的visitInsn方法插入返回指令之前,插入调用埋点方法的字节码指令。
由于返回指令之前,操作数栈顶存储的是方法的返回值,因此需要注意,在调用埋点方法之后,操作数栈顶的元素必须是方法的返回值。因为调用埋点方法需要用到返回值,所以我们使用dup指令将栈顶的返回值复制一份,并存储到局部变量表中,为的是在调用埋点方法之前,将调用埋点方法所需的参数放入操作数栈顶时,可以从局部变量表拿到这个返回值作为调用埋点方法的最后一个参数。
CallLogAspect的after方法最后一个参数类型是Object,如果当前方法的返回值类型为基本数据类型,需要使用基本数据类型对应的包装类型的valueOf方法将基本数据类型转为引用类型,再调用CallLogAspect的after方法。因此我们为不同的返回指令编写了不同的调用埋点方法的字节码指令,如代码清单7-11所示。
由于是修改方法的字节码,我们不清楚一个方法到底有多少个局部变量,而在代码清单7-11中,我们需要使用局部变量表的一个位置临时存储方法返回值,将方法返回值存放在局部变量表的最后一个位置。因此,我们可以通过重写visitVarInsn方法来计算出当前局部变量表的大小。如代码清单7-12所示。
代码清单7-12 MyMethodAdapter的visitVarInsn方法
public class MyMethodAdapter extends MethodVisitor { // ....... private int nextLocalIndex = 0; @Override public void visitVarInsn(int opcode, int var) { super.visitVarInsn(opcode, var); if (opcode == ILOAD || opcode == FLOAD || opcode == ALOAD || opcode == ISTORE || opcode == FSTORE || opcode == ASTORE) { if (var > nextLocalIndex) { nextLocalIndex = var + 1; } } else if (opcode == LLOAD || opcode == DLOAD || opcode == LSTORE || opcode == DSTORE) { // long和double类型占用局部变量表的两个slot if (var + 1 > nextLocalIndex) { nextLocalIndex = var + 2; } } } }
捕获方法异常插入埋点
实现异常捕获,需要在MethodVisitor的visitCode方法中插入try开始标志,再重写MethodVisitor的visitMaxs方法,实现在设置局部变量表与操作数栈大小之前,添加try-catch代码块。添加try开始标志如代码清单7-10所示,visitMaxs方法如代码清单7-13所示。
代码清单7-13 MyMethodAdapter的visitMaxs方法
public class MyMethodAdapter extends MethodVisitor { // ....... @Override public void visitMaxs(int maxStack, int maxLocals) { this.visitLabel(to); this.visitLabel(target); // catch块要做的事情 // 将当前栈顶的Throwable对象存储到局部变量表,执行完埋点后需要抛出 this.visitVarInsn(ASTORE, maxLocals + 1); // 插入埋点代码,调用CallLogAspect的error方法 this.visitLdcInsn(this.className); this.visitLdcInsn(this.methodName); this.visitLdcInsn(this.descriptor); this.visitVarInsn(ALOAD, maxLocals + 1); this.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CallLogAspect.class), "error", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V", false); // 抛出Throwable异常 this.visitVarInsn(ALOAD, maxLocals + 1); this.visitInsn(ATHROW); this.visitTryCatchBlock(from, to, target, Type.getInternalName(Throwable.class)); super.visitMaxs(maxStack, maxLocals); } }
我们需要在visitMaxs中插入catch块代码,而插入catch块代码之前需要将catch住的异常保存在局部变量表,在执行完埋点方法后,将catch住的异常从局部变量表中加载到操作数栈顶,以执行athrow指令将异常抛出。
我们在创建ClassWriter对象时,添加了COMPUTE_MAXS标志,在ClassWriter的visitMethod方法的实现中,会创建一个MethodWriter,会将COMPUTE_MAXS标志传递给这个MethodWriter对象,而在MethodWriter的visitMaxs方法中,如果有COMPUTE_MAXS标志,ASM则会自动帮我们计算出新的局部变量表和操作数栈的大小。因此,在重写visitMaxs方法时,不必修改局部变量表和操作数栈的大小。
现在我们再重新打包一次my-java-agent项目,然后运行demo项目,结果输出如图7.2所示。

图7.2 运行demo项目得到的结果
demo项目被插桩后的DemoAppcliction与UserService类的代码如下:
public class UserService { public UserService() { } public Map<String, Object> queryUser(String username, Integer age) { CallLogAspect.before("com/wujiuye/demo/UserService", "queryUser", "(Ljava/lang/String;Ljava/lang/Integer;)Ljava/util/Map;", new Object[]{username, age}); try { Map<String, Object> result = new HashMap(); result.put("username", username); result.put("age", age); CallLogAspect.after("com/wujiuye/demo/UserService", "queryUser", "(Ljava/lang/String;Ljava/lang/Integer;)Ljava/util/Map;", result); return result; } catch (Throwable var6) { CallLogAspect.error("com/wujiuye/demo/UserService", "queryUser", "(Ljava/lang/String;Ljava/lang/Integer;)Ljava/util/Map;", var6); throw var6; } } } public class DemoAppcliction { public DemoAppcliction() { } public static void main(String[] var0) { CallLogAspect.before("com/wujiuye/demo/DemoAppcliction", "main", "([Ljava/lang/String;)V", new Object[]{var0}); try { System.out.println("main function runing..."); UserService userService = new UserService(); Map<String, Object> user = userService.queryUser("wujiuye", 25); System.out.println(user); CallLogAspect.after("com/wujiuye/demo/DemoAppcliction", "main", "([Ljava/lang/String;)V", (Object)null); } catch (Throwable var5) { CallLogAspect.error("com/wujiuye/demo/DemoAppcliction", "main", "([Ljava/lang/String;)V", var5); throw var5; } } }