在 Java SE 5 以後,使用 Instrumentation,使得開發者可以構建一個獨立於應用程序的代理程序(Agent),用來監測和協助運行在 JVM 上的程序,甚至能夠替換和修改某些類的定義。有了這樣的功能,開發者就可以實現更爲靈活的運行時虛擬機監控和 Java 類操作了,這樣的特性實際上提供了一種虛擬機級別支持的 AOP 實現方式,使得開發者無需對 JDK 做任何升級和改動,就可以實現某些 AOP 的功能了。
在 Java SE 5 中,利用 java.lang.instrument 做靜態 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能從本地代碼中解放出來,使之可以用 Java 代碼的方式解決問題。
在 Java SE 6 裏面,instrumentation 包被賦予了更強大的功能:啓動後的 instrument、本地代碼(native code)instrument,以及動態改變 classpath 等等。這些改變,意味着 Java 具有了更強的動態控制、解釋能力,它使得 Java 語言變得更加靈活多變。
在 Java SE 5 中,Instrument 要求在運行前利用命令行參數或者系統參數來設置代理類。但在實際的運行之中,虛擬機在初始化之時(在絕大多數的 Java 類庫被載入之前),instrumentation 的設置已經啓動,並在虛擬機中設置了回調函數,檢測特定類的加載情況,並完成實際工作。但是在實際的很多的情況下,我們沒有辦法在虛擬機啓動之時就爲其設定代理,這樣實際上限制了 instrument 的應用。而 Java SE 6 改變了這種情況,通過 Java Tool API 中的 attach 方式,我們可以很方便地在運行過程中動態地設置加載代理類,以達到 instrumentation 的目的。
Instrumentation 的最大作用,就是類定義動態改變和操作。java.lang.instrument 包的實現,是基於JVMTI機制的:在 Instrumentation 的實現當中,存在一個 JVMTI 的代理程序,通過調用 JVMTI 當中 Java 類相關的函數來完成Java 類的動態操作。除開 Instrumentation 功能外,JVMTI 還在虛擬機內存管理,線程控制,方法和變量操作等方面提供了大量有價值的函數。
JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虛擬機提供的,爲 JVM 相關的工具提供的本地編程接口集合。JVMTI 是從 Java SE 5 開始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已經消失了。JVMTI 提供了一套“代理”程序機制,可以支持第三方工具程序以代理的方式連接和訪問 JVM,並利用 JVMTI 提供的豐富的編程接口,完成很多跟 JVM 相關的功能。
1. 使用
Agent分爲兩種,一種是在主程序之前運行的Agent,一種是在主程序之後運行的Agent(前者的升級版,1.6以後提供)。
- 在一個普通 Java 程序(帶有 main 函數的 Java 類)運行時,通過 -javaagent 參數指定一個特定的 jar 文件(包含 Instrumentation 代理)來啓動 Instrumentation 的代理程序。
- 在一個普通 Java 程序(帶有 main 函數的 Java 類)運行時,通過 Java Tool API 中的 attach 方式指定進程id和特定jar包地址,啓動 Instrumentation 的代理程序。
這裏測試使用 javaagent 替換類。
1.1 JVM啓動前靜態 Instrument
定義一個User類,getName()返回admin。
public class User {
public String getName() {
return "admin";
}
}
主程序,這裏的程序就是我們要代理的程序。創建User對象,調用getName()方法。
public class Main {
public static void main(String[] args) {
System.out.println("main start");
System.out.println("main args :" + Arrays.toString(args));
System.out.println("new User().getName() :" + new User().getName());
System.out.println("main end");
}
}
將User類的getName()方法返回值改爲user,使用javac編譯後,重命名編譯文件User.class爲User.class.2,再將User類中的getName()方法返回值改回admin。
實現 ClassFileTransformer 接口,transform 方法則完成了類定義的替換。
public class UserTransformer implements ClassFileTransformer {
// 待替換的類文件名
private static final String USER_CLASS_2 = "User.class.2";
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("className :" + className);
if (!className.contains("User")) {
return null;
}
return getBytesFromFile(USER_CLASS_2);
}
/**
* 根據文件名讀入二進制字符流
* @param fileName
* @return
*/
private byte[] getBytesFromFile(String fileName) {
File file = new File(fileName);
try (
InputStream inputStream = new FileInputStream(file);
){
long length = file.length();
byte[] bytes = new byte[(int) length];
int offset = 0;
int numRead = 0;
while (offset < bytes.length &&
(numRead = inputStream.read(bytes, offset, bytes.length - offset)) >= 0) {
offset += numRead;
}
if (offset < bytes.length) {
throw new IOException("無法完全讀取文件 " + file.getName());
}
return bytes;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
premain方法,代表着他將在主程序的main方法之前運行,agentArgs代表傳遞過來的參數,inst則是agent技術主要使用的API。
agentArgs 是 premain 函數得到的程序參數,隨同 -javaagent 一起傳入。與 main 函數不同的是,這個參數是一個字符串而不是一個字符串數組,如果程序參數有多個,程序將自行解析這個字符串。
Inst 是一個 java.lang.instrument.Instrumentation 的實例,由 JVM 自動傳入。java.lang.instrument.Instrumentation 是 instrument 包中定義的一個接口,也是這個包的核心部分,集中了其中幾乎所有的功能方法,例如類定義的轉換和操作等等。
public class Premain {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain start");
System.out.println("premain args :" + agentArgs);
// 在類加載之前,重新定義 Class 文件
inst.addTransformer(new UserTransformer());
System.out.println("premain end");
}
}
META-INF/MAINIFEST.MF 文件用於描述Jar包的信息,例如指定入口函數等。
需要添加Premain-Class屬性,指定帶有 premain 方法類的全路徑,然後將agent類打成Jar包。
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Main-Class>
com.shpun.Main
</Main-Class>
<!--main之前-->
<Premain-Class>
com.shpun.Premain
</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
測試
未代理,打印admin。
將 User.class.2 和 Jar 包放在同個目錄下測試。
代理後,先執行 premain 方法,然後加載類,每次都經過 transform() 方法。加載完後執行 main 方法,需要加載 User 類,經過transform() 方法,替換User,打印user,代理成功。
1.2 JVM啓動後動態 Instrument
User.class.2,User 類,UserTransformer 類和上面一樣。
修改 Main 方法,這裏判斷 getName() 的值,如果是user才退出。
public class Main {
public static void main(String[] args) throws InterruptedException {
System.out.println("main start");
System.out.println("main args :" + Arrays.toString(args));
while (true) {
Thread.sleep(1000);
String name = new User().getName();
System.out.println("new User().getName() :" + new User().getName());
if ("user".equals(name)) {
break;
}
}
System.out.println("main end");
}
}
agentmain 方法在 main 函數開始運行之後再運行。
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation inst) throws Exception {
System.out.println("agentmain start");
System.out.println("agentmain args :" + agentArgs);
// 增加一個 Class 文件的轉換器,轉換器用於改變 Class 二進制流的數據,
// 參數 canRetransform 設置是否允許重新轉換,爲true才能在運行時替換。
inst.addTransformer(new UserTransformer(), true);
// 在類加載之後,重新定義 Class。對於已經加載過的類,可以執行retransformClasses來重新觸發這個Transformer的攔截。
inst.retransformClasses(User.class);
System.out.println("agentmain end");
}
}
META-INF/MAINIFEST.MF 需要添加 Agent-Class 和 Can-Retransform-Classes 屬性。
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Main-Class>
com.shpun.Main
</Main-Class>
<!--main之後-->
<Agent-Class>
com.shpun.AgentMain
</Agent-Class>
<Can-Retransform-Classes>
true
</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
測試
使用 agentmain,需要通過 Attach API 。Attach API 不是 Java 的標準 API,而是 Sun 公司提供的一套擴展 API,用來向目標 JVM ”附着”(Attach)代理工具程序的。有了它,開發者可以方便的監控一個 JVM,運行一個外加的代理程序。Jar 包在JAVA_HOME的lib目錄下。
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>G:\Java\jdk1.8.0_181\lib\tools.jar</systemPath>
</dependency>
VirtualMachine 代表一個 Java 虛擬機,也就是程序需要監控的目標虛擬機,提供了 JVM 枚舉,Attach 動作和 Detach 動作(Attach 動作的相反行爲,從 JVM 上面解除一個代理)等等 。該類允許我們通過給 attach() 方法傳入一個jvm的pid(進程id),遠程連接到jvm上 。然後我們可以通過 loadAgent() 方法向jvm註冊一個代理程序agent,在該agent的代理程序中會得到一個Instrumentation實例,該實例可以在 Class 加載前改變 Class 的字節碼,也可以在 Class 加載後重新加載。在調用Instrumentation實例的方法時,這些方法會使用ClassFileTransformer 接口中提供的方法進行處理。
public class AttachAgent {
public static void main (String[] args) throws Exception {
// 通過jps命令,獲取進程號
VirtualMachine virtualMachine = VirtualMachine.attach("19056");
virtualMachine.loadAgent("E:\\IDEA_workspace\\java-agent-test\\java-agent-agentmain\\target\\java-agent-agentmain-1.0-SNAPSHOT.jar", "attach-agent");
virtualMachine.detach();
}
}
將 User.class.2 和 Jar 包放在同個目錄下測試。
運行 Jar 包,先打印的是admin。然後執行AttachAgent,發現agentmain方法被執行了,並且在替換了類,打印user。這個表示 agentmain 已經被 Attach API 成功附着到 JVM 上,代理程序生效了。
參考:
Java Agent簡介
javaagent使用指南
JavaAgent技術
☆基於Java Instrument的Agent實現
☆淺談JPDA中JVMTI模塊
☆JVMTI Agent 工作原理及核心源碼分析
☆JVMTI Attach機制與核心源碼分析
"程序包com.sun.tools.attach不存在"最簡單粗暴的解決方案
agentmain 使用過程中的坑,看看你有沒有遇到