[轉] Java Agent使用詳解


以下文章來源於古時的風箏 ,作者古時的風箏

  

  我們平時寫 Java Agent 的機會確實不多,也可以說幾乎用不着。但其實我們一直在用它,而且接觸的機會非常多。下面這些技術都使用了 Java Agent 技術,看一下你就知道為什么了。

  • 各個 Java IDE 的調試功能,例如 eclipse、IntelliJ ;
  • 熱部署功能,例如 JRebel、XRebel、 spring-loaded;
  • 各種線上診斷工具,例如 Btrace、Greys,還有阿里的 Arthas;
  • 各種性能分析工具,例如 Visual VM、JConsole 等;

  Java Agent 直譯過來叫做 Java 代理,還有另一種稱呼叫做 Java 探針。首先說 Java Agent 是一個 jar 包,只不過這個 jar 包不能獨立運行,它需要依附到我們的目標 JVM 進程中。我們來理解一下這兩種叫法。

  代理:比方說我們需要了解目標 JVM 的一些運行指標,我們可以通過 Java Agent 來實現,這樣看來它就是一個代理的效果,我們最后拿到的指標是目標 JVM ,但是我們是通過 Java Agent 來獲取的,對於目標 JVM 來說,它就像是一個代理;

  探針:這個說法我感覺非常形象,JVM 一旦跑起來,對於外界來說,它就是一個黑盒。而 Java Agent 可以像一支針一樣插到 JVM 內部,探到我們想要的東西,並且可以注入東西進去。

  拿上面的幾個我們平時會用到的技術舉例子。拿 IDEA 調試器來說吧,當開啟調試功能后,在 debugger 面板中可以看到當前上下文變量的結構和內容,還可以在 watches 面板中運行一些簡單的代碼,比如取值賦值等操作。還有 Btrace、Arthas 這些線上排查問題的工具,比方說有接口沒有按預期的返回結果,但日志又沒有錯誤,這時,我們只要清楚方法的所在包名、類名、方法名等,不用修改部署服務,就能查到調用的參數、返回值、異常等信息。

  上面只是說到了探測的功能,而熱部署功能那就不僅僅是探測這么簡單了。熱部署的意思就是說再不重啟服務的情況下,保證最新的代碼邏輯在服務生效。當我們修改某個類后,通過 Java Agent 的 instrument 機制,把之前的字節碼替換為新代碼所對應的字節碼。

 

1. Java Agent 結構

  Java Agent 最終以 jar 包的形式存在。主要包含兩個部分,一部分是實現代碼,一部分是配置文件。

  配置文件放在 META-INF 目錄下,文件名為 MANIFEST.MF 。包括以下配置項:

  Manifest-Version: 版本號 Created-By: 創作者 Agent-Class: agentmain 方法所在類 Can-Redefine-Classes: 是否可以實現類的重定義 Can-Retransform-Classes: 是否可以實現字節碼替換 Premain-Class: premain 方法所在類

  入口類實現 agentmain 和 premain 兩個方法即可,方法要實現什么功能就由你的需求決定了。

 

2. Java Agent 實現和使用

  接下來就來實現一個簡單的 Java Agent,基於 Java 1.8,主要實現兩點簡單的功能:

  1、打印當前加載的所有類的名稱;

  2、監控一個特定的方法,在方法中動態插入簡單的代碼並獲取方法返回值;

  在方法中插入代碼主要是用到了字節碼修改技術,字節碼修改技術主要有 javassist、ASM,已經 ASM 的高級封裝可擴展 cglib,這個例子中用的是 javassist。所以需要引入相關的 maven 包。

<dependency>
   <groupId>javassist</groupId>
   <artifactId>javassist</artifactId>
   <version>3.12.1.GA</version>
</dependency>

 

3. 實現入口類和功能邏輯

  入口類上面也說了,要實現 agentmain 和 premain 兩個方法。這兩個方法的運行時機不一樣。這要從 Java Agent 的使用方式來說了,Java Agent 有兩種啟動方式,一種是以 JVM 啟動參數 -javaagent:xxx.jar 的形式隨着 JVM 一起啟動,這種情況下,會調用 premain方法,並且是在主進程的 main方法之前執行。另外一種是以 loadAgent 方法動態 attach 到目標 JVM 上,這種情況下,會執行 agentmain方法。

  代碼實現如下:

package kite.lab.custom.agent;
import java.lang.instrument.Instrumentation;
public class MyCustomAgent {
   /**
    * jvm 參數形式啟動,運行此方法
    * @param agentArgs
    * @param inst
    */
   public static void premain(String agentArgs, Instrumentation inst){
       System.out.println("premain");
       customLogic(inst);
  }
   /**
    * 動態 attach 方式啟動,運行此方法
    * @param agentArgs
    * @param inst
    */
   public static void agentmain(String agentArgs, Instrumentation inst){
       System.out.println("agentmain");
       customLogic(inst);
  }
   /**
    * 打印所有已加載的類名稱
    * 修改字節碼
    * @param inst
    */
   private static void customLogic(Instrumentation inst){
       inst.addTransformer(new MyTransformer(), true);
       Class[] classes = inst.getAllLoadedClasses();
       for(Class cls :classes){
           System.out.println(cls.getName());
      }
  }
}

  我們看到這兩個方法都有參數 agentArgs 和 inst,其中 agentArgs 是我們啟動 Java Agent 時帶進來的參數,比如-javaagent:xxx.jar agentArgs。Instrumentation Java 開放出來的專門用於字節碼修改和程序監控的實現。我們要實現的打印已加載類和修改字節碼也就是基於它來實現的。其中 inst.getAllLoadedClasses()一個方法就實現了獲取所以已加載類的功能。

  inst.addTransformer方法則是實現字節碼修改的關鍵,后面的參數就是實現字節碼修改的實現類,代碼如下:

public class MyTransformer implements ClassFileTransformer {
   @Override
   public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
       System.out.println("正在加載類:"+ className);
       if (!"kite/attachapi/Person".equals(className)){
           return classfileBuffer;
      }
       CtClass cl = null;
       try {
           ClassPool classPool = ClassPool.getDefault();
           cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
           CtMethod ctMethod = cl.getDeclaredMethod("test");
           System.out.println("獲取方法名稱:"+ ctMethod.getName());
           ctMethod.insertBefore("System.out.println(\" 動態插入的打印語句 \");");
           ctMethod.insertAfter("System.out.println($_);");
           byte[] transformed = cl.toBytecode();
           return transformed;
      }catch (Exception e){
           e.printStackTrace();
      }
       return classfileBuffer;
  }
}

  以上代碼的邏輯就是當碰到加載的類是 kite.attachapi.Person的時候,在其中的 test 方法開始時插入一條打印語句,打印內容是"動態插入的打印語句",在test方法結尾處,打印返回值,其中$_就是返回值,這是 javassist 里特定的標示符。

 

4. MANIFEST.MF 配置文件

  在目錄 resources/META-INF/ 下創建文件名為 MANIFEST.MF 的文件,在其中加入如下的配置內容:

Manifest-Version: 1.0
Created-By: fengzheng
Agent-Class: kite.lab.custom.agent.MyCustomAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: kite.lab.custom.agent.MyCustomAgent

 

5. 配置打包所需的 pom 設置

  最后 Java Agent 是以 jar 包的形式存在,所以最后一步就是將上面的內容打到一個 jar 包里。

  在 pom 文件中加入以下配置:

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

  用的是 maven 的 maven-assembly-plugin 插件,注意其中要用 manifestFile 指定 MANIFEST.MF 所在路徑,然后指定 jar-with-dependencies ,將依賴包打進去。

  上面這是一種打包方式,需要單獨的 MANIFEST.MF 配合,還有一種方式,不需要在項目中單獨的添加 MANIFEST.MF 配置文件,完全在 pom 文件中配置上即可。

<build>
   <plugins>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-assembly-plugin</artifactId>
           <executions>
               <execution>
                   <goals>
                       <goal>attached</goal>
                   </goals>
                   <phase>package</phase>
                   <configuration>
                       <descriptorRefs>
                           <descriptorRef>jar-with-dependencies</descriptorRef>
                       </descriptorRefs>
                       <archive>
                           <manifestEntries>
                               <Premain-Class>kite.agent.vmargsmethod.MyAgent</Premain-Class>
                               <Agent-Class>kite.agent.vmargsmethod.MyAgent</Agent-Class>
                               <Can-Redefine-Classes>true</Can-Redefine-Classes>
                               <Can-Retransform-Classes>true</Can-Retransform-Classes>
                           </manifestEntries>
                       </archive>
                   </configuration>
               </execution>
           </executions>
       </plugin>
   </plugins>
</build>

  這種方式是將 MANIFEST.MF 的內容全部寫作 pom 配置中,打包的時候就會自動將配置信息生成 MANIFEST.MF 配置文件打進包里。

  運行打包命令

  接下來就簡單了,執行一條 maven 命令即可。

mvn assembly:assembly

  最后打出來的 jar 包默認是以「項目名稱-版本號-jar-with-dependencies.jar」這樣的格式生成到 target 目錄下。

 

6. 運行打包好的 Java Agent

  首先寫一個簡單的測試項目,用來作為目標 JVM,稍后會以兩種方式將 Java Agent 掛到這個測試項目上。

package kite.attachapi;
import java.util.Scanner;
public class RunJvm {
   public static void main(String[] args){
       System.out.println("按數字鍵 1 調用測試方法");
       while (true) {
           Scanner reader = new Scanner(System.in);
           int number = reader.nextInt();
           if(number==1){
               Person person = new Person();
               person.test();
          }
      }
  }
}

  以上只有一個簡單的 main 方法,用 while 的方式保證線程不退出,並且在輸入數字 1 的時候,調用 person.test()方法。

  以下是 Person 類

package kite.attachapi;
public class Person {
   public String test(){
       System.out.println("執行測試方法");
       return "I'm ok";
  }
}

  以命令行的方式運行

  因為項目是在 IDEA 里創建的,為了省事兒,我就直接在 IDEA 的 「Run/Debug Configurations」里加參數了。

-javaagent:/java-agent路徑/lab-custom-agent-1.0-SNAPSHOT-jar-with-dependencies.jar

 

   然后直接運行就可以看到效果了,會看到加載的類名稱。然后輸入數字鍵 "1",會看到字節碼修改后的內容。

  以動態 attach 的方式運行

  測試之前先要把這個測試項目跑起來,並把之前的參數去掉。運行后,找到這個它的進程id,一般利用jps -l即可。

  動態 attach 的方式是需要代碼實現的,實現代碼如下:

public class AttachAgent {
   public static void main(String[] args) throws Exception{
       VirtualMachine vm = VirtualMachine.attach("pid(進程號)");
       vm.loadAgent("java-agent路徑/lab-custom-agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
   }
}

  運行上面的 main 方法 並在測試程序中輸入“1”,會得到上圖同樣的結果。

  發現了沒,我們到這里實現的簡單的功能是不是和 BTrace 和 Arthas 有點像呢。我們攔截了指定的一個方法,並在這個方法里插入了代碼而且拿到了返回結果。如果把方法名稱變成可配置項,並且把返回結果保存到一個公共位置,例如一個內存數據庫,是不是我們就可以像 Arthas 那樣輕松的檢測線上問題了呢。當然了,Arthas 要復雜的多,但原理是一樣的。

 

7. sun.management.Agent 的實現

  不知道你平時有沒有用過 visualVM 或者 JConsole 之類的工具,其實,它們就是用了 management-agent.jar 這個Java Agent 來實現的。如果我們希望 Java 服務允許遠程查看 JVM 信息,往往會配置上一下這些參數:

-Dcom.sun.management.jmxremote
-Djava.rmi.server.hostname=192.168.1.1
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.rmi.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

  這些參數都是 management-agent.jar 定義的。

  我們進到 management-agent.jar 包下,看到只有一個 MANIFEST.MF 配置文件,配置內容為:

Manifest-Version: 1.0
Created-By: 1.7.0_07 (Oracle Corporation)
Agent-Class: sun.management.Agent
Premain-Class: sun.management.Agent

  可以看到入口 class 為 sun.management.Agent,進到這個類里面可以找到 agentmain 和 premain,並可以看到它們的邏輯。在這個類的開始,能看到我們前面對服務開啟遠程 JVM 監控需要開啟的那些參數定義。


免責聲明!

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



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