一、什么是javaagent
javaagent是一個JVM“插件”,一種專門精心制作的.jar文件,它能夠利用JVM提供的Instrumentation API。
1.1、概要
Java Agent由三部分組成:代理類、代理類元信息和JVM加載.jar和代理的機制,整體內容如下圖所示:
1.2、javaagent的基石
java.lang.instrument
為javaagent 通過修改方法字節碼的方式操作運行在JVM上的程序提供服務。javaagent以JAR包的形式部署,JAR文件清單中的屬性指定要加載的代理類,以啟動代理。
javaagent的啟動方式有以下幾種:
-
通過在命令行指定參數啟動。
-
JVM啟動后啟動。例如,提供一種工具,該工具可以依附到已運行的應用,并允許在已運行的應用內加載代理。
-
與應用一起打包為可執行文件。
1.3、啟動 javaagent
1.3.1、命令行啟動
命令行啟動參數如下:
-javaagent:<jarpath>[=<options>]
<jarpath>
:javaagent的路徑,比如 /opt/var/Agent-1.0.0.jar
。
<options>
: javaagent參數,參數的解析由javaagent負責。
javaagent JAR文件清單必須包含 Premain-Class
屬性,屬性的值為agent class的全路徑名(包名+類名)。代理類必須實現 premain
方法,premain
方法和 main
方法一樣分別是代理和應用的入口點。JVM初始化完成后首先調用代理的premain函數,然后調用應用的main函數,premain方法必須返回后進程才能啟動。
premain
方法簽名如下:
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
JVM首先嘗試在代理中調用簽名為1的方法,如果代理類沒有實現簽名為1的方法,JVM嘗試調用簽名為2的方法:
代理類可以有一個 agentmain
函數,函數會在JVM啟動完成之后調用。如果,使用命令行啟動代理,agentmain
方式不會被調用。
代理的所有參數被當作一個字符串通過 agentArgs
變量傳遞,代理負責解析參數字符串。
如果代理因為代理類無法被加載、代理類未實現 premain
方法或拋出了未被捕獲的異常,JVM將會退出。
javaagent的啟動不要求實現一定提供命令行的方式,如果,實現支持通過命令行啟動,實現必須支持在命令行中通過指定 -javaagent
參數啟動。 -javaagent
可以在命令行中使用多次,啟動多個代理。premain
函數的調用順序和命令行中指定的順序一致,多個代理可以使用相同 <jarpath>
。
沒有一個嚴格模型來定義 premain
函數的工作范圍,任何 main
函數可以做的工作,比如創建線程,在 premain
函數中都是合法的。
1.3.2、JVM啟動后啟動
實現可以提供在JVM啟動之后再啟動代理的機制。代理如何啟動的細節特定于實現,通常應用程序已經啟動,并且它的 main
方法已經被調用。如果實現支持在JVM啟動后啟動代理,代理必須滿足以下條件:
-
清單文件包含
Agent-Class
屬性,屬性的值為代理類全名。 -
代理類必須實現
public static agentmain
方法。
agentmain方法有以下兩個函數簽名:
public static void agentmain(String agentArgs, Instrumentation inst)
public static void agentmain(String agentArgs)
JVM首先嘗試調用具有簽名1的方法,如果,代理類沒有實現該方法,JVM嘗試調用簽名為2的方法。
代理類可以同時實現 premain
和 agentmain
兩個方法,當代理以命令行方式啟動時,JVM調用 premain
函數,當代理在JVM啟動之后啟動時,JVM調用 agentmain
函數,而且JVM不會調用 premain
函數。
agentmain
函數參數的傳遞也是通過 agentArgs
,所有參數組合為一個字符串,參數的解析由代理負責。
agentmain
函數必須完成啟動代理所有必須的初始化動作,當啟動完成后,agentmain
函數必須返回。如果,代理不能啟動或拋出未捕獲的異常,JVM都會退出。
1.3.3、打包為可執行文件
如果代理打包到可執行JAR文件中,可執行JAR文件的清單中必須包含 Launcher-Agent-Class
屬性,指定一個在應用main函數調用之前代理啟動的類。JVM嘗試在代理上調用以下方法:
public static void agentmain(String agentArgs, Instrumentation inst)
如果,代理類沒有實現上述方法,JVM則調用下面的方法。
public static void agentmain(String agentArgs)
agentArgs
參數的值必須為空字符串。
agentmain
函數必須完成代理啟動必須的所有初始化動作并在啟動后返回。如果,代理無法啟動或拋出未捕獲的異常,JVM會退出。
1.3.4、加載代理類以及代理類可用的模塊/類
系統類加載器負責加載代理JAR文件中的所有類,并且成為系統類加載器的未命名模塊的成員。 系統類加載器通常也定義包含應用程序 main
方法的類。對代理類可見的所有類都對系統類加載器可見,必須滿足下面的最低要求:
-
啟動層中的模塊導出的包中的類。 啟動層是否包含所有平臺模塊取決于初始模塊或應用程序的啟動方式。
-
類可被系統類加載器定義。
-
啟動類加載器定義的所有代理的類為其未命名模塊的成員。
如果代理類需要鏈接到不在啟動層中的平臺(或其他)模塊中的類,則需要以確保這些模塊位于啟動層中的方式啟動應用程序。 例如,在JDK實現中,--add-modules
命令行選項可用于將模塊添加到要在啟動時解析的根模塊集中。
啟動類加載器可以加載代理支持的類(通過 appendToBootstrapClassLoaderSearch
或指定Boot-Class-Path屬性)必須僅鏈接到定義啟動類加載器的類。 無法保證啟動類加載器可以在所有平臺工作。
如果配置了自定義系統類加載器(通過 getSystemClassLoader
方法中指定的系統屬性 java.system.class.loader
),則必須定義 appendToSystemClassLoaderSearch
中指定的 appendToClassPathForInstrumentation
方法。 換句話說,自定義系統類加載器必須支持將代理JAR文件添加到系統類加載器搜索范圍內的機制。
1.4、javaagent清單屬性
屬性 | 說明 | 是否必選 | 默認值 |
---|---|---|---|
Premain-Class | 包含premain方法的類 | 依賴啟動方式 | 無 |
Agent-Class | 包含agentmain方法的類 | 依賴啟動方式 | 無 |
Boot-Class-Path | 啟動類加載器搜索路徑 | 否 | 無 |
Can-Redefine-Classis | 是否可以重定義代理所需的類 | 否 | false |
Can-Retransform-Classis | 是否能夠重新轉換此代理所需的類 | 否 | false |
Can-Set-Native-Method-Prefix | 是否能夠設置此代理所需的本機方法前綴 | 否 | false |
二、寫一個Java Agent
基于上面的介紹,我們實現一個下載JVM中所有非系統類的javaagent。
整個開發過程包括以下三步:
-
1)定義代理類,實現類下載功能;
-
2)配置、打包;
-
3)命令行啟動測試。
2.1、代理類實現
實現 premain
函數
package io.ct.java.agent;import java.lang.instrument.Instrumentation;public class AgentApplication {public static void premain(String arg, Instrumentation instrumentation) {System.err.println("agent startup , args is " + arg);// 注冊我們的文件下載函數instrumentation.addTransformer(new DumpClassesService());}
}
文件下載類實現 ClassFileTransformer
接口,在類被加載時下載類的字節碼:
package io.ct.java.agent;import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.List;/*** Copyright (C), 2018-2018, open source* FileName: DumpClassesService** @author : 大哥* Date: 2018/12/8 21:01*/
public class DumpClassesService implements ClassFileTransformer {private static final List<String> SYSTEM_CLASS_PREFIX = Arrays.asList("java", "sum", "jdk");@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {if (!isSystemClass(className)) {System.out.println("load class " + className);FileOutputStream fos = null;try {// 將類名統一命名為classNamedump.class格式fos = new FileOutputStream(className + "dump.class");fos.write(classfileBuffer);fos.flush();} catch (IOException ioe) {ioe.printStackTrace();} finally {// 關閉文件輸出流if (null != fos) {try {fos.close();} catch (IOException e) {e.printStackTrace();}}}}return classfileBuffer;}/*** 判斷一個類是否為系統類** @param className 類名* @return System Class then return true,else return false*/private boolean isSystemClass(String className) {// 假設系統類的類名不為NULL而且不為空if (null == className || className.isEmpty()) {return false;}for (String prefix : SYSTEM_CLASS_PREFIX) {if (className.startsWith(prefix)) {return true;}}return false;}
}
2.2、配置MANIFEST.MF
MANIFEST.MF
文件兩種方式生成:手動配置和自動生成,手動配置只需要在 resources
文件下創建 META-INF/MENIFEST.MF
文件即可。除去手動配置外,可以使用maven插件在打包階段自動生成,maven的插件配置如下:
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><configuration><archive><manifestEntries><Premain-Class>io.ct.java.agent.AgentApplication</Premain-Class><Agent-Class>io.ct.java.agent.AgentApplication</Agent-Class><Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes></manifestEntries></archive></configuration></plugin>
生成的jar包格式如下:
其中MANIFEST.MF的文件內容如下(不同的配置生成的文件內容不完全一致):
Manifest-Version: 1.0
Implementation-Title: agent
Premain-Class: io.ct.java.agent.AgentApplication
Implementation-Version: 0.0.1-SNAPSHOT
Built-By: chentong
Agent-Class: io.ct.java.agent.AgentApplication
Can-Redefine-Classes: true
Implementation-Vendor-Id: io.ct.java
Can-Retransform-Classes: true
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_171
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/agent
2.3、命令行啟動Java Agent
執行下面的命令,運行已經編譯好的類Hello,可以在同級目錄下生成一個名為Hellodump.class的文件。
java -javaagent:agent-0.0.1-SNAPSHOT.jar Hello