天天看點

建構自己的監測器【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........