IDEA + maven 零基礎構建 java agent 項目

小灰灰blog 發佈 2020-03-16T05:42:52+00:00

JavaAgent雖說在jdk1.5之後就有了,但是對於絕大多數的業務開發javaer來說,這個東西還是比較神奇和陌生的;雖說在實際的業務開發中,很少會涉及到agent開發,但是每個java開發都用過,比如使用idea寫了個HelloWorld.java,並運行一下,仔細看控制台

Java Agent(java 探針)雖說在 jdk1.5 之後就有了,但是對於絕大多數的業務開發 javaer 來說,這個東西還是比較神奇和陌生的;雖說在實際的業務開發中,很少會涉及到 agent 開發,但是每個 java 開發都用過,比如使用 idea 寫了個 HelloWorld.java,並運行一下, 仔細看控制台輸出

image

本篇將作為 Java Agent 的入門篇,手把手教你開發一個統計方法耗時的 Java Agent

I. Java Agent 開發

首先明確我們的開發環境,選擇 IDEA 作為編輯器,maven 進行包管理

1. 核心邏輯

創建一個新的項目(or 子 module),然後我們新建一個 SimpleAgent 類

public class SimpleAgent {

    /**
     * jvm 參數形式啟動,運行此方法
     *
     * @param agentArgs
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain");
    }

    /**
     * 動態 attach 方式啟動,運行此方法
     *
     * @param agentArgs
     * @param inst
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain");
    }
}

我們先忽略上面兩個方法的具體玩法,先簡單看一下這兩個方法的區別,注釋上也說了

  • jvm 參數形式: 調用 premain 方法
  • attach 方式: 調用 agentmain 方法

其中 jvm 方式,也就是說要使用這個 agent 的目標應用,在啟動的時候,需要指定 jvm 參數 -javaagent:xxx.jar,當我們提供的 agent 屬於基礎必備服務時,可以用這種方式

當目標應用程式啟動之後,並沒有添加-javaagent加載我們的 agent,依然希望目標程序使用我們的 agent,這時候就可以使用 attach 方式來使用(後面會介紹具體的使用姿勢),自然而然的會想到如果我們的 agent 用來 debug 定位問題,就可以用這種方式

2. 打包

上面一個簡單 SimpleAgent 就把我們的 Agent 的核心功能寫完了(就是這麼簡單),接下來需要打一個 Jar 包

通過 maven 插件,可以比較簡單的輸出一個合規的 java agent 包,有兩種常見的使用姿勢

a. pom 指定配置

在 pom.xml 文件中,添加如下配置,請注意一下manifestEntries標籤內的參數

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

            <executions>
                <execution>
                    <goals>
                        <goal>attached</goal>
                    </goals>
                    <phase>package</phase>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

然後通過 mvn assembly:assembly 命令打包,在target目錄下,可以看到一個後綴為jar-with-dependencies的 jar 包,就是我們的目標

b. MANIFEST.MF 配置文件

通過配置文件MANIFEST.MF,可能更加常見,這裡也簡單介紹下使用姿勢

  • 在資源目錄(Resources)下,新建目錄META-INF
  • 在META-INF目錄下,新建文件MANIFEST.MF

文件內容如下

Manifest-Version: 1.0
Premain-Class: com.git.hui.agent.SimpleAgent
Agent-Class: com.git.hui.agent.SimpleAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true


請注意,最後的一個空行(如果我上面沒有顯示的話,多半是 markdown 渲染有問題),不能少,在 idea 中,刪除最後一行時,會有錯誤提醒

image

然後我們的pom.xml配置,需要作出對應的修改

<build>
  <plugins>
      <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>
                  <!--<manifestEntries>-->
                      <!--<Premain-Class>com.git.hui.agent.SimpleAgent</Premain-Class>-->
                      <!--<Agent-Class>com.git.hui.agent.SimpleAgent</Agent-Class>-->
                      <!--<Can-Redefine-Classes>true</Can-Redefine-Classes>-->
                      <!--<Can-Retransform-Classes>true</Can-Retransform-Classes>-->
                  <!--</manifestEntries>-->
              </archive>
          </configuration>

          <executions>
              <execution>
                  <goals>
                      <goal>attached</goal>
                  </goals>
                  <phase>package</phase>
              </execution>
          </executions>
      </plugin>
  </plugins>
</build>

同樣通過mvn assembly:assembly命令打包

II. Agent 使用

agent 有了,接下來就是需要測試一下使用 agent 的使用了,上面提出了兩種方式,我們下面分別進行說明

1. jvm 參數

首先新建一個 demo 項目,寫一個簡單的測試類

public class BaseMain {

    public int print(int i) {
        System.out.println("i: " + i);
        return i + 2;
    }

    public void run() {
        int i = 1;
        while (true) {
            i = print(i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        BaseMain main = new BaseMain();
        main.run();
        Thread.sleep(1000 * 60 * 60);
    }
}

測試類中,有一個死循環,各 1s 調用一下 print 方法,IDEA 測試時,可以直接在配置類,添加 jvm 參數,如下

image

請注意上面紅框的內容為上一節打包的 agent 絕對地址: -javaagent:/Users/..../target/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar

執行 main 方法之後,會看到控制台輸出

image

請注意上面的premain, 這個就是我們上面的SimpleAgent中的premain方法輸出,且只輸出了一次

2. attach 方式

在使用 attach 方式時,可以簡單的理解為要將我們的 agent 注入到目標的應用程式中,所以我們需要自己起一個程序來完成這件事情

public class AttachMain {
    public static void main(String[] args)
            throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
        // attach方法參數為目標應用程式的進程號
        VirtualMachine vm = VirtualMachine.attach("36633");
        // 請用你自己的agent絕對地址,替換這個
        vm.loadAgent("/Users/......./target/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
    }
}

上面的邏輯比較簡單,首先通過jps -l獲取目標應用的進程號

image

當上面的 main 方法執行完畢之後,控制台會輸出類似下面的兩行日誌,可以簡單的理解為我連上目標應用,並丟了一個 agent,然後揮一揮衣袖不帶走任何雲彩的離開了

Connected to the target VM, address: '127.0.0.1:63710', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:63710', transport: 'socket'

接下來再看一下上面的 BaseMain 的輸出,中間夾著一行agentmain, 就表明 agent 被成功注入進去了

image

3. 小結

本文介紹了 maven + idea 環境下,手把手教你開發一個 hello world 版 JavaAgent 並打包的全過程

兩個方法

方法說明使用姿勢premain()agent 以 jvm 方式加載時調用,即目標應用在啟動時,指定了 agent-javaagent:xxx.jaragentmain()agent 以 attach 方式運行時調用,目標應用程式正常工作時使用VirtualMachine.attach(pid)來指定目標進程號
vm.loadAgent("...jar")加載 agent

兩種打包姿勢

打包為可用的 java agent 時,需要注意配置參數,上面提供了兩種方式,一個是直接在pom.xml中指定配置

<manifestEntries>
    <Premain-Class>com.git.hui.agent.SimpleAgent</Premain-Class>
    <Agent-Class>com.git.hui.agent.SimpleAgent</Agent-Class>
    <Can-Redefine-Classes>true</Can-Redefine-Classes>
    <Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>

另外一個是在配置文件 META-INF/MANIFEST.MF 中寫好(需要注意最後一個空行不可或缺)

Manifest-Version: 1.0
Premain-Class: com.git.hui.agent.SimpleAgent
Agent-Class: com.git.hui.agent.SimpleAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true


當然本篇內容看完之後,會發現對 java agent 的實際開發還是不太清楚,難道 agent 就是在前面輸出一行hello world就完事了麼,這和想像中的完全不一樣啊

下一篇博文將手把手教你實現一個方法統計耗時的 java agent 包,將詳細說明利用接口Instrumentation來實現字節碼修改,從而是實現功能增強

II. 其他

0. 源碼

  • https://github.com/liuyueyi/java-agent
關鍵字: