构建自己的监测器【3】-instrumentation

原创 2012年07月29日 19:13:10

其实前一节已经看到过instrumentation了,就是在premain方法的参数里:

public static void premain(String agentArgs, Instrumentation inst);

 java.lang.instrument 在jdk5之前的版本中是没有的,它是jdk5之后引入的新特性,这个特定将java的instrument功能从native库中解脱了出来,而使用纯java的方式来解决问题。

那么java instrumentation具体能干些什么呢?

使用instrumentation开发者可以构建独立于应用程序的java agent(代理)程序,用来监测运行在JVM上的程序,甚至可以动态的修改和替换类的定义。给力的说,这种方式相当于在JVM级别做了AOP支持,这样我们可以在不修改应用程序的基础上就做到了AOP.你不必去修改应用程序的配置,也不必重新打包部署验证。


下面讲到的基本都是以jdk5为基础的,当然JDK6已经更好的支持了这个特性。比如JDK5中只能通过命令行参数在启动JVM时指定javaagent参数来设置代理类,比如:

rem start the monitor..
set JAVA_OPTS=%JAVA_OPTS% -javaagent:D:\tools\java\monitor.jar

而JDK6中已经不仅限于在启动JVM时通过配置参数来设置代理类,JDK6中通过 Java Tool API 中的 attach 方式,我们也可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。


但是作为介绍和学习起步,我觉得JDK5支持的这点特定已经够了。now,let's go!微笑

java instrument相关的类主要在Package java.lang.instrument下面,看它package的描述:

Provides services that allow Java programming language agents to instrument programs running on the JVM. The mechanism for instrumentation is modification of the byte-codes of methods.


 关于java instrument

“java.lang.instrument”包的具体实现,依赖于 JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。JVMTI 是从 Java SE 5 开始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已经消失了。JVMTI 提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。事实上,java.lang.instrument 包的实现,也就是基于这种机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作。除开 Instrumentation 功能外,JVMTI 还在虚拟机内存管理,线程控制,方法和变量操作等等方面提供了大量有价值的函数。

我认为Instrumentation 的最大作用,就是类定义动态改变和操作。在 Java SE 5 及其后续版本当中,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 –javaagent 参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序(这和上面讲到agent时是一致的,只是当时没有利用Instrumentation功能)。

现在写个简单的例子来说明java instrument的功能,这个例子很简单,就是计算某些方法的耗时,在最原始的方法中我们是这样做的,如下代码:

package monitor.agent;
/**
 * TODO Comment of MyTest
 * 
 * @author yongkang.qiyk
 */
public class MyTest {
    public static void main(String[] args) {
        sayHello();
    }
    public static void sayHello() {
        long startTime = System.currentTimeMillis();
        try {
            Thread.sleep(2000);
            System.out.println("hello world!!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("this method cost:" + (endTime - startTime) + "ms.");
    }
}

这样的方式优势劣势都很明显,优势:简单,任何人都会都能做。 劣势:假如有很多个方法要统计耗时时,需要手工在每个方法里加入上面红色部分的代码,然后编译打包部署。

如果利用Instrumentation 代理来实现这个功能是什么样的呢?

首先我们要测试的类依然是:MyTest.java,源码如下:

package monitor.agent;
/**
 * TODO Comment of MyTest
 * 
 * @author yongkang.qiyk
 */
public class MyTest {
    public static void main(String[] args) {
        sayHello();
        sayHello2("hello world222222222");
    }
    public static voidsayHello() {
        try {
            Thread.sleep(2000);
            System.out.println("hello world!!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void sayHello2(String hello) {
        try {
            Thread.sleep(1000);
            System.out.println(hello);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这一次我没有手工的加入System.currentTimeMillis();   上面是源码。我们可以直接运行它,可以得到如下结果:

hello world!!
hello world222222222

接下来,我们建立一个 Transformer 类:MonitorTransformer  。   

这个类实现了接口public interface ClassFileTransformer。  实现这个接口的目的就是在class被装载到JVM之前将class字节码转换掉,从而达到动态注入代码的目的。

那么首先要了解MonitorTransformer 这个类的目的,就是对想要修改的类做一次转换,这个用到了javassist对字节码进行修改,可以暂时不用关心jaavssist的原理,用ASM同样可以修改字节码,只不过比较麻烦些。只要知道这个类利用jaavssist将 monitor.agent.MyTest.sayHello  和 monitor.agent.MyTest.sayHello2 两个方法动态了添加了耗时统计的代码就可以了。源码如下:

/**
 * TODO Comment of MonitorTransformer
 * @author yongkang.qiyk
 *
 */
publicclass MonitorTransformerimplements ClassFileTransformer {
   
    finalstatic Stringprefix ="\nlong startTime = System.currentTimeMillis();\n";
    finalstatic Stringpostfix ="\nlong endTime = System.currentTimeMillis();\n";
    finalstatic List<String>methodList =new ArrayList<String>();
    static{
        methodList.add("monitor.agent.MyTest.sayHello");
        methodList.add("monitor.agent.MyTest.sayHello2");
    }
 
    /* (non-Javadoc)
     * @see java.lang.instrument.ClassFileTransformer#transform(java.lang.ClassLoader, java.lang.String, java.lang.Class, java.security.ProtectionDomain, byte[])
     */
    @Override
    publicbyte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,byte[] classfileBuffer)
            throws IllegalClassFormatException {
        //先判断下现在加载的class的包路径是不是需要监控的类,通过instrumentation进来的class路径用‘/’分割
        if(className.startsWith("monitor/agent")){
            //将‘/’替换为‘.’m比如monitor/agent/Mytest替换为monitor.agent.Mytest
            className = className.replace("/",".");
            CtClass ctclass = null;
            try {
                //用于取得字节码类,必须在当前的classpath中,使用全称 ,这部分是关于javassist的知识
                ctclass = ClassPool.getDefault().get(className);
            //循环一下,看看哪些方法需要加时间监测
            for(String method :methodList){
                if (method.startsWith(className)){
                         //获取方法名
                        String methodName = method.substring(method.lastIndexOf('.')+1, method.length());
                        String outputStr ="\nSystem.out.println(\"this method "+methodName+" cost:\" +(endTime - startTime) +\"ms.\");";
                        //得到这方法实例
                        CtMethod ctmethod = ctclass.getDeclaredMethod(methodName);
                        //新定义一个方法叫做比如sayHello$impl 
                        String newMethodName = methodName +"$impl";
                     //原来的方法改个名字 
                        ctmethod.setName(newMethodName);
                       
                      //创建新的方法,复制原来的方法 ,名字为原来的名字
                        CtMethod newMethod = CtNewMethod.copy(ctmethod, methodName, ctclass,null);
                        //构建新的方法体
                        StringBuilder bodyStr =new StringBuilder();
                        bodyStr.append("{");
                        bodyStr.append(prefix); 
                        //调用原有代码,类似于method();($$)表示所有的参数 
                        bodyStr.append(newMethodName +"($$);\n"); 
                 
                        bodyStr.append(postfix);
                        bodyStr.append(outputStr);
                 
                        bodyStr.append("}"); 
                        //替换新方法 
                        newMethod.setBody(bodyStr.toString());
                        //增加新方法 
                        ctclass.addMethod(newMethod); 
                }
            }    
                return ctclass.toBytecode();
            } catch (IOException e) {
                //TODO Auto-generated catch block
                e.printStackTrace();
            } catch (CannotCompileException e) {
                //TODO Auto-generated catch block
                e.printStackTrace();
            } catch (NotFoundException e) {
                //TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        returnnull;
    }
 
}

经过这个代码动态的添加代码之后原来的代码会变成和第一个手工添加System.currentTimeMillis();一样。

最后,我们还需要一个agent类,就是建立一个 Premain 类,将instrumentation注入进去,代码如下:

package monitor.agent;
 
import java.lang.instrument.Instrumentation;
 
/**
 * TODO Comment of MyAgent
 * @author yongkang.qiyk
 *
 */
publicclass MyAgent {
   
    publicstaticvoid premain(String agentArgs, Instrumentation inst){
        System.out.println("premain-1."+agentArgs);
        inst.addTransformer(new MonitorTransformer());
    }
  
}

到此为止,agent类已经修改字节码的类都已经写好了。将agent类打成jar包,

注意:MAINFESR.MF文件也打进去,最后一行一定要留空行,不然肯定会报错

Manifest-Version: 1.0
Premain-Class: monitor.agent.MyAgent
Can-Redefine-Classes: true
Boot-Class-Path: javassist.jar

空行

上面打成的jar包我叫做monitor.jar,放在D:\tools\java\monitor.jar 路径下,当然这个路径下还有刚才编写classtransformer类需要的一些第三方jar包,比如javassist.jar,在MENFEST.MF的Boot-Class-Path属性中也指定了这个jar包。

现在条件都具备了,就可以运行MyTest这个类了。

如前面两节所说的一样,要想使用agent类,需要设置JVM启动的参数。右键Run as  --> Run configurations,设置运行参数:

运行,可以输出结果已经有点意思了:

so,我们既没有手工的去修改MyTest类的每个方法,也不需要重新打包部署应用代码。只要在启动应用时加上-javaagent参数,利用java instrumentation来修改class字节码,从而达到AOP的效果。

以后要是在多加一个monitor.agent.MyTest.sayHello3需要监测耗时,也不需要修改应用代码,只要在MonitorTransformer的methodList中多加一个方法就可以了:

    static{
        methodList.add("monitor.agent.MyTest.sayHello");
        methodList.add("monitor.agent.MyTest.sayHello2");
      methodList.add("monitor.agent.MyTest.sayHello3");
    }

如果想搞的更智能一些,methodList可以搞成配置文件配置的,而不用写死在代码中。

下一次就写一下怎么利用java的-D参数用配置文件来配置methodList........

Java Instrument动态修改字节码入门-添加方法耗时监控

平常在统计方法执行的耗时时长时,一般都是在方法的开头和结尾通过System.currentTimeMillis()拿到时间,然后做差值,计算耗时,这样不得不在每个方法中都重复这样的操作,现在使用Ins...
  • tterminator
  • tterminator
  • 2017年01月12日 20:48
  • 2076

java.lang.instrument 学习(一)

转自:http://jiangbo.me/blog/2012/02/21/java-lang-instrument/ Instrumentation介绍: java Instrumentation指的...
  • ykdsg
  • ykdsg
  • 2013年09月27日 09:37
  • 18595

JAVA Instrumentation

什么是Instrumentation? java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于...
  • productshop
  • productshop
  • 2016年02月02日 14:45
  • 3840

Java Instrument

经常看到OpenJPA、Jacoco、cobertura等工具对Java Class进行Instrument操作, Java 从1.5开始也提供了instrument包,那么到底什么是instrume...
  • cloud_ll
  • cloud_ll
  • 2014年01月25日 14:39
  • 1800

Java Instrumentation笔记

  • 2011年12月20日 20:35
  • 472KB
  • 下载

java instrument 初探

原文地址:http://blog.csdn.net/pwlazy/article/details/5109742 java在1.5引入java.lang.instrument,你可以...
  • Dancen
  • Dancen
  • 2013年07月16日 17:01
  • 1149

Java 5 特性 Instrumentation 实践

简介 不使用instrumentation 来测量函数运行时间的传统方法是:在函数调用之前记录当前系统时间,在函数调用完成之后再次记录当前系统时间(为了简化描述,本文不考虑虚拟机进程映射到本地操...
  • songshuaiyang
  • songshuaiyang
  • 2016年02月24日 17:23
  • 555

AOP实践:java.lang.instrument的使用

背景 想调用ASM API (用于字节码处理的开源API)对字节码进行处理,目标是实现对java程序运行时各种对象的动态跟踪,并进一步分析各个对象之间的关系(研究前提是目前的UML锁阐释的whole-...
  • BIAOBIAOqi
  • BIAOBIAOqi
  • 2011年11月22日 11:17
  • 9971

Instrumentation.java

/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Ve...
  • w1857518575
  • w1857518575
  • 2013年12月27日 09:22
  • 1010

关于android instrumentation的理解、使用

一般的应用不太会用到instrumentation,所以网上对其介绍也比较少。 但因其强大的跟踪application及activity生命周期的功能,在一些android 应用测试框架中被做为基类使...
  • shan987
  • shan987
  • 2016年01月13日 16:09
  • 10486
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:构建自己的监测器【3】-instrumentation
举报原因:
原因补充:

(最多只允许输入30个字)