Java Agent 簡介


一、寫在前面

Java Agent 這個技術出現在 JDK1.5 之后,對於大多數人來說都比較陌生,但是多多少少又接觸過,實際上,我們平時用的很多工具,都是基於 Java Agent 實現的,例如常見的熱部署 JRebel,各種線上診斷工具(Btrace, Greys),還有阿里開源的 Arthas。

其實 Java Agent 一點都不神秘,也是一個 Jar 包,只是啟動方式和普通 Jar 包有所不同,對於普通的Jar包,通過指定類的 main 函數進行啟動,但是 Java Agent 並不能單獨啟動,必須依附在一個 Java 應用程序運行。

我們可以使用 Agent 技術構建一個獨立於應用程序的代理程序,用來協助監測、運行甚至替換其他 JVM 上的程序,使用它可以實現虛擬機級別的 AOP 功能。

二、動手寫一個 Java Agent

首先,我們先來寫一段簡單的 Agent 程序:

public class AgentTest {
    /**
     * 以 vm 參數的方式載入,在 java 程序的 main 方法執行之前執行
     *
     * @param agentArgs
     * @param inst      Agent技術主要使用的 api,我們可以使用它來改變和重新定義類的行為
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain start");

        System.out.println(agentArgs);
    }

    /**
     * 以 Attach 的方式載入,在 Java 程序啟動后執行
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain start");

        System.out.println(agentArgs);
    }
}

因為 Java Agent 的特殊性,需要一些特殊的配置,例如指定 Agent 的啟動類等。這樣才能在加載 Java Agent 之后,找到並運行對應的 agentmain 或者 premain 方法。配置方式主要有兩種,一種是利用 maven-assembly-plugin 插件(推薦),一種是 MANIFEST.MF 文件。

2.1 maven-assembly-plugin 插件

          <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>org.agent.AgentTest</Premain-Class>
                            <Agent-Class>org.agent.AgentTest</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>

2.2 MANIFEST.MF 文件

在 META-INF 目錄下創建 MANIFEST.MF 文件:

Manifest-Version: 1.0
Agent-Class: org.agent.AgentTest
Premain-Class: org.agent.AgentTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true

值得一提的是,即使新建了 MANIFEST.MF 文件,仍然需要配置 maven-assembly-plugin 信息,否則 MANIFEST.MF 信息會被 Maven 生成的信息覆蓋掉。

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestFile>
                            src/main/resources/META-INF/MANIFEST.MF
                        </manifestFile>
                    </archive>
                </configuration>
            </plugin>

配置完上面的內容,運行 mvn assembly:single 打包屬於 Java Agent 的 jar 包。

三、運行你的 Agent 程序

Java Agent 程序寫好了,怎么運行它呢?上面看到 Agent 程序分為兩種,一種是 premain 函數,在主程序運行之前執行;一種是 agentmain 函數,在主程序運行之后執行。Java 加載這兩種 Agent 程序也有區別:

3.1 主程序運行前加載

通過 JVM 參數 -javaagent:**.jar[=test] 啟動,其中 test 為傳入 premain 的 agentArgs 的參數,程序啟動的時候,會優先加載 Java Agent,並執行其 premain 方法,這個時候,其實大部分的類都還沒有被加載,這個時候可以實現對新加載的類進行字節碼修改,但是如果 premain 方法執行失敗或拋出異常,那么 JVM 會被中止,這是很致命的問題。

3.2 主程序運行后加載

程序啟動之后,通過某種特定的手段加載 Java Agent,這個特定的手段就是 VirtualMachine 的 attach api,這個 api 其實是 JVM 進程之間的的溝通橋梁,底層通過socket 進行通信,JVM A 可以發送一些指令給JVM B,B 收到指令之后,可以執行對應的邏輯,比如在命令行中經常使用的 jstack、jps 等,很多都是基於這種機制實現的。

VirtualMachine 的實現位於 tools.jar 中。

        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>

因為是進程間通信,所以使用 attach api 的也是一個獨立的Java進程,下面是一個簡單的實現:

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        VirtualMachine virtualMachine = null;
        try {
            // 1100 是進程號
            virtualMachine = VirtualMachine.attach("1100");
            // 第一個參數是 agent jar包路徑,第二個參數為傳入 agentmain 的 args 參數
            virtualMachine.loadAgent("D:\\concurrency-0.0.1-SNAPSHOT-jar-with-dependencies.jar", "test");
        } finally {
            if (virtualMachine != null) {
                virtualMachine.detach();
            }
        }

    }


推薦閱讀:

基於 Java Instrument的 Agent 實現

Instrumentation 新功能


免責聲明!

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



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