天天看點

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 檔案:

Java Agent 簡介
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 新功能