[字节码系列]ObjectWeb ASM构建Method Monitor

标签: Gmail  Access  JVM  设计模式  虚拟机

      在前面的篇章中,我们看到Java Instrutment的强大能力,本篇,我们将介绍如何使用ObjectWeb ASM的字节码增强能力构建Method Monitor
      1.什么是ObjectWeb ASM

     ObjectWeb ASM是轻量级的Java字节码处理框架。它可以动态生成二进制格式的stub类或其他代理类,或者在类被JAVA虚拟机装入内存之前,动态修改类。 ASM 提供了与 BCEL和SERP相似的功能,只有22K的大小,比起350K的BCEL和150K的SERP来说,是相当小巧的,并且它有更高的执行效率,是BCEL 的7倍,SERP的11倍以上。

      在我看来,ObjectWeb ASM具有如下几个非常诱人的特点

  • 小巧、高效
  • 源代码实现非常简洁而又优雅,简直就是Gof的《设计模式》非常棒的注解
  • 字节码级的控制,能够更高效地实现字节码的控制

      ObjectWeb ASM有2组接口:

  • 基于事件驱动的接口,类似于xml的SAX接口,visitor模式,在访问到类定义某个部分的时候进行回调,实现上比tree接口高效,占用内存更小
  • 基于tree的接口,类似于xml的DOM接口,将类定义解析成tree

      这里我们将使用ObjectWeb ASM的事件驱动接口
      2. 目标
      我们将对已有的字节码进行增强,收集进入方法和退出方法的信息,这里主要解决Method Monitor的字节码增强部分,不对收集后的数据处理做更深入地研究,出于演示的目的,我们定义了如下的收集方法的访问信息处理,在实际应用中,我们可能会使用更好的格式收集更多的数据、使用异步处理提高性能、使用批量处理提高处理能力、使用友好的UI显示信息等等,此处不对这部分进行探讨

Java代码 复制代码
  1. package blackstar.methodmonitor.instrutment.monitor;   
  2. public class MonitorUtil   
  3. {   
  4.     public final static String CLASS_NAME = MonitorUtil.class.getName()   
  5.             .replaceAll("\\.""/");   
  6.     public final static String ENTRY_METHOD = "entryMethod";   
  7.     public final static String EXIT_METHOD = "exitMethod";   
  8.     public final static String METHOD = "(Ljava/lang/String;Ljava/lang/String;)V";   
  9.   
  10.     public static void entryMethod(String className, String methodName)   
  11.     {   
  12.         System.out.println("entry : " + className + "." + methodName);   
  13.     }   
  14.   
  15.     public static void exitMethod(String className, String methodName)   
  16.     {   
  17.         System.out.println("exit : " + className + "." + methodName);   
  18.     }   
  19. }  
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. 从字节码开始
      实际上,对于被监控制的代码,我们所需要实现的功能如下,红色部分的代码是我们需要在动态期插到字节码中间的

public xxx method(…)
{
    try
    {
         methodEntry(…)


         methodCode
     }
      finally
     {
          methodExit(…)
     }

      这个问题看起来简单,实际则没有那么容易,因为在JVM的字节码设计中,字节码并不直接支持finally语句,而是使用try…catch来模拟的,我们先来看一个例子

Java代码 复制代码
  1. package blackstar.methodmonitor.instrutment.test;   
  2.   
  3. public class Test   
  4. {   
  5.     public void sayHello() throws Exception   
  6.     {   
  7.         try  
  8.         {   
  9.             System.out.println("hi");   
  10.         } catch (Exception e)   
  11.         {   
  12.             System.out.println("exception");   
  13.             return;   
  14.         } finally  
  15.         {   
  16.             System.out.println("finally");   
  17.         }   
  18.     }   
  19. }  
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语句的:

  • 在任何return语句之前,都会增加finally语句中的字节码
  • 定义一个捕获所有异常的语句,增加finally语句中的字节码,如果finally中没有return语句,则会将异常再次抛出去(处理方法以抛出异常的方式结束)
Exceptions:
[0-8): 11 - java.lang.Exception
[0-20): 29

     我们再看看字节码具体是如何做的

0 getstatic java.lang.System.out
3 ldc "hi" (java.lang.String)
5 invokevirtual println
8 goto 40
// System.out.println("hi");,执行完之后执行返回(goto 40)
11 astore_1
12 getstatic java.lang.System.out
15 ldc "exception" (java.lang.String)
17 invokevirtual println
// System.out.println("exception");
20 getstatic java.lang.System.out
23 ldc "finally" (java.lang.String)
25 invokevirtual println
// return语句之前插入finally部分字节码
// System.out.println("finally");

28 return
29 astore_2
30 getstatic java.lang.System.out
33 ldc "finally" (java.lang.String)
35 invokevirtual println
38 aload_2
39 athrow
//当在执行0-29语句中,如果有异常抛出,则执行这段finally语句
//此处的astore_2(将栈顶值——即exception的地址——设给第2个local变量)和aload_2(将第2个local变量的值入栈)这两个字节码实际是不必要的,但需要注意的是,如果这2段代码去掉的话,要考虑增大操作栈(max stack)以容纳这个exception地址
//System.out.println("finally");

40 getstatic java.lang.System.out
43 ldc "finally" (java.lang.String)
45 invokevirtual println
// return语句之前插入finally部分字节码
// System.out.println("finally");

48 return

    实际上,我们需要做的就是

  • 在方法进入时插入方法进入代码(需要注意,对于构造函数不允许做这种处理,构造函数第一步必须调用父类的构造函数。
  • 在每个return操作(包括return、ireturn、freturn等)之前,插入方法退出代码
  • 定义一个捕获所有异常的处理,在处理中,插入方法退出代码(即方法以抛异常的方式终止执行)

     4. 实现
      我们看看使用ObjectWeb ASM如何实现我们上面描述的功能
      1)ObjectWeb ASM的字节码修改

Java代码 复制代码
  1. ClassReader cr = new ClassReader(byteArray); //使用字节码构监一个reader   
  2. ClassWriter cw = new ClassWriter(cr, 0);//writer将基于已有的字节码进行修改   
  3. MonitorClassVisitor ca = new MonitorClassVisitor(cw);//修改处理回调类   
  4. cr.accept(ca, 0);  
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部分

Java代码 复制代码
  1. package blackstar.methodmonitor.instrutment;   
  2.   
  3. import org.objectweb.asm.ClassAdapter;   
  4. import org.objectweb.asm.ClassVisitor;   
  5. import org.objectweb.asm.MethodVisitor;   
  6.   
  7. /**  
  8.  * @author raywu ([email protected])  
  9.  *  
  10.  */  
  11. public class MonitorClassVisitor extends ClassAdapter   
  12. {   
  13.     private String className;   
  14.   
  15.     public MonitorClassVisitor(ClassVisitor classvisitor)   
  16.     {   
  17.         super(classvisitor);   
  18.     }   
  19.   
  20.     public void visit(int version, int access, String name, String signature,   
  21.             String superName, String[] interfaces)   
  22.     {   
  23.         this.className = name.replaceAll("/"".");   
  24.         super.visit(version, access, name, signature, superName, interfaces);   
  25.     }   
  26.   
  27.     public MethodVisitor visitMethod(int access, String name, String desc,   
  28.             String signature, String[] exceptions)   
  29.     {   
  30.         MethodVisitor visitor = super.visitMethod(access, name, desc,   
  31.                 signature, exceptions);   
  32.         //构造函数不修改字节码   
  33.         if ("<init>".equals(name))   
  34.         {   
  35.             return visitor;   
  36.         }   
  37.         //类定义初始化方法不修改字节码   
  38.         if ("<cinit>".equals(name))   
  39.         {   
  40.             return visitor;   
  41.         }   
  42.         //main函数不修改字节码   
  43.         if ("main".equals(name))   
  44.         {   
  45.             return visitor;   
  46.         }   
  47.   
  48.         return new MonitorAdapter(className, name, visitor);   
  49.     }   
  50. }  
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

Java代码 复制代码
  1. package blackstar.methodmonitor.instrutment;   
  2.   
  3. import org.objectweb.asm.Label;   
  4. import org.objectweb.asm.MethodAdapter;   
  5. import org.objectweb.asm.MethodVisitor;   
  6. import org.objectweb.asm.Opcodes;   
  7.   
  8. import blackstar.methodmonitor.instrutment.monitor.MonitorUtil;   
  9.   
  10. /**  
  11.  * @author raywu ([email protected])  
  12.  *  
  13.  */  
  14. public class MonitorAdapter extends MethodAdapter implements Opcodes   
  15. {   
  16.     private final static int MONITOR_STACK = 2 + 1;//max_stack至少需要能够容纳2个常量地址(监控方法使用)和1个exception地址   
  17.   
  18.     private String className;   
  19.   
  20.     private String methodName;   
  21.   
  22.     private Label start = new Label();// 方法方法字节码开始位置   
  23.     private Label end = new Label();// 方法方法字节码结束位置   
  24.   
  25.     public MonitorAdapter(String className, String methodName, MethodVisitor mv)   
  26.     {   
  27.         super(mv);   
  28.   
  29.         this.className = className;   
  30.         this.methodName = methodName;   
  31.     }   
  32.   
  33.     public void visitCode()   
  34.     {   
  35.         mv.visitCode();   
  36.   
  37.         mv.visitLabel(start);// 设置开始标志   
  38.           
  39.         //在方法开始位置,增加entry监控   
  40.         mv.visitLdcInsn(this.className);   
  41.         mv.visitLdcInsn(this.methodName);   
  42.         mv.visitMethodInsn(INVOKESTATIC, MonitorUtil.CLASS_NAME,   
  43.                 MonitorUtil.ENTRY_METHOD, MonitorUtil.METHOD);   
  44.     }   
  45.   
  46.     public void visitInsn(int opcode)   
  47.     {   
  48.         //在所有return子句之前,增加exit监控   
  49.         if (opcode >= IRETURN && opcode <= RETURN)   
  50.         {   
  51.             mv.visitLdcInsn(this.className);   
  52.             mv.visitLdcInsn(this.methodName);   
  53.             mv.visitMethodInsn(INVOKESTATIC, MonitorUtil.CLASS_NAME,   
  54.                     MonitorUtil.EXIT_METHOD, MonitorUtil.METHOD);   
  55.         }   
  56.         mv.visitInsn(opcode);   
  57.     }   
  58.   
  59.     public void visitEnd()   
  60.     {   
  61.         //从方法开始位置start到方法结束位置end部分,   
  62.         //处理方法使用抛出异常的方式终结方法执行   
  63.         mv.visitLabel(end);   
  64.         mv.visitTryCatchBlock(start, end, end, null);   
  65.   
  66.         mv.visitLdcInsn(this.className);   
  67.         mv.visitLdcInsn(this.methodName);   
  68.         mv.visitMethodInsn(INVOKESTATIC, MonitorUtil.CLASS_NAME,   
  69.                 MonitorUtil.EXIT_METHOD, MonitorUtil.METHOD);   
  70.   
  71.         mv.visitInsn(ATHROW); // 重新把异常抛出   
  72.   
  73.         mv.visitEnd();   
  74.     }   
  75.   
  76.     public void visitMaxs(int maxStack, int maxLocals)   
  77.     {   
  78.         //保证max stack足够大   
  79.         super.visitMaxs(Math.max(MONITOR_STACK, maxStack), maxLocals);   
  80.     }   
  81. }  
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方法调用

版权声明:本文为iteye_7465原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/iteye_7465/article/details/82031313

智能推荐

说说 Python Django 应用的基础目录结构

通过以下 django-admin 指令创建应用之后,就会生成应用的基础目录结构。 比如,我们建立了一个叫 ‘first’ 的应用,它的目录结构是这样的: 目录或文件 说明 最外层的 first/ 这是新应用的根目录,所有与该应用相关的内容都放在这里。 manage.py 用于管理 Django 项目的命令行工具。 里面一层的 first/ 目录 是一个...

Springboot整合rabbitMQ

依赖: 配置文件application.yml RabbitConfig 消息生产者RabbitProducer 消息消费者RabbitCustomer 通过Controller进行调用 启动项目后调用接口: 结果:...

Thread.join()方法的使用

如果一个线程A执行了thread.join()语句,代表当前线程A等待thread线程终止后才从thread.join()方法返回 并且这个方法具有超时特性,可以添加参数设置 输出结果: jdk中Thread.join()方法的源码(进行了部门调整)   每个线程终止的条件是前驱线程的终止,每个线程等待前驱线程终止后,才从join()方法返回,  当线程终止时,会调用自身的no...

linux服务器部署jenkins笔记

安装jenkins参考文档:https://blog.csdn.net/tomatocc/article/details/83930714 1. 打开jenkins官网:https://jenkins.io/download/ 将war包下载到本地 **ps:**这里要注意的是要下载左边下方的war包,不要下载右边下面的war包。左边是稳定版本,右边是最新版本,建议大家使用稳定版本(我刚开始下载的...

k8s部署elasticsearch集群

百度营销大学     环境准备 我们使用的k8s和ceph环境见: https://blog.51cto.com/leejia/2495558 https://blog.51cto.com/leejia/2499684 ECK简介 Elastic Cloud on Kubernetes,这是一款基于 Kubernetes Operator 模式的新型编排产品,用户可使用该产品在...

猜你喜欢

saas-export项目-AdminLTE介绍与入门

AdminLTE介绍 (1)AdminLTE是什么? AdminLTE是一款建立在bootstrap和jquery之上的开源的模板主题工具 (2)AdminLTE有什么特点? 提供一系列响应的、可重复使用的组件, 并内置了多个模板页面 自适应多种屏幕分辨率,兼容PC和移动端 快速的创建一个响应式的Html5网站 AdminLTE 不但美观, 而且可以免去写很大CSS与JS的工作量 AdminLTE...

MyBatis中ResultMap结果集映射

用于解决属性名和字段名不一致的情况: resultMap 元素是 MyBatis 中最重要最强大的元素。...

编写一个shell

编写shell的过程: 1.从标准输入中读入一个字符串。 2.解析字符串 3.创建一个子进程的执行程序。 4.子进程程序替换。 5.父进程等待子进程退出。...

WEB自动化测试中Xpath定位方法

前言: Xpath是在XML文档中查找信息的一种语言,使用路径表达式来选取XML文档中的节点或节点集,由于XML与HTML结构类似(前者用于传输数据,后者用于显示数据),所以Xpath也常用于查找HTML文档中的节点或节点集。 一  路径表达式: 路径以“/”开始     表示找到满足该绝对路径的元素; 路径以//”开始  ...

力扣困难难度 第4题 寻找两个正序数组的中位数

先看一眼题 我的思路: 设置下标i,j分别用于遍历两个数组,初始值均为0,直到找到两个数组中从小到大的第第length/2个数为止结束循环,length为两个数组长度之和。 ·每次比较nums[i]nums[j],如果前者小则i++,否则j++ ·循环结束时,如果count已经达到length/2,则说明已经找到了中位数,[注意:此时有可能正好其中一个数组遍历完了!所以...