利用Java Agent進行代碼植入
Java Agent 又叫做 Java 探針,是在 JDK1.5 引入的一種可以動態修改 Java 字節碼的技術。可以把javaagent理解成一種代碼注入的方式。但是這種注入比起spring的aop更加的優美。
Java agent的使用方式有兩種:
- 實現
premain
方法,在JVM啟動前加載。 - 實現
agentmain
方法,在JVM啟動后加載。
premain和agentmain函數聲明如下,方法名相同情況下,擁有Instrumentation inst參數的方法優先級更高:
public static void agentmain(String agentArgs, Instrumentation inst) {
...
}
public static void agentmain(String agentArgs) {
...
}
public static void premain(String agentArgs, Instrumentation inst) {
...
}
public static void premain(String agentArgs) {
...
}
JVM 會優先加載帶 Instrumentation
簽名的方法,加載成功忽略第二種;如果第一種沒有,則加載第二種方法。
-
第一個參數
String agentArgs
就是Java agent的參數。 -
Inst
是一個java.lang.instrument.Instrumentation
的實例,可以用來類定義的轉換和操作等等。
premain方式
JVM啟動時 會先執行 premain
方法,大部分類加載都會通過該方法,注意:是大部分,不是所有。遺漏的主要是系統類,因為很多系統類先於 agent 執行,而用戶類的加載肯定是會被攔截的。也就是說,這個方法是在 main 方法啟動前攔截大部分類的加載活動,既然可以攔截類的加載,就可以結合第三方的字節碼編譯工具,比如ASM,javassist,cglib等等來改寫實現類。
使用實例:
1)創建應用程序Task.jar
先創建一個Task.jar用於模擬在實際場景中的應用程序,Task.java:
public class Task {
public static void main (String[] args) {
System.out.println("task mian run");
}
}
把Task打成jar包:
此jar包可以單獨執行:java -jar Task.jar
2)創建premain方式的Agent
新建一個Agent01.jar,用於在task之前執行:
import java.lang.instrument.Instrumentation;
public class Agent01 {
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("premain run----");
}
}
此時項目如果打成jar包,缺少入口main文件,所以需要自己定義一個MANIFEST.MF
文件,用於指明premain
的入口在哪里:
在src/main/resources/
目錄下創建META-INF/MANIFEST.MF
:
Manifest-Version: 1.0
Premain-Class: com.test.Agent01
注意:最后一行是空行,不能省略。以下是MANIFEST.MF的其他選項
Premain-Class: 包含 premain 方法的類(類的全路徑名)
Agent-Class: 包含 agentmain 方法的類(類的全路徑名)
Boot-Class-Path: 設置引導類加載器搜索的路徑列表。查找類的特定於平台的機制失敗后,引導類加載器會搜索這些路徑。按列出的順序搜索路徑。列表中的路徑由一個或多個空格分開。路徑使用分層 URI 的路徑組件語法。如果該路徑以斜杠字符(“/”)開頭,則為絕對路徑,否則為相對路徑。相對路徑根據代理 JAR 文件的絕對路徑解析。忽略格式不正確的路徑和不存在的路徑。如果代理是在 VM 啟動之后某一時刻啟動的,則忽略不表示 JAR 文件的路徑。(可選)
Can-Redefine-Classes: true表示能重定義此代理所需的類,默認值為 false(可選)
Can-Retransform-Classes: true 表示能重轉換此代理所需的類,默認值為 false (可選)
Can-Set-Native-Method-Prefix: true表示能設置此代理所需的本機方法前綴,默認值為 false(可選)
同樣的打成jar包:
回顧下我們之前單獨運行task.jar時候,控制台前后並沒有打印其他信息
現在我們來使用premain進行注入: java -javaagent:Agent01.jar -jar Task.jar
可以看到premain比task先運行,通過啟動時候指定參數javaagent來達到注入的效果
以下是先知社區師傅的流程圖:
這種方法存在一定的局限性——只能在啟動時使用-javaagent
參數指定。在實際環境中,目標的JVM通常都是已經啟動的狀態,無法預先加載premain。相比之下,agentmain更加實用。
agentmain方式
同樣使用一個案例來說明使用方式
使用實例:
1)創建應用程序Task.jar
和之前的premain方式一樣,創建一個Task.jar作為應用程序:
import java.util.Scanner;
public class Task {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
scanner.hasNext();
}
}
把創建的Task.jar運行起來:java -jar Task.jar
2)創建一個agentmain方式的Agent
創建一個agentmain方式的Agent02.jar,Agent02.java:
import java.lang.instrument.Instrumentation;
public class Agent02 {
public static void agentmain(String agentArgs, Instrumentation inst){
System.out.println("打印全部加載的類:");
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class allLoadedClass : allLoadedClasses) {
System.out.println(allLoadedClass.getName());
}
}
}
同樣生成jar包的話,需要手動定義一個MANIFEST.MF
文件
Manifest-Version: 1.0
Agent-Class: com.test.Agent02
3)利用VirtualMachine注入
使用VirtualMachine
類來利用前面創建的Agent進行代理類注入,VirtualMachine
類在jdk目錄下的lib/tools.jar包,需要手動導入
package com.test;import com.sun.tools.attach.AgentInitializationException;import com.sun.tools.attach.AgentLoadException;import com.sun.tools.attach.AttachNotSupportedException;import com.sun.tools.attach.VirtualMachine;import java.io.IOException;public class App { public static void main( String[] args ) { try { //VirtualMachine 來自tools.jar // VirtualMachine.attach("9444") 9444為線程PID,使用jps查看 VirtualMachine vm = VirtualMachine.attach("9444"); //指定要使用的Agent路徑 vm.loadAgent("C:\\Users\\xxx\\Desktop\\Agent02.jar"); } catch (AttachNotSupportedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (AgentLoadException e) { e.printStackTrace(); } catch (AgentInitializationException e) { e.printStackTrace(); } }}
運行這個名為App的類之后,正在運行的Task程序會執行代碼:
以下是先知社區的圖:
Java Agent 代碼植入
利用agentmain配合Javassist,在方法執行前,修改任意類的方法。在演示之前,先來看幾個知識點。
Instrumentation類
在agentmain的構造函數中,第二個參數就是Instrumentation
public static void agentmain(String agentArgs, Instrumentation inst)
這個類就是用來進行aop操作的類,能夠替換和修改某些類的定義
public interface Instrumentation { // 增加一個 Class 文件的轉換器,轉換器用於改變 Class 二進制流的數據,參數 canRetransform 設置是否允許重新轉換。在類加載之前,重新定義 Class 文件,ClassDefinition 表示對一個類新的定義,如果在類加載之后,需要使用 retransformClasses 方法重新定義。addTransformer方法配置之后,后續的類加載都會被Transformer攔截。對於已經加載過的類,可以執行retransformClasses來重新觸發這個Transformer的攔截。類加載的字節碼被修改后,除非再次被retransform,否則不會恢復。 void addTransformer(ClassFileTransformer transformer); // 刪除一個類轉換器 boolean removeTransformer(ClassFileTransformer transformer); // 在類加載之后,重新定義 Class。這個很重要,該方法是1.6 之后加入的,事實上,該方法是 update 了一個類。 void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; // 判斷目標類是否能夠修改。 boolean isModifiableClass(Class<?> theClass); // 獲取目標已經加載的類。 @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); ......}
其中addTransformer()
和retransformClasses()
用來篡改Class的字節碼。
從源碼中看到addTransformer
方法參數中,第一個參數傳遞的為ClassFileTransformer
類型
ClassFileTransformer接口
這是一個接口,它提供了一個transform
方法:
public interface ClassFileTransformer { default byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { .... }}
接下來就用一個示例來演示利用agentmain配合Javassist進行代碼植入的操作
示例:
1)新建一個hello.jar模擬啟動的應用程序
//HelloWorld.javapublic class HelloWorld { public static void main(String[] args) { System.out.println("start..."); hello h1 = new hello(); h1.hello(); // 產生中斷,等待注入 Scanner sc = new Scanner(System.in); sc.nextInt(); hello h2 = new hello(); h2.hello(); System.out.println("ends..."); }}//hello.javapublic class hello { public void hello(){ System.out.println("hello world"); }}
2)創建javaAgent.jar
//AgentDemo.javapackage com.test;import java.io.IOException;import java.lang.instrument.Instrumentation;import java.lang.instrument.UnmodifiableClassException;public class AgentDemo { public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException { Class[] classes = inst.getAllLoadedClasses(); // 判斷類是否已經加載 for (Class aClass : classes) { if (aClass.getName().equals(TransformerDemo.editClassName)) { // 添加 Transformer inst.addTransformer(new TransformerDemo(), true); // 觸發 Transformer inst.retransformClasses(aClass); } } }}//TransformerDemo.javapackage com.test;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;public class TransformerDemo implements ClassFileTransformer { // 只需要修改這里就能修改別的函數 public static final String editClassName = "com.test.hello"; public static final String editClassName2 = editClassName.replace('.', '/'); public static final String editMethodName = "hello"; @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { try { ClassPool cp = ClassPool.getDefault(); if (classBeingRedefined != null) { ClassClassPath ccp = new ClassClassPath(classBeingRedefined); cp.insertClassPath(ccp); } CtClass ctc = cp.get(editClassName); CtMethod method = ctc.getDeclaredMethod(editMethodName); String source = "{System.out.println(\"hello transformer\");}"; method.insertBefore(source); byte[] bytes = ctc.toBytecode(); ctc.detach(); return bytes; } catch (Exception e){ e.printStackTrace(); } return null; }}
在MANIFEST.MF
文件中加入
Manifest-Version: 1.0Agent-Class: com.test.AgentDemoCan-Redefine-Classes: trueCan-Retransform-Classes: true
3)利用VirtualMachine注入
package com.test;import com.sun.tools.attach.AgentInitializationException;import com.sun.tools.attach.AgentLoadException;import com.sun.tools.attach.AttachNotSupportedException;import com.sun.tools.attach.VirtualMachine;import java.io.IOException;public class App { public static void main( String[] args ) { try { //VirtualMachine 來自tools.jar // VirtualMachine.attach("9444") 9444為線程PID VirtualMachine vm = VirtualMachine.attach("9444"); //指定要使用的Agent路徑 vm.loadAgent("C:\\Users\\xxx\\Desktop\\javaAgent.jar"); } catch (AttachNotSupportedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (AgentLoadException e) { e.printStackTrace(); } catch (AgentInitializationException e) { e.printStackTrace(); } }}
測試:
運行hello.jar
使用VirtualMachine連接VM,進行注入后,第二次調用hello方法已經成功增加了一行hello transformer