Java Agent入門實戰(一)-Instrumentation介紹與使用

學會Java Agent你能作什麼?html

  • 自動添加getter/setter方法的工具lombok就使用了這一技術
  • btrace、Arthas和housemd等動態診斷工具也是用了instrument技術
  • Intellij idea 的 HotSwap、Jrebel 等也是該技術的實現之一
  • pinpoint、skywalking、newrelic、聽雲的 APM 產品等都基於 Instrumentation 實現

Java Instrumentation 簡介

來源:網易有道詞典-專業釋義-計算機科學技術
Instrumentation: 在計算機科學技術中的英文釋義是插樁、植入。
instrument: 儀器(儀器是指用以檢出、測量、觀察、計算各物理量、物質成分、物性參數等的器具或設備。)java

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


java.lang.instrument 包被賦予了更強大的功能:啓動後的 監測、本地代碼(native code)監測,以及動態改變 classpath 等等。這些改變,意味着 Java 具備了更強的動態控制與解釋能力,它使得 Java 語言變得更加靈活多變。apache


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


另外,對 native method 的 Instrumentation 也是 Java SE 6 的一個嶄新的功能,這使之前沒法完成的功能,能夠在 Java SE 6 中經過對 native method 接口的 Instrumentation,經過一個或者一系列的 prefix 添加而得以完成。api

Java SE 6 裏的 Instrumentation 也增長了動態添加 class path 的功能。這些新的功能,都使得 java.lang.instrument 包的功能更加豐富,使得 Java 語言更增強大。數組


java.lang.instrument包的具體實現,依賴於 JVMTI(Java Virtual Machine Tool Interface)這是一套由 Java 虛擬機提供的,爲 JVM 相關工具提供的本地編程接口集合。JVMTI 是從 Java SE 5 開始引入,JVMTI 提供了一套「代理」程序機制,能夠支持第三方工具程序以代理的方式鏈接和訪問 JVM,並利用 JVMTI 提供的編程接口,完成不少跟 JVM 相關的功能。事實上,java.lang.instrument 包的實現,也就是基於這種機制的bash

Instrumentation 的實現當中,存在一個 JVMTI 的代理程序,經過調用 JVMTI 當中與Java 類相關的函數,來完成對 Java 類的動態操做。微信

除了 Instrumentation 功能外,JVMTI 還在虛擬機內存管理,線程控制,方法和變量操做等等方面提供了大量可用的函數。關於 JVMTI 的詳細信息,能夠參考 Java SE 6 JVM TI文檔oracle

Java Instrumentation 的基本用法

在java中如何實現 Instrumentation 呢,簡單來講有如下幾步

  1. 建立一個普通的類,內含靜態方法premain(),這個方法名是java agent內定的方法名,它總會在main函數以前執行

    package cn.jpsite.learning.javaagent01;
    
    import java.lang.instrument.Instrumentation;
    
    public class JpAgent {
        public static void premain(String agentArgs, Instrumentation instrumentation)  {
    
            /*轉換髮生在 premain 函數執行以後,main 函數執行以前,這時每裝載一個類,transform 方法就會執行一次,看看是否須要轉換,
            因此,在 transform(Transformer 類中)方法中,程序用 className.equals("TransClass") 來判斷當前的類是否須要轉換。*/
            // 方式一:
            System.out.println("我是兩個參數的 Java Agent premain");
        }
        public static void premain(String agentArgs){
            System.out.println("我是一個參數的 Java Agent premain");
        }
    }
    
    複製代碼

    如上有2個premain()方法,當1個參數和2個參數的premain()的方法同事存在的時候,premain(String agentArgs)將被忽略

  2. 在resource目錄下新建META-INF/MANIFEST.MF文件,內容以下:

    Manifest-Version: 1.0
    Premain-Class: cn.jpsite.learning.javaagent01.JpAgent
    Can-Redefine-Classes: true
    Can-Retransform-Classes: true
    複製代碼

    以上內容能夠經過Maven的org.apache.maven.pluginsmaven-assembly-plugin插件配合完成,在mvn install的時候生成MANIFEST.MF文件內容

  3. 目錄結構是這樣的

4. 對當前工程執行 mvn clean install 打包,生成了 jpAgent.jar文件 5. 新建一個maven工程example01,內含Main.java、Dog.java,並最終打包成 example01-1.0-SNAPSHOT.jar public class Dog { public String say() { return "dog"; } } public class Main { public static void main(String[] args) { System.out.println("夜太黑"); System.out.println("----"+new Dog().say()); } } 6. 執行 jpAgent.jar 須要經過 -javaagent 參數來指定Java代理包, > -javaagent 這個參數的個數是不限的,能夠指定多個,會按指定的前後順序執行,執行完各個 agent 後,纔會執行主程序的 main 方法。

```
// 爲了執行方便,把jar文件放在同一層級目錄下
java -javaagent:jpAgent.jar -cp example01-1.0-SNAPSHOT.jar  cn.jpsite.learning.Main
```
其中`example01-1.0-SNAPSHOT.jar`的`Main()`方法只是簡單的輸出2行內容,經過agent代理後多輸出了一段內容。
複製代碼

addTransformer方法

對 Java 類文件的操做,能夠理解爲對一個 byte 數組的操做(將類文件的二進制字節流讀入一個 byte 數組)。開發者能夠在 interface ClassFileTransformertransform 方法(經過 classfileBuffer 參數)中獲得,操做並最終返回一個類的定義(一個 byte 數組),接下來演示下transform 轉換類的用法,採用簡單的類文件替換方式。

  1. 新建JpClassFileTransformerDemo.java 實現 ClassFileTransformer接口,getBytesFromFile() 方法根據文件名讀入二進制字符流,而 ClassFileTransformer 當中的 transform 方法則完成了類定義的替換。
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.IllegalClassFormatException;
    import java.security.ProtectionDomain;
    
    public class JpClassFileTransformerDemo implements ClassFileTransformer {
    
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("loader className: " + className);
            if (!className.equalsIgnoreCase("cn/jpsite/learning/Dog")) {
                return null;
            }
    
            return getBytesFromFile("D:\\learning\\Dog.class");
        }
    
        public static byte[] getBytesFromFile(String fileName) {
            File file = new File(fileName);
            try (InputStream is = new FileInputStream(file)) {
                // precondition
    
                long length = file.length();
                byte[] bytes = new byte[(int) length];
    
                // Read in the bytes
                int offset = 0;
                int numRead = 0;
                while (offset < bytes.length
                        && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
                    offset += numRead;
                }
    
                if (offset < bytes.length) {
                    throw new IOException("Could not completely read file "
                            + file.getName());
                }
                is.close();
                return bytes;
            } catch (Exception e) {
                System.out.println("error occurs in _ClassTransformer!"
                        + e.getClass().getName());
                return null;
            }
        }
    }
    複製代碼
  2. JpAgent.java 增長instrumentation.addTransformer(new JpClassFileTransformerDemo());內容以下,從新打包
    public class JpAgent {
        public static void premain(String agentArgs, Instrumentation instrumentation)  {
    
            /*轉換髮生在 premain 函數執行以後,main 函數執行以前,這時每裝載一個類,transform 方法就會執行一次,看看是否須要轉換,
            因此,在 transform(Transformer 類中)方法中,程序用 className.equals("TransClass") 來判斷當前的類是否須要轉換。*/
            // 方式一:
           instrumentation.addTransformer(new JpClassFileTransformerDemo());
            System.out.println("我是兩個參數的 Java Agent premain");
        }
        public static void premain(String agentArgs){
            System.out.println("我是一個參數的 Java Agent premain");
        }
    }
    複製代碼
  3. 此時咱們去修改以前的example01工程中的Dog.java, 把say()方法返回的字符串"dog"改成"cat", 而後編譯成.class文件,getBytesFromFile("D:\\learning\\Dog.class")就是讀取修改後的class文件。
  4. 執行java -javaagent:jpAgent.jar -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.Main 查看結果以下:

轉換髮生在 premain 函數執行以後,main 函數執行完成以前,這時每裝載一個類,transform 方法就會執行一次,看看是否須要轉換,因此,在 transform方法中,這裏用了 className.equals("cn/jpsite/learning/Dog") 來判斷當前的類是否須要轉換。

除了用 addTransformer 的方式,Instrumentation 當中還有另一個方法「redefineClasses」來實現 premain 當中指定的轉換。用法相似,以下:

ClassDefinition def = new ClassDefinition(Dog.class, Objects.requireNonNull(JpClassFileTransformerDemo
                .getBytesFromFile("D:\\learning\\Dog.class")));
instrumentation.redefineClasses(new ClassDefinition[] { def });
複製代碼

擴展閱讀

Java虛擬機參數分析平臺
java.lang.instrument api doc
Java SE 6 JVM TI文檔
Java SE 8 doc Java Attach API
JavaAgent源碼分析
Java探針-Java Agent技術-阿里面試題
本身實現一個Native方法的調用
本身實現一個Native方法的調用2

點關注,不迷路

文章每週持續更新,能夠微信搜索「 十分鐘學編程 」第一時間閱讀和催更,若是這個文章寫得還不錯,以爲有點東西的話 ~求點贊👍 求關注❤️ 求分享❤️
各位的支持和承認,就是我創做的最大動力,咱們下篇文章見!

相關文章
相關標籤/搜索