子曰:小勝靠智,大勝靠德,常勝靠身體。
1 什么是javaagent
javaagent是一個(gè)JVM“插件”,一種專門精心制作的.jar文件,它能夠利用JVM提供的Instrumentation API。
1.1 概要
Java Agent由三部分組成:代理類、代理類元信息和JVM加載.jar和代理的機(jī)制,整體內(nèi)容如下圖所示:

1.2 javaagent的基石
java.lang.instrument為javaagent 通過(guò)修改方法字節(jié)碼的方式操作運(yùn)行在JVM上的程序提供服務(wù)。javaagent以JAR包的形式部署,JAR文件清單中的屬性指定要加載的代理類,以啟動(dòng)代理。javaagent的啟動(dòng)方式有以下幾種:
- 通過(guò)在命令行指定參數(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è)字符串通過(guò)agentArgs變量傳遞,代理負(fù)責(zé)解析參數(shù)字符串。
如果代理因?yàn)榇眍悷o(wú)法被加載、代理類未實(shí)現(xiàn)premain方法或拋出了未被捕獲的異常,JVM將會(huì)退出。
javaagent的啟動(dòng)不要求實(shí)現(xiàn)一定提供命令行的方式,如果,實(shí)現(xiàn)支持通過(guò)命令行啟動(dòng),實(shí)現(xiàn)必須支持在命令行中通過(guò)指定-javaagent參數(shù)啟動(dòng)。-javaagent可以在命令行中使用多次,啟動(dòng)多個(gè)代理。premain函數(shù)的調(diào)用順序和命令行中指定的順序一致,多個(gè)代理可以使用相同<jarpath>.
沒有一個(gè)嚴(yán)格模型來(lái)定義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ù)的傳遞也是通過(guò)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)后返回。如果,代理無(wú)法啟動(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)類加載器可以加載代理支持的類(通過(guò)appendToBootstrapClassLoaderSearch或指定Boot-Class-Path屬性)必須僅鏈接到定義啟動(dòng)類加載器的類。 無(wú)法保證啟動(dòng)類加載器可以在所有平臺(tái)工作。
如果配置了自定義系統(tǒng)類加載器(通過(guò)getSystemClassLoader方法中指定的系統(tǒng)屬性java.system.class.loader),則必須定義appendToSystemClassLoaderSearch中指定的appendToClassPathForInstrumentation方法。 換句話說(shuō),自定義系統(tǒng)類加載器必須支持將代理JAR文件添加到系統(tǒng)類加載器搜索范圍內(nèi)的機(jī)制。
1.4 javaagent清單屬性
| 屬性 | 說(shuō)明 | 是否必選 | 默認(rèn)值 |
|---|---|---|---|
| Premain-Class | 包含premain方法的類 | 依賴啟動(dòng)方式 | 無(wú) |
| Agent-Class | 包含agentmain方法的類 | 依賴啟動(dòng)方式 | 無(wú) |
| Boot-Class-Path | 啟動(dòng)類加載器搜索路徑 | 否 | 無(wú) |
| Can-Redefine-Classes | 是否可以重定義代理所需的類 | 否 | false |
| Can-Retransform-Classes | 是否能夠重新轉(zhuǎn)換此代理所需的類 | 否 | false |
| Can-Set-Native-Method-Prefix | 是否能夠設(shè)置此代理所需的本機(jī)方法前綴 | 否 | false |
2 寫一個(gè)Java Agent
基于上面的介紹,我們實(shí)現(xiàn)一個(gè)下載JVM中所有非系統(tǒng)類的javaagent。整個(gè)開發(fā)過(guò)程包括以下三步: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");
@Override
public 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)容不完全一致):
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-bo
ot-starter-parent/agent
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