Java Instrumentation


 


說明:本博文是博主學習 Instrumentation 歷程的總結,整合了學習過程中參考的關於Instrumentation 的教程,並加入博主自己的見解和實例。

參考鏈接:

Instrumentation 新功能

JDK源碼-java.lang.instrument-第一部分-源碼學習


Instrumentation 簡介

  利用 Java 代碼,即 java.lang.instrument 做動態 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能從本地代碼中解放出來,使之可以用 Java 代碼的方式解決問題。使用 Instrumentation,開發者可以構建一個獨立於應用程序的代理程序(Agent),用來監測和協助運行在 JVM 上的程序,甚至能夠替換和修改某些類的定義。有了這樣的功能,開發者就可以實現更為靈活的運行時虛擬機監控和 Java 類操作了,這樣的特性實際上提供了一種虛擬機級別支持的 AOP 實現方式,使得開發者無需對 JDK 做任何升級和改動,就可以實現某些 AOP 的功能了。

  在 Java SE 6 里面,instrumentation 包被賦予了更強大的功能:啟動后的 instrument、本地代碼(native code)instrument,以及動態改變 classpath 等等。這些改變,意味着 Java 具有了更強的動態控制、解釋能力,它使得 Java 語言變得更加靈活多變。

  “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 Instrumentation,首先應該了解 JVMTI 的基本知識,此處推薦 IBM 的一篇文章 JVMTI 和 Agent 實現 ,文章寫得相當貼切。

Instrumentation 源碼簡介

java.lang.instrument包結構



代理監控JVM運行的JAVA程序,對字節碼修改


ClassFileTransformer(接口)

//轉換類文件的代理接口
public interface ClassFileTransformer { 
    //protectionDomain - 要定義或重定義的類的保護域
    //classfileBuffer - 類文件格式的輸入字節緩沖區(不得修改)
    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}

###Instrumentation(接口)

Java SE 6新特性,
獲取 Instrumentation 接口的實例有兩種方式:

1. 當 JVM 以指示一個代理類的方式啟動時,將傳遞給代理類的 premain 方法一個 Instrumentation 實例。 

2. 當 JVM 提供某種機制在 JVM 啟動之后某一時刻啟動代理時,將傳遞給代理代碼的 agentmain 方法一個 Instrumentation 實例

源碼解釋:

//提供檢測 Java 編程語言代碼所需的服務。檢測是向方法中添加字節碼
//以搜集各種工具所使用的數據
public interface Instrumentation {
    /**
     *  注冊提供的轉換器。
     */
    void
    addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    /**
     * 注冊提供的轉換器。
     */
    void
    addTransformer(ClassFileTransformer transformer);

    /**
     * 注銷提供的轉換器
     */
    boolean
    removeTransformer(ClassFileTransformer transformer);

    /**
     * 返回當前 JVM 配置是否支持類的重轉換。
     */
    boolean
    isRetransformClassesSupported();

    /**
     *  重轉換提供的類集
     */
    void
    retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    /**
     * 返回當前 JVM 配置是否支持類的重定義
     */
    boolean
    isRedefineClassesSupported();

    /**
     * 使用提供的類文件重定義提供的類集
     */
    void
    redefineClasses(ClassDefinition... definitions)
        throws  ClassNotFoundException, UnmodifiableClassException;


    /**
     * 確定一個類是否可以被 retransformation 或 redefinition 修改。
     */
    boolean
    isModifiableClass(Class<?> theClass);

    /**
     * 返回 JVM 當前加載的所有類的數組
     */
    Class[]
    getAllLoadedClasses();

    /**
     * 返回所有初始化加載器是 loader 的類的數組。
     */
    Class[]
    getInitiatedClasses(ClassLoader loader);

    /**
     * 返回指定對象使用的特定於實現的近似存儲量。
     */
    long
    getObjectSize(Object objectToSize);


    /**
     * 指定 JAR 文件,檢測類由引導類加載器定義
     */
    void
    appendToBootstrapClassLoaderSearch(JarFile  jarfile);

    /**
     * 指定 JAR 文件,檢測類由系統類加載器定義。
     */
    void
    appendToSystemClassLoaderSearch(JarFile jarfile);

    /**
     * 返回當前 JVM 配置是否支持設置本機方法前綴。
     */
    boolean
    isNativeMethodPrefixSupported();

    /**
     * 通過允許重試,將前綴應用到名稱,此方法修改本機方法解析的失敗處理
     */
    void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}

 


IllegalClassFormatException(異常)

ClassFileTransformer.transform 的實現拋出該異常。拋出此異常的原因或者由於初始類文件字節無效,或者由於以前應用的轉換損壞了字節


UnmodifiableClassException(異常)

在無法修改指定類之一時,由 Instrumentation.redefineClasses 的實現拋出此異常。


ClassDefinition

public final class ClassDefinition { 
/** 
* 自身class 
*/ 
private final Class mClass;
/**
 *  本地class文件
 */
private final   byte[]  mClassFile;

/**
 * 使用提供的類和類文件字節創建一個新的 ClassDefinition 綁定
 */
public
ClassDefinition(    Class<?> theClass,
                    byte[]  theClassFile) {
    if (theClass == null || theClassFile == null) {
        throw new NullPointerException();
    }
    mClass      = theClass;
    mClassFile  = theClassFile;
}

/**
 * 返回該類。
 */
public Class<?>
getDefinitionClass() {
    return mClass;
}

/**
 * 返回包含新的類文件的 byte 數組。
 */
public byte[]
getDefinitionClassFile() {
    return mClassFile;
}
}

 


 

Instrumentation 基本功能和用法

  功能:通過代理,在main函數運行前或后動態的改變類的定義和其他處理操作

  具體包括:premain ,agentmain,動態改變 classpath,本地方法prefix等等,下面進行詳細介紹:

1. premain

開發者可以讓 Instrumentation 代理在 main 函數運行前執行:

1.1 包中的類

1.2 定義TransClass 類

package wqz.zoom.test;

public class TransClass {
    public void sayHello() {
        System.out.println("Hello!!!");
    }

在此類做加載時,進行處理。

1.3 定義 Transformer 類

 

package wqz.zoom.test;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;

public class Transformer implements ClassFileTransformer{

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
        ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        // TODO Auto-generated method stub
        System.out.println("transform()");
        if(className.equals("wqz/zoom/test/TransClass")) {
            ClassPool classPool = ClassPool.getDefault();
            
            try {
                CtClass class1 = classPool.get(className.replaceAll("/", "."));
                CtMethod ctMethod = class1.getDeclaredMethod("sayHello");
                if(!ctMethod.isEmpty()) {
                    ctMethod.insertBefore("System.out.println(\"before hello!!!\");");
                }
                return class1.toBytecode();
            } catch (NotFoundException | CannotCompileException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return null;
    }
}

  這個類實現了 ClassFileTransformer 接口。ClassFileTransformer 當中規定的 transform 方法則完成類定義的替換轉換。

  此處使用 javassist 技術對字節碼進行處理,對TransClass類的sayHello方法體前插入一行代碼

ctMethod.insertBefore("System.out.println(\"before hello!!!\");");

  javassist 是功能十分強大的字節碼處理工具,其使用參考 javassist使用簡介

  需要將javassist的jar包導入到形目中。

Premain 代碼中:

 

inst.addTransformer(new Transformer());

 

  addTransformer 方法並沒有指明要轉換哪個類。轉換發生在 premain 函數執行之后,main 函數執行之前,這時每裝載一個類,transform 方法就會執行一次,看看是否需要轉換,所以,在 transform(Transformer 類中)方法中,程序用 ClassName.equals("wqz/zoom/test/TransClass") 來判斷當前的類是否需要轉換。此處需要注意className的形式。

 

1.4 定義 Premain 類(類名稱可自定義)

編寫一個 Java 類,包含如下兩個方法當中的任何一個

public static void premain(String agentArgs, Instrumentation inst);  //[1]
public static void premain(String agentArgs); //[2]

其中,[1] 的優先級比 [2] 高,將會被優先執行([1] 和 [2] 同時存在時,[2] 被忽略)。

在這個 premain 函數中,開發者可以進行對類的各種操作。

agentArgs 是 premain 函數得到的程序參數,隨同 “– javaagent”一起傳入。與 main 函數不同的是,這個參數是一個字符串而不是一個字符串數組,如果程序參數有多個,程序將自行解析這個字符串。

Inst 是一個 java.lang.instrument.Instrumentation 的實例,由 JVM 自動傳入。java.lang.instrument.Instrumentation 是 instrument 包中定義的一個接口,也是這個包的核心部分,集中了其中幾乎所有的功能方法,例如類定義的轉換和操作等等。

 

package wqz.zoom.test;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class Premain {
    public static void premain(String agentArgs,Instrumentation inst) throws ClassNotFoundException,UnmodifiableClassException{
        inst.addTransformer(new Transformer());
        System.out.println("premain ok!");
    }
}

1.4 定義測試類 TestMainInJar

package wqz.zoom.test;

public class TestMainInJar {
    public static void main(String[] args) {
        System.out.println("TestMainInJar main()");
        new TransClass().sayHello();
    }
}

premain函數會在此main函數執行前執行。

1.5 instrumentation 打包運行

instrumentation應用需要打包執行(eclipse/cmd 兩種打 jar 包的方式,自行百度)

將wqz.zoom.test 包 打包成 test.jar, 並將javassist的jar包放於同一目錄

使用壓縮工具打開test.jar,編輯 MANIFEST.MF並保存

添加如下配置:

Class-Path: javassist-3.15.0-GA.jar
Premain-Class: wqz.zoom.test.Premain

運行jar:

java -javaagent:test.jar -cp test.jar wqz.zoom.test.TestMainInJar

運行結果:


重定義

redefineClasses(ClassDefinition... definitions) 用法

public class Premain { 
   public static void premain(String agentArgs, Instrumentation inst) 
           throws ClassNotFoundException, UnmodifiableClassException { 
       ClassDefinition def = new ClassDefinition(TransClass.class,bytes);//bytes 為TranClass的字節碼數組(修改前或修改后) 
       inst.redefineClasses(new ClassDefinition[] { def }); 
       System.out.println("success"); 
   } 
}

 

ClassDefinition def = new ClassDefinition(TransClass.class,bytes);//bytes 為TranClass的字節碼數組(修改前或修改后)

此字節數組參數可與 javassist 配合使用,傳入javassist修改后的數組。

例:如果要對一個已經加載的類進行重定義,從原理上講需要提供已經修改過的字節碼,這個修改過的字節碼就來源於bytes(此 bytes 可以由javassist獲取原ctclass,在此基礎上進行編輯與修改,獲取修改后的bytes,傳入ClassDefinition)。

【注意事項】詳細閱讀 jdk文檔!!!

 1. 僅當 Can-Retransform-Classes 清單屬性在代理 JAR 文件中設置為 true(如包規范所述),且 JVM 支持此性能時,才支持重轉換。

inst.isRedefineClassesSupported();//返回當前 JVM 配置是否支持類的重定義

2. 如果重定義的方法有活動的堆棧幀,那么這些活動的幀將繼續運行原方法的字節碼。將在新的調用上使用此重定義的方法。

3. 重定義可能會更改方法體、常量池和屬性。重定義不得添加、移除、重命名字段或方法

 


2. agentmain 

虛擬機啟動后的動態 instrument,開發者可以在 main 函數開始執行以后,再啟動自己的 Instrumentation 程序,對應premain的同樣有方法:

public static void agentmain (String agentArgs, Instrumentation inst);         // [1] 
public static void agentmain (String agentArgs);            //[2]

用法和premain用法基本相同,jar清單文件MANIFEST.MF 區別Premain-Class 變為  Agent-Class

常與 retransformClasses()方法配合使用。使用前認真閱讀就jdk文檔

3. 本地方法的 instrument

  在 Java SE 6 中,新的 Native Instrumentation 提出了一個新的 native code 的解析方式,作為原有的 native method 的解析方式的一個補充,來很好地解決了一些問題。這就是在新版本的 java.lang.instrument 包里,我們擁有了對 native 代碼的 instrument 方式 —— 設置 prefix

  博主在應用此種方式給更改本地方法前綴時走了不少彎路,如今仍沒有搞明白(T-T),但並不是無法可取,博主發現以下實現方式:

使用重定義技術實現(使用javassist重定義本地方法,調用代理類的本地方法,在代理類的本地方法中回調需要的本地方法)!!!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM