天天看点

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

其实前一节已经看到过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!

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

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,设置运行参数:

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

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

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

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........