Java javaagent 使用

在 Java SE 5 以後,使用 Instrumentation,使得開發者可以構建一個獨立於應用程序的代理程序(Agent),用來監測和協助運行在 JVM 上的程序,甚至能夠替換和修改某些類的定義。有了這樣的功能,開發者就可以實現更爲靈活的運行時虛擬機監控和 Java 類操作了,這樣的特性實際上提供了一種虛擬機級別支持的 AOP 實現方式,使得開發者無需對 JDK 做任何升級和改動,就可以實現某些 AOP 的功能了。

在 Java SE 5 中,利用 java.lang.instrument 做靜態 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能從本地代碼中解放出來,使之可以用 Java 代碼的方式解決問題。

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

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

Instrumentation 的最大作用,就是類定義動態改變和操作。java.lang.instrument 包的實現,是基於JVMTI機制的:在 Instrumentation 的實現當中,存在一個 JVMTI 的代理程序,通過調用 JVMTI 當中 Java 類相關的函數來完成Java 類的動態操作。除開 Instrumentation 功能外,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 相關的功能。

1. 使用

Agent分爲兩種,一種是在主程序之前運行的Agent,一種是在主程序之後運行的Agent(前者的升級版,1.6以後提供)。

  1. 在一個普通 Java 程序(帶有 main 函數的 Java 類)運行時,通過 -javaagent 參數指定一個特定的 jar 文件(包含 Instrumentation 代理)來啓動 Instrumentation 的代理程序。
  2. 在一個普通 Java 程序(帶有 main 函數的 Java 類)運行時,通過 Java Tool API 中的 attach 方式指定進程id和特定jar包地址,啓動 Instrumentation 的代理程序。

這裏測試使用 javaagent 替換類。

1.1 JVM啓動前靜態 Instrument

定義一個User類,getName()返回admin。

public class User {
    public String getName() {
        return "admin";
    }
}

主程序,這裏的程序就是我們要代理的程序。創建User對象,調用getName()方法。

public class Main {
    public static void main(String[] args) {
        System.out.println("main start");
        System.out.println("main args :" + Arrays.toString(args));
        System.out.println("new User().getName() :" + new User().getName());
        System.out.println("main end");
    }
}

將User類的getName()方法返回值改爲user,使用javac編譯後,重命名編譯文件User.class爲User.class.2,再將User類中的getName()方法返回值改回admin。

實現 ClassFileTransformer 接口,transform 方法則完成了類定義的替換。

public class UserTransformer implements ClassFileTransformer {

	// 待替換的類文件名
    private static final String USER_CLASS_2 = "User.class.2";

    @Override
    public byte[] transform(ClassLoader loader, 
    						String className, 
    						Class<?> classBeingRedefined, 
    						ProtectionDomain protectionDomain,
    						byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println("className :" + className);
        if (!className.contains("User")) {
            return null;
        }
        return getBytesFromFile(USER_CLASS_2);
    }

    /**
     * 根據文件名讀入二進制字符流
     * @param fileName
     * @return
     */
    private byte[] getBytesFromFile(String fileName) {
        File file = new File(fileName);
        try (
                InputStream inputStream = new FileInputStream(file);
                ){
            long length = file.length();
            byte[] bytes = new byte[(int) length];

            int offset = 0;
            int numRead = 0;
            while (offset < bytes.length && 
            	(numRead = inputStream.read(bytes, offset, bytes.length - offset)) >= 0) {
                offset += numRead;
            }

            if (offset < bytes.length) {
                throw new IOException("無法完全讀取文件 " + file.getName());
            }
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

premain方法,代表着他將在主程序的main方法之前運行,agentArgs代表傳遞過來的參數,inst則是agent技術主要使用的API。

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

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

public class Premain {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain start");
        System.out.println("premain args :" + agentArgs);
        // 在類加載之前,重新定義 Class 文件
        inst.addTransformer(new UserTransformer());
        System.out.println("premain end");
    }
}

META-INF/MAINIFEST.MF 文件用於描述Jar包的信息,例如指定入口函數等。
需要添加Premain-Class屬性,指定帶有 premain 方法類的全路徑,然後將agent類打成Jar包。

<plugin>
  <artifactId>maven-jar-plugin</artifactId>
  <version>3.0.2</version>
  <configuration>
    <archive>
      <manifest>
        <addClasspath>true</addClasspath>
      </manifest>
      <manifestEntries>
        <Main-Class>
          com.shpun.Main
        </Main-Class>
        <!--main之前-->
        <Premain-Class>
          com.shpun.Premain
        </Premain-Class>
      </manifestEntries>
    </archive>
  </configuration>
</plugin>

測試

未代理,打印admin。未代理
將 User.class.2 和 Jar 包放在同個目錄下測試。

代理後,先執行 premain 方法,然後加載類,每次都經過 transform() 方法。加載完後執行 main 方法,需要加載 User 類,經過transform() 方法,替換User,打印user,代理成功。
代理1
代理2

1.2 JVM啓動後動態 Instrument

User.class.2,User 類,UserTransformer 類和上面一樣。

修改 Main 方法,這裏判斷 getName() 的值,如果是user才退出。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main start");
        System.out.println("main args :" + Arrays.toString(args));

        while (true) {
            Thread.sleep(1000);
            String name = new User().getName();
            System.out.println("new User().getName() :" + new User().getName());
            if ("user".equals(name)) {
                break;
            }
        }
        System.out.println("main end");
    }
}

agentmain 方法在 main 函數開始運行之後再運行。

public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst)  throws Exception {
        System.out.println("agentmain start");
        System.out.println("agentmain args :" + agentArgs);
        // 增加一個 Class 文件的轉換器,轉換器用於改變 Class 二進制流的數據,
        // 參數 canRetransform 設置是否允許重新轉換,爲true才能在運行時替換。
        inst.addTransformer(new UserTransformer(), true);
        // 在類加載之後,重新定義 Class。對於已經加載過的類,可以執行retransformClasses來重新觸發這個Transformer的攔截。
        inst.retransformClasses(User.class);
        System.out.println("agentmain end");
    }
}

META-INF/MAINIFEST.MF 需要添加 Agent-Class 和 Can-Retransform-Classes 屬性。

<plugin>
  <artifactId>maven-jar-plugin</artifactId>
  <version>3.0.2</version>
  <configuration>
    <archive>
      <manifest>
        <addClasspath>true</addClasspath>
      </manifest>
      <manifestEntries>
        <Main-Class>
          com.shpun.Main
        </Main-Class>
        <!--main之後-->
        <Agent-Class>
          com.shpun.AgentMain
        </Agent-Class>
        <Can-Retransform-Classes>
          true
        </Can-Retransform-Classes>
      </manifestEntries>
    </archive>
  </configuration>
</plugin>

測試

使用 agentmain,需要通過 Attach API 。Attach API 不是 Java 的標準 API,而是 Sun 公司提供的一套擴展 API,用來向目標 JVM ”附着”(Attach)代理工具程序的。有了它,開發者可以方便的監控一個 JVM,運行一個外加的代理程序。Jar 包在JAVA_HOME的lib目錄下。

<dependency>
  <groupId>com.sun</groupId>
  <artifactId>tools</artifactId>
  <version>1.8.0</version>
  <scope>system</scope>
  <systemPath>G:\Java\jdk1.8.0_181\lib\tools.jar</systemPath>
</dependency>

VirtualMachine 代表一個 Java 虛擬機,也就是程序需要監控的目標虛擬機,提供了 JVM 枚舉,Attach 動作和 Detach 動作(Attach 動作的相反行爲,從 JVM 上面解除一個代理)等等 。該類允許我們通過給 attach() 方法傳入一個jvm的pid(進程id),遠程連接到jvm上 。然後我們可以通過 loadAgent() 方法向jvm註冊一個代理程序agent,在該agent的代理程序中會得到一個Instrumentation實例,該實例可以在 Class 加載前改變 Class 的字節碼,也可以在 Class 加載後重新加載。在調用Instrumentation實例的方法時,這些方法會使用ClassFileTransformer 接口中提供的方法進行處理。

public class AttachAgent {
    public static void main (String[] args) throws Exception {
        // 通過jps命令,獲取進程號
        VirtualMachine virtualMachine = VirtualMachine.attach("19056");
        virtualMachine.loadAgent("E:\\IDEA_workspace\\java-agent-test\\java-agent-agentmain\\target\\java-agent-agentmain-1.0-SNAPSHOT.jar", "attach-agent");
        virtualMachine.detach();
    }
}

將 User.class.2 和 Jar 包放在同個目錄下測試。

運行 Jar 包,先打印的是admin。然後執行AttachAgent,發現agentmain方法被執行了,並且在替換了類,打印user。這個表示 agentmain 已經被 Attach API 成功附着到 JVM 上,代理程序生效了。
agentmain述

參考:
Java Agent簡介
javaagent使用指南
JavaAgent技術
☆基於Java Instrument的Agent實現
☆淺談JPDA中JVMTI模塊
☆JVMTI Agent 工作原理及核心源碼分析
☆JVMTI Attach機制與核心源碼分析
"程序包com.sun.tools.attach不存在"最簡單粗暴的解決方案
agentmain 使用過程中的坑,看看你有沒有遇到

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章