一个简单JavaAgent的实现
一、什么是javaagent
javaagent是一個(gè)JVM“插件”,一種專門精心制作的.jar文件,它能夠利用JVM提供的Instrumentation API。
1.1、概要
Java Agent由三部分組成:代理類、代理類元信息和JVM加載.jar和代理的機(jī)制,整體內(nèi)容如下圖所示:
1.2、javaagent的基石
java.lang.instrument 為javaagent 通過修改方法字節(jié)碼的方式操作運(yùn)行在JVM上的程序提供服務(wù)。javaagent以JAR包的形式部署,JAR文件清單中的屬性指定要加載的代理類,以啟動(dòng)代理。
javaagent的啟動(dòng)方式有以下幾種:
-
通過在命令行指定參數(shù)啟動(dòng)。
-
JVM啟動(dòng)后啟動(dòng)。例如,提供一種工具,該工具可以依附到已運(yùn)行的應(yīng)用,并允許在已運(yùn)行的應(yīng)用內(nèi)加載代理。
-
與應(yīng)用一起打包為可執(zhí)行文件。
1.3、啟動(dòng) javaagent
1.3.1、命令行啟動(dòng)
命令行啟動(dòng)參數(shù)如下:
-javaagent:<jarpath>[=<options>]<jarpath> :javaagent的路徑,比如 /opt/var/Agent-1.0.0.jar 。
<options> : javaagent參數(shù),參數(shù)的解析由javaagent負(fù)責(zé)。
javaagent JAR文件清單必須包含 Premain-Class 屬性,屬性的值為agent class的全路徑名(包名+類名)。代理類必須實(shí)現(xiàn) premain 方法,premain 方法和 main 方法一樣分別是代理和應(yīng)用的入口點(diǎn)。JVM初始化完成后首先調(diào)用代理的premain函數(shù),然后調(diào)用應(yīng)用的main函數(shù),premain方法必須返回后進(jìn)程才能啟動(dòng)。
premain 方法簽名如下:
public static void premain(String agentArgs, Instrumentation inst) public static void premain(String agentArgs)JVM首先嘗試在代理中調(diào)用簽名為1的方法,如果代理類沒有實(shí)現(xiàn)簽名為1的方法,JVM嘗試調(diào)用簽名為2的方法:
代理類可以有一個(gè) agentmain函數(shù),函數(shù)會(huì)在JVM啟動(dòng)完成之后調(diào)用。如果,使用命令行啟動(dòng)代理,agentmain 方式不會(huì)被調(diào)用。
代理的所有參數(shù)被當(dāng)作一個(gè)字符串通過 agentArgs 變量傳遞,代理負(fù)責(zé)解析參數(shù)字符串。
如果代理因?yàn)榇眍悷o法被加載、代理類未實(shí)現(xiàn) premain 方法或拋出了未被捕獲的異常,JVM將會(huì)退出。
javaagent的啟動(dòng)不要求實(shí)現(xiàn)一定提供命令行的方式,如果,實(shí)現(xiàn)支持通過命令行啟動(dòng),實(shí)現(xiàn)必須支持在命令行中通過指定 -javaagent 參數(shù)啟動(dòng)。 -javaagent 可以在命令行中使用多次,啟動(dòng)多個(gè)代理。premain 函數(shù)的調(diào)用順序和命令行中指定的順序一致,多個(gè)代理可以使用相同 <jarpath>。
沒有一個(gè)嚴(yán)格模型來定義 premain 函數(shù)的工作范圍,任何 main 函數(shù)可以做的工作,比如創(chuàng)建線程,在 premain 函數(shù)中都是合法的。
1.3.2、JVM啟動(dòng)后啟動(dòng)
實(shí)現(xiàn)可以提供在JVM啟動(dòng)之后再啟動(dòng)代理的機(jī)制。代理如何啟動(dòng)的細(xì)節(jié)特定于實(shí)現(xiàn),通常應(yīng)用程序已經(jīng)啟動(dòng),并且它的 main 方法已經(jīng)被調(diào)用。如果實(shí)現(xiàn)支持在JVM啟動(dòng)后啟動(dòng)代理,代理必須滿足以下條件:
-
清單文件包含 Agent-Class 屬性,屬性的值為代理類全名。
-
代理類必須實(shí)現(xiàn) public static agentmain 方法。
agentmain方法有以下兩個(gè)函數(shù)簽名:
public static void agentmain(String agentArgs, Instrumentation inst) public static void agentmain(String agentArgs)JVM首先嘗試調(diào)用具有簽名1的方法,如果,代理類沒有實(shí)現(xiàn)該方法,JVM嘗試調(diào)用簽名為2的方法。
代理類可以同時(shí)實(shí)現(xiàn) premain 和 agentmain 兩個(gè)方法,當(dāng)代理以命令行方式啟動(dòng)時(shí),JVM調(diào)用 premain 函數(shù),當(dāng)代理在JVM啟動(dòng)之后啟動(dòng)時(shí),JVM調(diào)用 agentmain 函數(shù),而且JVM不會(huì)調(diào)用 premain 函數(shù)。
agentmain 函數(shù)參數(shù)的傳遞也是通過 agentArgs,所有參數(shù)組合為一個(gè)字符串,參數(shù)的解析由代理負(fù)責(zé)。
agentmain 函數(shù)必須完成啟動(dòng)代理所有必須的初始化動(dòng)作,當(dāng)啟動(dòng)完成后,agentmain 函數(shù)必須返回。如果,代理不能啟動(dòng)或拋出未捕獲的異常,JVM都會(huì)退出。
1.3.3、打包為可執(zhí)行文件
如果代理打包到可執(zhí)行JAR文件中,可執(zhí)行JAR文件的清單中必須包含 Launcher-Agent-Class 屬性,指定一個(gè)在應(yīng)用main函數(shù)調(diào)用之前代理啟動(dòng)的類。JVM嘗試在代理上調(diào)用以下方法:
public static void agentmain(String agentArgs, Instrumentation inst)如果,代理類沒有實(shí)現(xiàn)上述方法,JVM則調(diào)用下面的方法。
public static void agentmain(String agentArgs)agentArgs 參數(shù)的值必須為空字符串。
agentmain 函數(shù)必須完成代理啟動(dòng)必須的所有初始化動(dòng)作并在啟動(dòng)后返回。如果,代理無法啟動(dòng)或拋出未捕獲的異常,JVM會(huì)退出。
1.3.4、加載代理類以及代理類可用的模塊/類
系統(tǒng)類加載器負(fù)責(zé)加載代理JAR文件中的所有類,并且成為系統(tǒng)類加載器的未命名模塊的成員。 系統(tǒng)類加載器通常也定義包含應(yīng)用程序 main 方法的類。對(duì)代理類可見的所有類都對(duì)系統(tǒng)類加載器可見,必須滿足下面的最低要求:
-
啟動(dòng)層中的模塊導(dǎo)出的包中的類。 啟動(dòng)層是否包含所有平臺(tái)模塊取決于初始模塊或應(yīng)用程序的啟動(dòng)方式。
-
類可被系統(tǒng)類加載器定義。
-
啟動(dòng)類加載器定義的所有代理的類為其未命名模塊的成員。
如果代理類需要鏈接到不在啟動(dòng)層中的平臺(tái)(或其他)模塊中的類,則需要以確保這些模塊位于啟動(dòng)層中的方式啟動(dòng)應(yīng)用程序。 例如,在JDK實(shí)現(xiàn)中,--add-modules 命令行選項(xiàng)可用于將模塊添加到要在啟動(dòng)時(shí)解析的根模塊集中。
啟動(dòng)類加載器可以加載代理支持的類(通過 appendToBootstrapClassLoaderSearch 或指定Boot-Class-Path屬性)必須僅鏈接到定義啟動(dòng)類加載器的類。 無法保證啟動(dòng)類加載器可以在所有平臺(tái)工作。
如果配置了自定義系統(tǒng)類加載器(通過 getSystemClassLoader 方法中指定的系統(tǒng)屬性 java.system.class.loader ),則必須定義 appendToSystemClassLoaderSearch 中指定的 appendToClassPathForInstrumentation 方法。 換句話說,自定義系統(tǒng)類加載器必須支持將代理JAR文件添加到系統(tǒng)類加載器搜索范圍內(nèi)的機(jī)制。
1.4、javaagent清單屬性
| Premain-Class | 包含premain方法的類 | 依賴啟動(dòng)方式 | 無 |
| Agent-Class | 包含agentmain方法的類 | 依賴啟動(dòng)方式 | 無 |
| Boot-Class-Path | 啟動(dòng)類加載器搜索路徑 | 否 | 無 |
| Can-Redefine-Classis | 是否可以重定義代理所需的類 | 否 | false |
| Can-Retransform-Classis | 是否能夠重新轉(zhuǎn)換此代理所需的類 | 否 | false |
| Can-Set-Native-Method-Prefix | 是否能夠設(shè)置此代理所需的本機(jī)方法前綴 | 否 | false |
二、寫一個(gè)Java Agent
基于上面的介紹,我們實(shí)現(xiàn)一個(gè)下載JVM中所有非系統(tǒng)類的javaagent。
整個(gè)開發(fā)過程包括以下三步:
-
1)定義代理類,實(shí)現(xiàn)類下載功能;
-
2)配置、打包;
-
3)命令行啟動(dòng)測(cè)試。
2.1、代理類實(shí)現(xiàn)
實(shí)現(xiàn) premain 函數(shù)
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);// 注冊(cè)我們的文件下載函數(shù)instrumentation.addTransformer(new DumpClassesService());} }文件下載類實(shí)現(xiàn) ClassFileTransformer 接口,在類被加載時(shí)下載類的字節(jié)碼:
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 {// 將類名統(tǒng)一命名為classNamedump.class格式fos = new FileOutputStream(className + "dump.class");fos.write(classfileBuffer);fos.flush();} catch (IOException ioe) {ioe.printStackTrace();} finally {// 關(guān)閉文件輸出流if (null != fos) {try {fos.close();} catch (IOException e) {e.printStackTrace();}}}}return classfileBuffer;}/*** 判斷一個(gè)類是否為系統(tǒng)類** @param className 類名* @return System Class then return true,else return false*/private boolean isSystemClass(String className) {// 假設(shè)系統(tǒng)類的類名不為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 文件兩種方式生成:手動(dòng)配置和自動(dòng)生成,手動(dòng)配置只需要在 resources 文件下創(chuàng)建 META-INF/MENIFEST.MF 文件即可。除去手動(dòng)配置外,可以使用maven插件在打包階段自動(dòng)生成,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的文件內(nèi)容如下(不同的配置生成的文件內(nèi)容不完全一致):
2.3、命令行啟動(dòng)Java Agent
執(zhí)行下面的命令,運(yùn)行已經(jīng)編譯好的類Hello,可以在同級(jí)目錄下生成一個(gè)名為Hellodump.class的文件。
java -javaagent:agent-0.0.1-SNAPSHOT.jar Hello總結(jié)
以上是生活随笔為你收集整理的一个简单JavaAgent的实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 公积金发生额是什么意思
- 下一篇: Java基础——synchronized