利用ASM进行方法拦截中获取相关数据的实现

方法拦截中获取相关数据的实现

如果你看不懂我的上一篇文章,请你退回去一步一步根据例子运行起来,在你运行的过程中你会加深对
程序的理解。

本篇文章主要是利用ASM来在拦截方法时获取方法调用时的相关数据,如参数列表,本地变量列表,方法
调用栈以及操作状态码等重要数据,以及方法执行时间。结合当前系统的内存状态,CPU占用率等系统信息
可以为业务逻辑出现异常时提供最可靠的分析依据。本篇要求对ByteCode有一定了解,即使你因为某些原
因不得不照抄这里的实现,我也希望你能在先使用这些实现范例后再弄懂它。能看懂这些内容并能正确使用
这些实现的,你本来就有能力弄懂它。

一个比较绕人的地方,我们来比较一下用ASM动态生成class和利用代理代理来注入代码的不同:
在利用代理来注入代码时,其实我们是利用反射来invoke一个Method对象。所以要注入的代码直接插入
在invoke的前后:

before(); invoke(obj,m,args); after();
这样的实现非常好理解,说白了就是如果你要执行方法m,那么请你交给我来帮助你执行,这样我就可以向别
人炫耀一下:我要执行方法m了(before),我已经执行完方法m了(after)。
而在ASM动态生成class时,MyAdviceAdaptor的onMethodEnter和onMethodEixt方法是在包装某个方法
调用的,而不是运行某个方法时调用的。如果我们在onMethodEnter方法中调用
System.out.println("Hello,world");
那么是在visitMethod时调用了和要生成的新方法无关的一个System.out.println("Hello");的调用。
生成后的这个方法没有被注入任何指令,所以在执行的时候不会调用System.out.println("Hello");
正确的做法是在onMethodEnter中调用ASM操作来向要包装的方法插入指令:
visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;"); visitLdcInsn("Hello world!"); visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println", "(Ljava/lang/String;)V");
这样的才能将System.out.println("Hello,world");插入到新生成的方法的最前端。
比如原来的方法是
void test{ new Thread().start(); }
那么,利用ASM生成这个方法应该是
visitTypeInsn(NEW, "java/lang/Thread"); visitInsn(DUP); visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "()V"); visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "start", "()V"); visitInsn(RETURN);

而现在因为MyAdviceAdaptor的onMethodEnterr的方法中有插入指令的代码,所以这些代码会在MethoeVisitor
开始原始方法的生成之前最先插入,即生成新方法的指令变成:

visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;"); visitLdcInsn("Hello world!"); visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println", "(Ljava/lang/String;)V"); //这里如果栈没有平衡应该加上pop。使你插入的代码在执行完成后,原始方法代码开始之前栈还原成原始状态。 visitTypeInsn(NEW, "java/lang/Thread"); visitInsn(DUP); //还记得我去年写的为什么对象new操作后一定会DUP? //http://blog.csdn.net/axman/archive/2008/05/05/2393621.aspx visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "()V"); visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "start", "()V"); visitInsn(RETURN);

这样原来的方法就被重新生成为:

void test{ System.out.println("Hello,world"); new Thread().start(); }
同样,必须在onMethodEixt中使用ASM插入指令的方法才能将代码注入到生成后的方法中。

好了,弄明白上面的理论。我们来实现相关数据的获取。
首先,在一个方法调用时,利用ASM我们能够获取到方法的参数列表和本地变量列表。以及方法最后操作状态码
利用注入在原方法体前后的代码的时间差可以计算出原方法代码的执行时间。
因为onMethodEnter方法中插入的代码会在原始方法的所有操作包括实参传递之前插入,所以获取参数表,
本地变量表,操作状态码都在onMethodEixt方法获取:

获取本地变量列表:

void onMethodExit(int opcode){ int localVarCnt = 0; if(nextLocal > firstLocal) localVarCnt = nextLocal - firstLocal; //这两个变量是AdviceAdapter的超类实现的,因为一个MethodVisitor在visitMethod可以从原始 //的class中的某方法中分析出本地变量表。注意我说本地变量表是指静态表。而本地变量列表是说 //方法调用时实际的内存中的方法栈中的实际变量的列表。 loadVarArray(localVarCnt); } private void loadVarArray(int varCnt){ Type OBJECT_TYPE = Type.getObjectType("java/lang/Object"); push(varCnt); newArray(OBJECT_TYPE); for (int i = 0; i < varCnt; i++) { int index = super.firstLocal + i; dup(); push(i); loadLocal(index); box(getLocalType(index)); arrayStore(OBJECT_TYPE); } //这时已经将本地变量全部放在一个Object[]中,这就是本地变量列表,如何传出来,下面再说。 }

获取方法参数列表:
void onMethodExit(int opcode){ loadArgArray(); //这个方法也是AdviceAdapter的超类已经实现的。其实和本地变量列表获取差不多,它的具体实现: //push(argumentTypes.length); //newArray(OBJECT_TYPE); //for (int i = 0; i < argumentTypes.length; i++) { // dup(); // push(i); // loadArg(i); // box(argumentTypes[i]); // arrayStore(OBJECT_TYPE); //} //上面的loadVarArray其实就是参考这段代码,然后计算出本地变量实际的index来实现的。 }

好了,假如我们现在要传出这样几个信息:
被拦截的方法所在的类,方法名称,本地变量列表(数组),参数列表(数组),操作状态码。那么:
void onMethodExit(int opcode){ visitLdcInsn(className); visitLdcInsn(methodName); //前一篇文章我们只用了方法的sortName,真正实现时应该用FullName,因为 //方法有重载,只凭sortName不能限定到某一个方法。 visitLdcInsn(String.valueOf(opcode)); int localVarCnt = 0; if(nextLocal > firstLocal) { localVarCnt = nextLocal - firstLocal; loadVarArray(localVarCnt); }else{ visitInsn(ACONST_NULL); //为了占用一个栈位置。 } int argsCnt = 0; argCnt = xxx; //从方法签名可以分析出参数个数。 if(argsCnt > 0){ loadArgArray(); } else{ visitInsn(ACONST_NULL); //为了占用一个栈位置。 } }
OK,现在栈顶向下的五个操作数分别是args,vars,opcode,mthodName,className,我们当然可以同样用ASM指令

生成代码将栈中的五个操作数传递到指定的目的地,但这样的操作太复杂,我们只要调用一个有五个参数的方法,然后在

这个方法内用普通JAVA代码自由地实现向目的地传送,而不是在被拦截的方法中用ASM代码来实现向目的地传送。

所以我们只要在外部定义好一个方法形如:
class org.axman.test.Sender{ public static void send(String className,String method,String opCode,Object[] localVar,Object[] args){ //打印 或者 保存到文件/数据库或传给其它服务处理。 } } 那么在onMethodExit中: void onMethodExit(int opcode){ visitLdcInsn(className); visitLdcInsn(methodName); //前一篇文章我们只用了方法的sortName,真正实现时应该用FullName,因为 //方法有重载,只凭sortName不能限定到某一个方法。 visitLdcInsn(String.valueOf(opcode)); int localVarCnt = 0; if(nextLocal > firstLocal) { localVarCnt = nextLocal - firstLocal; loadVarArray(localVarCnt); }else{ visitInsn(ACONST_NULL); //为了占用一个栈位置。 } int argsCnt = 0; argCnt = xxx; //从方法签名可以分析出参数个数。 if(argsCnt > 0){ loadArgArray(); } else{ visitInsn(ACONST_NULL); //为了占用一个栈位置。 } visitMethodInsn(Opcodes.INVOKESTATIC, "org.axman.test.Sender","send", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;[Ljava/lang/Object;)V"); }
就可以将被拦截的方法的实参列表,本地变量列表,操作状态码等信息传递到外部来。

获取调用栈
利用sun.reflect.Reflection.getCallerClass(x);我们只能获取调用者所在的类,如果是同一类中不同方法的
之间的调用测无法明确地查看方法调用链。
使用Throwable的getStackTrace()则可以获取完整的方法调用栈。我们只要构造一个Throwable对象就可以通过
它来获取调用这个方法的所有调用者:
public static String getInvokeStack(){ Throwable t = new Throwable(); StackTraceElement ste[] = t.getStackTrace(); StringBuilder sb = new StringBuilder(); for (int i = 1; i < ste.length; i++) { sb.append(ste[i].toString()).append(";"); } return sb.toString(); }
这里仅仅是调用StackTraceElement的toString()方法,你可以根据需要获取它的详细信息。
由于set[0]是t对象所在的方法本身,我们不需要这个调用者信息。所以循环直接从1开始。
如果Test.main()方法中调用了这个方法,那么第一个调用者直接是Test.main();
本来我们需要在onMethodEixt方法中利用ASM指令来让被拦截的方法调用这个方法,并将方法返回值压栈作为一个参数
传给send方法。但是因为被拦截的方法一定会调用send方法,所以我们直接在send中调用getInvokeStack(),那么被拦截的
方法就是第二调用者。所以我们只要将循环从2开始,然后在send方法中调用getInvokeStack();就可以获取到被拦截的方法
的所有调用链。

实际上,我们在OnMethidEnter方法中同样会将className,methodName利用类似的方法传递出来。同时在输入要拦截的方法时还会传入
一个随机串给MyAdviceAdaptor的构造方法,然后在OnMethidEnter和OnMethidEixt方法中同时会调用ASM指令把这个随机串都传给形如

Sender.send()的方法,这样可以对方法进入和退出的send数据进行配对。并可以用两个配对的send的时间差计算方法执行时间而不是把

计算原方法代码执行时间的代码注入到原方法。


其它的细节问题。以后再作交待。(整个测试项目的压缩文档需要联系获取,以前在blog上提供MMS项目的压缩档,有很多兄弟竟然直接拿项目中我的手机号进行开发测试,弄得我收到大量的测试彩信)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值