Instrument API介紹

1. Instrumentation介紹

JVMTI(JVM Tool Interface)是 Java 虛擬機所提供的 native 編程接口,是 JVMPI(Java Virtual Machine Profiler Interface)和 JVMDI(Java Virtual Machine Debug Interface)的更新版本。JVMTI 提供了一套「代理」程序機制,能夠支持第三方工具程序以代理的方式鏈接和訪問 JVM,並利用 JVMTI 提供的豐富的編程接口,完成不少跟 JVM 相關的功能。html

 Agent 即 JVMTI 的客戶端,它和執行 Java 程序的虛擬機運行在同一個進程上。他們一般由另外一個獨立的進程控制,充當這個獨立進程和當前虛擬機之間的中介,經過調用 JVMTI 提供的接口和虛擬機交互,負責獲取並返回當前虛擬機的狀態或者轉發控制命令。java.lang.instrument 包的實現,也是基於這種機制的。在 Instrumentation 的實現當中,存在一個 JVMTI 的代理程序,經過調用 JVMTI 當中於 Java 類相關的函數來完成Java 類的動態操做。java

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

 在 Java SE6 裏面,最大的改變是運行時的 Instrumentation 成爲可能。在 Java SE 5 中,Instrument 要求在運行前利用命令行參數或者系統參數來設置代理類,在實際的運行之中,虛擬機在初始化之時(在絕大多數的 Java 類庫被載入以前),instrumentation 的設置已經啓動,並在虛擬機中設置了回調函數,檢測特定類的加載狀況,並完成實際工做。可是在實際的不少的狀況下,咱們沒有辦法在虛擬機啓動之時就爲其設定代理,這樣實際上限制了 instrument 的應用。而 Java SE 6 的新特性改變了這種狀況,經過 Java Tool API 中的 attach 方式,咱們能夠很方便地 在運行過程當中動態地設置加載代理類,以達到 instrumentation 的目的。api

 Instrumentation 的最大做用,就是類定義動態改變和操做。在 Java SE 5 及其後續版本當中,開發者能夠在一個普通 Java 程序(帶有 main 函數的 Java 類)運行時,經過 -javaagent參數指定一個特定的 jar 文件(包含 Instrumentation 代理)來啓動 Instrumentation 的代理程序。數組

2. Transformer

 Transformer是字節碼轉換的接口,Instrumentation是管理Transformer、調度Transformer進行字節碼轉換的門面。 當執行Instrumentation的addTransformer、removeTransformer方法時,最終是調用了TransformerManager的addTransformer、removeTransformer,以此來管理Transformer。微信

 Instrumentation的retransformClasses、redefineClasses是用於通知TransformerManager調度字節碼轉換的。除此以外,在調用ClassLoader.defineClass1()這個native方法用於進行類的定義時,也會通知TransformerManager調度Transformer來進行字節碼轉換。這三個字節碼轉換通知時機分別稱爲:oracle

  • 加載類時(1)
  • 重定義類時(2)
  • 重轉換類時(3)

Transformer能夠分爲兩類:可重轉換的Transformer、不可重轉換的Transformer。任何一個Transformer均可以用於加載類時、重定義類時進行轉換。若是是可重轉換的Transformer,也能夠在重轉換時進行轉換。對於全部的註冊轉換器,在發生類加載時(1)或者重定義類時(2),會觸發轉換器的執行。重轉換類時只有可中轉換的Transformer會觸發。jvm

 當存在多個轉換器時,轉換將由transform調用鏈組成。也就是說,一個transform調用返回的byte數組將成爲下一個調用的輸入。 轉換將按如下順序進行:ide

  • 不可重轉換轉換器
  • 不可重轉換本地(native)轉換器
  • 可重轉換轉換器
  • 可重轉換本地(native)轉換器

一樣,在重轉換時(3),不會調用不可重轉換轉換器,而是重用前一個轉換的結果。對於全部其餘狀況,調用此方法。在每一個這種調用組中,轉換器將按照註冊的順序調用。函數

 ClassFileTransformer接口只有一個方法:

byte[] transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;

其中classfileBuffer字段爲加載的class內容的byte數組,返回結果未待初始化的class內容的byte數組。便可以經過該方法修改原class內容,返回修改後的內容來修改類的行爲。若是不作任何轉換,則要返回null。 若是轉換器拋出異常(未捕獲的異常),後續轉換器仍然將被調用並加載,仍然將嘗試重定義或重轉換。所以,拋出異常與返回 null 的效果相同。

 請參考https://docs.oracle.com/javase/7/docs/api/java/lang/instrument/ClassFileTransformer.html

2.1. redefineClasses

 使用提供的類文件從新定義提供的一組類。

 該方法用於替換類的定義,而不引用現有的類文件字節,就像從源頭進行從新編譯以進行修復和繼續調試時同樣。 在現有的類文件字節要轉換的地方應該使用retransformClasses。

 該方法對一組class進行操做,以便同時容許多個相互依賴的類的更改,如A類的從新定義可能須要從新定義B類。

 若是從新定義的方法具備活動堆棧幀,則這些活動幀將繼續運行原始方法的字節碼。 從新定義的方法將作用於新的調用。

 該方法不會致使任何初始化,除了在常規JVM語義下會發生。 換句話說,從新定義一個類並不會致使它的初始化器被運行。 靜態變量的值將保持在調用以前。從新定義的類的實例不受影響。

 從新定義可能會改變方法體,常量池和屬性。 重定義不能添加,刪除或重命名字段或方法,更改方法的簽名或更改繼承。 這些限制可能在未來的版本中解除。 類文件字節不會被檢查,驗證和安裝,直到應用轉換爲止,若是結果字節錯誤,則此方法將拋出異常。若是此方法拋出異常,則不會從新定義任何類。

 該方法的定義以下:

void redefineClasses(ClassDefinition... definitions)  throws ClassNotFoundException,UnmodifiableClassException

public ClassDefinition(Class<?> theClass,byte[]  theClassFile) {
    if (theClass == null || theClassFile == null) {
        throw new NullPointerException();
    }
    mClass      = theClass;
    mClassFile  = theClassFile;
}

如上所述,該方法須要指定須要替換的Class以及提供自定義類文件的字節碼內容,請參考https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html#redefineClasses-java.lang.instrument.ClassDefinition...-

2.2. retransformClasses

 從新轉換提供的一組類。

 該方法主要做用於已經加載過的class。能夠用ClassFileTransformer對初始化過或者redifine過的class進行從新處理, 不管之前是否發生轉換,此函數都將從新運行轉換過程。 轉換過程遵循如下步驟:

  • 從初始類文件字節開始

  • 對於將canRetransform設置爲false的每一個轉換器,在上一個類加載或重定義期間由轉換器返回的字節將被從新用做當前轉換的輸出,至關於當前轉換器不生效

  • 對於將canRetransform設置爲true的每一個轉換器,將會在當前調用該轉換器

  • 轉換後的類文件字節做爲類的新定義安裝

 該方法對一組class進行操做,以便同時容許多個相互依賴的類的更改,如A類的從新定義可能須要從新定義B類。

 若是從新定義的方法具備活動堆棧幀,則這些活動幀將繼續運行原始方法的字節碼。 從新定義的方法將作用於新的調用。

 該方法不會致使任何初始化,除了在常規JVM語義下會發生。 換句話說,從新定義一個類並不會致使它的初始化器被運行。 靜態變量的值將保持在調用以前。從新定義的類的實例不受影響

 從新轉換可能會改變方法體,常量池和屬性。 從新傳輸不能添加,刪除或重命名字段或方法,更改方法的簽名或更改繼承。 這些限制可能在未來的版本中解除。 類文件字節不會被檢查,驗證和安裝,直到應用轉換爲止,若是結果字節錯誤,則此方法將拋出異常。若是此方法拋出異常,則不會從新建立任何類。

 該方法的內容以下:

void retransformClasses(Class<?>... classes)  throws UnmodifiableClassException

該方法要傳入須要進行重轉換的類,請參考https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html#retransformClasses-java.lang.Class...-

 須要注意的是,爲Agent開啓redefine功能須要在javaagent的MANIFEST.MF裏設置Can-Redefine-Classes:true。爲Agent開啓retransform功能須要在javaagent的MANIFEST.MF文件裏定義了Can-Retransform-Classes:true。

 介紹完了相關的內容,下面介紹如何實現。

3. JDK5 premain方式

 使用premain方式進行處理須要以下幾個步驟

3.1. 提供一個公共的靜態方法premain:
//<1>
public static void premain(String agentArgs, Instrumentation inst);
//<2>
public static void premain(String agentArgs);

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

 正如這個方法名,該方法會先於main方法被執行。通常會在這個方法中建立一個代理對象,經過參數 inst 的 addTransformer() 方法,將建立的代理對象再傳遞給虛擬機。agentArgs 是 premain 函數獲得的程序參數,隨同 「– javaagent」一塊兒傳入。與 main 函數不一樣的是,這個參數是一個字符串而不是一個字符串數組,若是程序參數有多個,程序將自行解析這個字符串。

3.2. 提供一個或者多個ClassFileTransformer實現類

 上面說過,會在premain中調用inst的addTransformer()方法,該方法的入參就是ClassFileTransformer對象。

 對於字節碼的修改在上一節已經介紹過了,能夠有多種方式。這裏使用上一節的例子,對CoreActionImpl類進行修改以達到AOP的效果。代碼以下:

public class PreMainProxyAction implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
        byte[] classfileBuffer) throws IllegalClassFormatException {
        if (!className.equals("demo/CoreActionImpl")) {
            return classfileBuffer;
        }
        ASMProxyAction proxyAction = new ASMProxyAction();
        byte[] bytes = proxyAction.aop(classfileBuffer);
        //這裏能夠將bytes寫入到文件,輸出處理後的calss內容
        return bytes;
    }


    public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
        inst.addTransformer(new PreMainProxyAction());
    }
}

其中ASMProxyAction的內容爲上一節ASM例子的內容,只是從新組織了代碼以進行復用,核心內容以下:

public byte[] aop(byte[] bytes) {
        ClassReader cr = new ClassReader(bytes);
        return aop(cr);
    }

    public byte[] aop(ClassReader cr) {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        cr.accept(new ClassVisitor(Opcodes.ASM6, cw) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                if (!"say".equals(name)) {
                    return mv;
                }
                MethodVisitor aopMV = new MethodVisitor(super.api, mv) {
                    @Override
                    public void visitCode() {
                        super.visitCode();
                        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                        mv.visitLdcInsn("before core action");
                        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                    }

                    @Override
                    public void visitInsn(int opcode) {
                        if (Opcodes.RETURN == opcode) {
                            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                            mv.visitLdcInsn("after core action");
                            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                        }
                        super.visitInsn(opcode);
                    }
                };
                return aopMV;
            }
        }, ClassReader.SKIP_DEBUG);
        return cw.toByteArray();
    }
3.3. jar 文件打包

 將這個 Java 類打包成一個 jar 文件,並在其中的 manifest 屬性當中加入」 Premain-Class」來指定步驟3.1當中編寫的那個帶有 premain 的 Java 類。

3.4. 運行

 用以下方式運行帶有 Instrumentation 的 Java 程序:

java -javaagent:jar 文件的位置 [= 傳入 premain 的參數 ]

 按照上面示例代碼註釋的內容,輸出處理事後的字節碼以下:

public class demo/CoreActionImpl implements demo/Action  {


  // access flags 0x1
  public <init>()V
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public say()V
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "before core action"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "hello world"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "after core action"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 1
}

其實就是上節ASM處理事後的結果,實現了AOP。即Instrument提供的premain方法,提供了一個入口,能夠在main方法執行前,修改原class的內容,增長自定義邏輯。

 須要指出的是,addTransformer 方法並無指明要轉換哪一個類,於是在 transform(Transformer 類中)方法中,程序須要本身判斷當前的類是否須要轉換,如上面的示例。

4. JDK6 agentmain方式

 JDK5提供的premain方式只能在應用啓動前對class進行處理,JDK6 在此基礎上進行了改進,開發者能夠在 main 函數開始執行之後,再啓動本身的處理程序。

 使用agentmain方式進行處理須要以下幾個步驟

4.1. 提供一個公共的靜態方法agentmain:
//<1>
public static void agentmain (String agentArgs, Instrumentation inst);
//<2>
public static void agentmain (String agentArgs);

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

4.2. 提供一個或者多個ClassFileTransformer實現類

 方法同3.2一致。不一樣的是,因爲agentmain方式是在虛擬機啓動後進行處理的,這時候目標class可能已經被加載過了,須要從新對目標class進行處理,根據上面的介紹,能夠調用retransformClasses方法對類進行從新處理。

4.3. jar 文件打包

 將這個 Java 類打包成一個 jar 文件,並在其中的 manifest 屬性當中加入」 Agent-Class」來指定步驟4.1當中編寫的那個帶有 agentmain 的 Java 類。

4.4. 加載jar包

 同premain不一致的是,agentmain的接入須要外部應用顯示觸發。Java SE 6 當中提供的 Attach API,用來向目標 JVM attach代理工具程序。須要注意的是,Attach API 不是 Java 的標準 API,而是 Sun 公司提供的一套擴展 API。

 Attach API 很簡單,只有 2 個主要的類,都在 com.sun.tools.attach 包裏面: VirtualMachine 表明一個 Java 虛擬機,也就是程序須要監控的目標虛擬機,提供了 JVM 枚舉,attach 動做和 detach 動做(Attach 動做的相反行爲,從 JVM 上面解除一個代理)等等 ; VirtualMachineDescriptor 則是一個描述虛擬機的容器類,配合 VirtualMachine 類完成各類功能。

 可用以下的方式將一個jar包attach到一個運行的虛擬機上去:

public void start(String processId,String agentArgs, String agentJarPath) throws Exception {
    VirtualMachine virtualMachine = null;
    try {
        virtualMachine = VirtualMachine.attach(processId);
        virtualMachine.loadAgent(agentJarPath,agentArgs);
    } finally {
        if (virtualMachine != null) {
            virtualMachine.detach();
        }
    }
}

其中須要的參數爲:

  • processId:目標應用pid
  • agentArgs:傳給agentmain的參數
  • agentJarPath:待加載的jar包

更多原創內容請搜索微信公衆號:啊駝(doubaotaizi)

相關文章
相關標籤/搜索