轉自:https://blog.csdn.net/qinhaotong/article/details/100693414
javaAgent
Javaagent 是什么?
javaAgent運行類加載器在加載類之前對類做出動態的修改.
運行java命令執行時添加參數 -javaagent指定打包好的agent的jar即可以. 可以定義多個agent,按指定順序執行
java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar
字節碼插樁,bTrace,Arthas 都是通過這種方式來實現。
javaAgent類方法
創建一個類里面定義agent方法
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
JVM 會優先加載 帶 Instrumentation 簽名的方法,加載成功忽略第二種,如果第一種沒有,則加載第二種方法。Instrumentation是一個重要的參數。
Instrumentation 接口定義的方法
public interface Instrumentation {
//增加一個Class 文件的轉換器,轉換器用於改變 Class 二進制流的數據,參數 canRetransform 設置是否允許重新轉換。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);//在類加載之前,重新定義 Class 文件,ClassDefinition 表示對一個類新的定義,如果在類加載之后,需要使用 retransformClasses 方法
重新定義。
addTransformer方法配置之后,后續的類加載都會被Transformer攔截。對於已經加載過的類,可以執行retransformClasses來重新觸發
這個Transformer的攔截。類加載的字節碼被修改后,除非再次被retransform,否則不會恢復。
void addTransformer(ClassFileTransformer transformer);
//刪除一個類轉換器
boolean removeTransformer(ClassFileTransformer transformer);boolean isRetransformClassesSupported();
//在類加載之后,重新定義 Class。這個很重要,該方法是1.6 之后加入的,事實上,該方法是 update 了一個類。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
boolean isRedefineClassesSupported();
void redefineClasses(ClassDefinition... definitions)throws ClassNotFoundException, UnmodifiableClassException;
boolean isModifiableClass(Class<?> theClass);
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);//獲取一個對象的大小
long getObjectSize(Object objectToSize);void appendToBootstrapClassLoaderSearch(JarFile jarfile);
void appendToSystemClassLoaderSearch(JarFile jarfile);
boolean isNativeMethodPrefixSupported();
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
Java Agent - Hello World
使用 javaagent 需要幾個步驟:
定義一個 MANIFEST.MF 文件,必須包含 Premain-Class 選項,通常也會加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項。
創建一個Premain-Class 指定的類,類中包含 premain 方法,方法邏輯由用戶自己確定。
將 premain 的類和 MANIFEST.MF 文件打成 jar 包。
使用參數 -javaagent: jar包路徑 啟動要代理的方法。
在執行以上步驟后,JVM 會先執行 premain 方法,大部分類加載都會通過該方法,注意:是大部分,不是所有。當然,遺漏的主要是系統類,因為很多系統類先於 agent 執行,而用戶類的加載肯定是會被攔截的。也就是說,這個方法是在 main 方法啟動前攔截大部分類的加載活動,既然可以攔截類的加載,那么就可以去做重寫類這樣的操作,結合第三方的字節碼編譯工具,比如ASM,javassist,cglib等等來改寫實現類。
創建javaAgent類
public class HelloAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("agentArgs : " + agentArgs);
inst.addTransformer(new DefineTransformer(), true);
}static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomainprotectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("premain load Class:" + className);
return classfileBuffer;
}
}
}
創建MANIFEST.MF
Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: com.tttiger.HelloAgent
注意最后一行需要有空行。在idea中添加article,提示不接受不用管,idea會自動給你創建MANIFEST.MF可能會沖突,讓idea創建,創建好后再去修改配置。
agent掛載運行
新創建一個類或jar,在運行的時候添加命令 -javaagent:e:/xxx.jar 打包完成的javaagentjar包路徑。
使用maven進行打包
添加maven插件指定javaagent類,maven自動完成manifest配置,不用自己再去配置推薦
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!--自動添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.rickiyang.learn.PreMainTraceAgent</Premain-Class>
<Agent-Class>com.rickiyang.learn.PreMainTraceAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
MANIFEST.MF參數說明
Premain-Class :包含 premain 方法的類(類的全路徑名)main方法運行前代理
Agent-Class :包含 agentmain 方法的類(類的全路徑名)另一種代理main開始后可以修改類結構
Boot-Class-Path :設置引導類加載器搜索的路徑列表。查找類的特定於平台的機制失敗后,引導類加載器會搜索這些路徑。按列出的順序搜索路徑。列表中的路徑由一個或多個空格分開。路徑使用分層 URI 的路徑組件語法。如果該路徑以斜杠字符(“/”)開頭,則為絕對路徑,否則為相對路徑。相對路徑根據代理 JAR 文件的絕對路徑解析。忽略格式不正確的路徑和不存在的路徑。如果代理是在 VM 啟動之后某一時刻啟動的,則忽略不表示 JAR 文件的路徑。(可選)說白就是agent依賴的類
Can-Redefine-Classes :true表示能重定義此代理所需的類,默認值為 false(可選)
Can-Retransform-Classes :true 表示能重轉換此代理所需的類,默認值為 false (可選)
Can-Set-Native-Method-Prefix: true表示能設置此代理所需的本機方法前綴,默認值為 false(可選)
mainAgent
在 Java SE 6 的 Instrumentation 當中,提供了一個新的代理操作方法:agentmain,可以在 main 函數開始運行之后再運行。
跟premain函數一樣, 開發者可以編寫一個含有agentmain函數的 Java 類:
//采用attach機制,被代理的目標程序VM有可能很早之前已經啟動,當然其所有類已經被加載完成,
//這個時候需要借助Instrumentation#retransformClasses(Class<?>... classes)
//讓對應的類可以重新轉換,從而激活重新轉換的類執行ClassFileTransformer列表中的回調
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)
agentMain 主要用於對java程序的監控,調用java進程,將自己編寫的agentMain 注入目標完成對程序的監控,修改。
創建agentmain
public class TestMainAgent {
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("loadagent after main run.args=" + agentArgs);
Class<?>[] classes = instrumentation.getAllLoadedClasses();
for (Class<?> cls : classes){
System.out.println(cls.getName());
}System.out.println("agent run completely.");
}static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("premain load Class:" + className);
return classfileBuffer;
}
}
}
添加maven插件打包
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-jar-plugin</artifactId>
- <version>3.1.0</version>
- <configuration>
- <archive>
- <!--自動添加META-INF/MANIFEST.MF -->
- <manifest>
- <addClasspath>true</addClasspath>
- </manifest>
- <manifestEntries>
- <Agent-Class>com.tttiger.TestMainAgent</Agent-Class>
- <Can-Redefine-Classes>true</Can-Redefine-Classes>
- <Can-Retransform-Classes>true</Can-Retransform-Classes>
- </manifestEntries>
- </archive>
- </configuration>
- </plugin>
- </plugins>
- </build>
測試agentMain插樁到其他類
另外啟用了一個jvm進程,找到需要attach的jvm進程,讓它加載agentMain,那么agentMain就會被加載到對方jvm執行。arthas就是使用這種方式attach進jvm進程,開啟一個socket然后進行目標jvm的監控。
public static void main(String[] args) throws IOException, AttachNotSupportedException,
AgentLoadException, AgentInitializationException,InterruptedException {
//獲取當前系統中所有 運行中的 虛擬機
System.out.println("running JVM start ");
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
//如果虛擬機的名稱為 xxx 則 該虛擬機為目標虛擬機,獲取該虛擬機的 pid
//然后加載 agent.jar 發送給該虛擬機
System.out.println(vmd.displayName());
if (vmd.displayName().endsWith("com.tttiger.TestJVM")) {
System.out.println(vmd.id());
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("e:/test-agentMain-1.0-SNAPSHOT.jar");
virtualMachine.detach();
System.out.println("attach");
}
}
Thread.sleep(10000L);
}
VirtualMachine 字面意義表示一個Java 虛擬機,也就是程序需要監控的目標虛擬機,提供了獲取系統信息(比如獲取內存dump、線程dump,類信息統計(比如已加載的類以及實例個數等), loadAgent,Attach 和 Detach (Attach 動作的相反行為,從 JVM 上面解除一個代理)等方法,可以實現的功能可以說非常之強大 。該類允許我們通過給attach方法傳入一個jvm的pid(進程id),遠程連接到jvm上 。
代理類注入操作只是它眾多功能中的一個,通過loadAgent方法向jvm注冊一個代理程序agent,在該agent的代理程序中會得到一個Instrumentation實例,該實例可以 在class加載前改變class的字節碼,也可以在class加載后重新加載。在調用Instrumentation實例的方法時,這些方法會使用ClassFileTransformer接口中提供的方法進行處理。
VirtualMachineDescriptor 則是一個描述虛擬機的容器類,配合 VirtualMachine 類完成各種功能
通過VirtualMachine類的attach(pid)方法,便可以attach到一個運行中的java進程上,之后便可以通過loadAgent(agentJarPath)來將agent的jar包注入到對應的進程,然后對應的進程會調用agentmain方法。
Instrumentation的局限性
大多數情況下,我們使用Instrumentation都是使用其字節碼插樁的功能,或者籠統說就是類重定義(Class Redefine)的功能,但是有以下的局限性:
premain和agentmain兩種方式修改字節碼的時機都是類文件加載之后,也就是說必須要帶有Class類型的參數,不能通過字節碼文件和自定義的類名重新定義一個本來不存在的類。
類的字節碼修改稱為類轉換(Class Transform),類轉換其實最終都回歸到類重定義Instrumentation#redefineClasses()方法,此方法有以下限制:
1 新類和老類的父類必須相同;
2 新類和老類實現的接口數也要相同,並且是相同的接口;
3 新類和老類訪問符必須一致。 新類和老類字段數和字段名要一致;
4 新類和老類新增或刪除的方法必須是private static/final修飾的;
5 可以修改方法體。
除了上面的方式,如果想要重新定義一個類,可以考慮基於類加載器隔離的方式:創建一個新的自定義類加載器去通過新的字節碼去定義一個全新的類,不過也存在只能通過反射調用該全新類的局限性。