利用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