在前面的篇章中,我们看到Java Instrutment的强大能力,本篇,我们将介绍如何使用ObjectWeb ASM的字节码增强能力构建Method Monitor
1.什么是ObjectWeb ASM
在我看来,ObjectWeb ASM具有如下几个非常诱人的特点
ObjectWeb ASM有2组接口:
这里我们将使用ObjectWeb ASM的事件驱动接口
2. 目标
我们将对已有的字节码进行增强,收集进入方法和退出方法的信息,这里主要解决Method Monitor的字节码增强部分,不对收集后的数据处理做更深入地研究,出于演示的目的,我们定义了如下的收集方法的访问信息处理,在实际应用中,我们可能会使用更好的格式收集更多的数据、使用异步处理提高性能、使用批量处理提高处理能力、使用友好的UI显示信息等等,此处不对这部分进行探讨
package blackstar.methodmonitor.instrutment.monitor; public class MonitorUtil { public final static String CLASS_NAME = MonitorUtil.class.getName() .replaceAll("\\.", "/"); public final static String ENTRY_METHOD = "entryMethod"; public final static String EXIT_METHOD = "exitMethod"; public final static String METHOD = "(Ljava/lang/String;Ljava/lang/String;)V"; public static void entryMethod(String className, String methodName) { System.out.println("entry : " + className + "." + methodName); } public static void exitMethod(String className, String methodName) { System.out.println("exit : " + className + "." + methodName); } }
3. 从字节码开始
实际上,对于被监控制的代码,我们所需要实现的功能如下,红色部分的代码是我们需要在动态期插到字节码中间的
这个问题看起来简单,实际则没有那么容易,因为在JVM的字节码设计中,字节码并不直接支持finally语句,而是使用try…catch来模拟的,我们先来看一个例子
package blackstar.methodmonitor.instrutment.test; public class Test { public void sayHello() throws Exception { try { System.out.println("hi"); } catch (Exception e) { System.out.println("exception"); return; } finally { System.out.println("finally"); } } }
我们看看字节码是如何处理finally语句的
首先看看异常表,异常是在JVM级别上直接支持的,下面异常表的意思是,在执行0-8语句的时候,如果有异常java.lang.Exception抛出,则进入第11语句,在执行0-20语句的时候,有任何异常抛出,都进入29语句。实际上JVM是这样实现finally语句的:
我们再看看字节码具体是如何做的
实际上,我们需要做的就是
4. 实现
我们看看使用ObjectWeb ASM如何实现我们上面描述的功能
1)ObjectWeb ASM的字节码修改
ClassReader cr = new ClassReader(byteArray); //使用字节码构监一个reader ClassWriter cw = new ClassWriter(cr, 0);//writer将基于已有的字节码进行修改 MonitorClassVisitor ca = new MonitorClassVisitor(cw);//修改处理回调类 cr.accept(ca, 0);
2)定制MonitorClassVisitor,主要处理逻辑在MonitorAdapter部分
package blackstar.methodmonitor.instrutment; import org.objectweb.asm.ClassAdapter; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; /** * @author raywu ([email protected]) * */ public class MonitorClassVisitor extends ClassAdapter { private String className; public MonitorClassVisitor(ClassVisitor classvisitor) { super(classvisitor); } public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { this.className = name.replaceAll("/", "."); super.visit(version, access, name, signature, superName, interfaces); } public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor visitor = super.visitMethod(access, name, desc, signature, exceptions); //构造函数不修改字节码 if ("<init>".equals(name)) { return visitor; } //类定义初始化方法不修改字节码 if ("<cinit>".equals(name)) { return visitor; } //main函数不修改字节码 if ("main".equals(name)) { return visitor; } return new MonitorAdapter(className, name, visitor); } }
3) 定制MethodVisitor
package blackstar.methodmonitor.instrutment; import org.objectweb.asm.Label; import org.objectweb.asm.MethodAdapter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import blackstar.methodmonitor.instrutment.monitor.MonitorUtil; /** * @author raywu ([email protected]) * */ public class MonitorAdapter extends MethodAdapter implements Opcodes { private final static int MONITOR_STACK = 2 + 1;//max_stack至少需要能够容纳2个常量地址(监控方法使用)和1个exception地址 private String className; private String methodName; private Label start = new Label();// 方法方法字节码开始位置 private Label end = new Label();// 方法方法字节码结束位置 public MonitorAdapter(String className, String methodName, MethodVisitor mv) { super(mv); this.className = className; this.methodName = methodName; } public void visitCode() { mv.visitCode(); mv.visitLabel(start);// 设置开始标志 //在方法开始位置,增加entry监控 mv.visitLdcInsn(this.className); mv.visitLdcInsn(this.methodName); mv.visitMethodInsn(INVOKESTATIC, MonitorUtil.CLASS_NAME, MonitorUtil.ENTRY_METHOD, MonitorUtil.METHOD); } public void visitInsn(int opcode) { //在所有return子句之前,增加exit监控 if (opcode >= IRETURN && opcode <= RETURN) { mv.visitLdcInsn(this.className); mv.visitLdcInsn(this.methodName); mv.visitMethodInsn(INVOKESTATIC, MonitorUtil.CLASS_NAME, MonitorUtil.EXIT_METHOD, MonitorUtil.METHOD); } mv.visitInsn(opcode); } public void visitEnd() { //从方法开始位置start到方法结束位置end部分, //处理方法使用抛出异常的方式终结方法执行 mv.visitLabel(end); mv.visitTryCatchBlock(start, end, end, null); mv.visitLdcInsn(this.className); mv.visitLdcInsn(this.methodName); mv.visitMethodInsn(INVOKESTATIC, MonitorUtil.CLASS_NAME, MonitorUtil.EXIT_METHOD, MonitorUtil.METHOD); mv.visitInsn(ATHROW); // 重新把异常抛出 mv.visitEnd(); } public void visitMaxs(int maxStack, int maxLocals) { //保证max stack足够大 super.visitMaxs(Math.max(MONITOR_STACK, maxStack), maxLocals); } }
4)我们看看最终会产生什么样的字节码,如下图,我们正确地在进入方法时加entry方法调用、每个return子句中插入exit方法调用、在方法异常抛出时插入exit方法调用