您的位置:首页 > 其它

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

2009-06-20 22:37 756 查看
方法拦截中获取相关数据的实现

如果你看不懂我的上一篇文章,请你退回去一步一步根据例子运行起来,在你运行的过程中你会加深对
程序的理解。
本篇文章主要是利用ASM来在拦截方法时获取方法调用时的相关数据,如参数列表,本地变量列表,方法
调用栈以及操作状态码等重要数据,以及方法执行时间。结合当前系统的内存状态,CPU占用率等系统信息
可以为业务逻辑出现异常时提供最可靠的分析依据。本篇要求对ByteCode有一定了解,即使你因为某些原
因不得不照抄这里的实现,我也希望你能在先使用这些实现范例后再弄懂它。能看懂这些内容并能正确使用
这些实现的,你本来就有能力弄懂它。
一个比较绕人的地方,我们来比较一下用ASM动态生成class和利用代理代理来注入代码的不同:
在利用代理来注入代码时,其实我们是利用反射来invoke一个Method对象。所以要注入的代码直接插入
在invoke的前后:
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");插入到新生成的方法的最前端。
比如原来的方法是
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
开始原始方法的生成之前最先插入,即生成新方法的指令变成:
void test{
 System.out.println("Hello,world");
 new Thread().start();
}

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


好了,假如我们现在要传出这样几个信息:
被拦截的方法所在的类,方法名称,本地变量列表(数组),参数列表(数组),操作状态码。那么:
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项目的压缩档,有很多兄弟竟然直接拿项目中我的手机号进行开发测试,弄得我收到大量的测试彩信)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐