探秘 Java 熱部署三(Java agent agentmain)
前言
讓我們繼續探秘 Java 熱部署。在前文 探秘 Java 熱部署二(Java agent premain)中,我們介紹了 Java agent premain。通過在main方法之前通過類似 AOP 的方式添加 premain 方法,我們可以在類加載之前做修改字節碼的操作,無論是第一次加載,還是每次新的 ClassLoader 加載,都會經過 ClassFileTransformer 的 transform 方法,也就是說,都可以在這個方法中修改字節碼,雖然他的方法名是 premain ,但是我們確實可以利用這個特性做這個事情。
在文章的最後,我們也提到了,雖然相比較在自定義類中修改字節碼,premain 沒有什麽侵入性,對業務透明,但是美中不足的是,他還需要在啟動的時候增加參數。
我們還提到了,premain 雖然可以熱部署,但是還需要重新創建類加載器,雖然,這的確也符合 JVM 關於類的唯一性的定義。但是,有一種情況,如果使用的是系統類加載器,我們也無法創建新的ClassLoader對象。那麽我們也就無法重新加載類了,怎麽辦呢?還好 Java 6 為我們提供了一種方法,也就是今天的主角 agentmain。
1. 什麽是 agentmain?
和 premain 師出同門,我們知道,premain 只能在類加載之前修改字節碼,類加載之後無能為力,只能通過重新創建ClassLoader 這種方式重新加載。而 agentmain 就是為了彌補這種缺點而誕生的。簡而言之,agentmain 可以在類加載之後再次加載一個類,也就是重定義,你就可以通過在重定義的時候進行修改類了,甚至不需要創建新的類加載器,JVM 已經在內部對類進行了重定義(重定義的過程相當復雜)。
但是這種方式對類的修改是由限制的,對比原來的老類,由如下要求:
1.父類是同一個;
- 實習那的接口數也要相同;
- 類訪問符必須一致;
- 字段數和字段名必須一致;
- 新增的方法必須是 private static/final 的;
- 可以刪除修改方法;
可以看的出來,相比較重新創建類加載器,限制還是挺多的,最重要的字段是無法修改的。因此,使用的時候要註意。
但是,agentmain 還有一個強大的特點,就是目標程序什麽都不需要管,就能夠被代理。還記得 premain 是如何使用的嗎?需要在目標應用啟動的時候增加 -javaagent 參數。雖說沒有侵入性,但相比 agentmain 而言,還是有侵入性的,畢竟 agentmain 什麽都不要。目標程序獨立運行,什麽都不用管。
那我們就來試試吧!
2. 如何使用?
agentmain 使用步驟如下:
- 定義一個MANIFEST.MF 文件,文件中必須包含 Agent-Class;
- 創建一個 Agent-Class 指定的類,該類必須包含 agentmain 方法(參數和 premian 相同)。
- 將MANIFEST.MF 和 Agent 類打成 jar 包;
- 將 jar 包載入目標虛擬機。目標虛擬機將會自動執行 agentmain 方法執行方法邏輯,同時,ClassFileTransformer 也會長期有效,在每一個類加載器加載 Class 的時候都會攔截。
讓我們按著步驟來一遍吧!
- 定義一個MANIFEST.MF 文件,文件中必須包含 Agent-Class;
Manifest-Version: 1.0
Can-Redefine-Classes: true
Agent-Class: cn.think.in.java.clazz.loader.asm.agent.AgentMainTraceAgent
Can-Retransform-Classes: true
- 創建一個 Agent-Class 指定的類,該類必須包含 agentmain 方法(參數和 premian 相同)。
public class AgentMainTraceAgent {
public static void agentmain(String agentArgs, Instrumentation inst)
throws UnmodifiableClassException {
System.out.println("Agent Main called");
System.out.println("agentArgs : " + agentArgs);
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
System.out.println("agentmain load Class :" + className);
return classfileBuffer;
}
}, true);
inst.retransformClasses(Account.class);
}
上面的類邏輯很簡單,打印 Agent Main called,並打印參數,然後添加一個類轉換器,轉換器中的邏輯只是打印字符串,為了簡單起見,並沒有修改字節碼(各位可自行使用ASM 等類庫修改)。最後一點很重要,執行了 inst.retransformClasses(Account.class); 這段代碼的意思是,重新轉換目標類,也就是 Account 類。也就是說,你需要重新定義哪個類,需要指定,否則 JVM 不可能知道。還有一個類似的方法 redefineClasses ,註意,這個方法是在類加載前使用的。類加載後需要使用 retransformClasses 方法。
- 將MANIFEST.MF 和 Agent 類打成 jar 包;
這個請自行 google。maven 或者 ide 都可以。 - 將 jar 包載入目標虛擬機。目標虛擬機將會自動執行 agentmain 方法執行方法邏輯,同時,ClassFileTransformer 也會長期有效,在每一個類加載器加載 Class 的時候都會攔截。
這段代碼很重要:
class JVMTIThread {
public static void main(String[] args)
throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
if (vmd.displayName().endsWith("AccountMain")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("E:\\self\\demo\\out\\artifacts\\test\\test.jar ", "cxs");
System.out.println("ok");
virtualMachine.detach();
}
}
}
}
註意:寫這段代碼的時候 IDE 可能提示找不到 jar 包,這時候將 jdk/lib/tools.jar 添加的項目的 classpath 中,具體請自行 google。
該main方法步驟如下:
- 獲取當前系統所有的虛擬機,類似 jps 命令。
- 循環所有虛擬機,找到 AccountMain 虛擬機。
- 將當前JVM 鏈接上目標JVM,並加載 loadAgent jar 包且傳遞參數。
- 卸載虛擬機。
如何測試呢?
首先看測試類,也就是AccountMain類:
class AccountMain {
public static void main(String[] args) throws InterruptedException {
for (;;) {
new Account().operation();
Thread.sleep(5000);
}
}
}
當我們一切準備就緒,啟動 AccountMain 後,再啟動 JVMTIThread 類,結果如下:
可以看到,執行了1遍 operation方法後,我們啟動了 attach jvm,隨後,agentmain 方法就被執行了,並打印了我們我們傳遞的字符串參數,同時也進入到了 ClassFileTransformer 方法中,表示重新加載了 Account 類。有點遺憾的是,限於篇幅,我們沒有修改字節碼。不過樓主還是展示一下樓主修改字節碼的結果吧,假設樓主想統計 operation 方法的時長,樓主將使用 ASM 增加一段統計時長的代碼:
public class TimeStat {
static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void start() {
threadLocal.set(System.currentTimeMillis());
}
public static void end() {
long time = System.currentTimeMillis() - threadLocal.get();
System.out.print(Thread.currentThread().getStackTrace()[2] + "方法耗費時間: ");
System.out.println(time + "毫秒");
}
}
然後修改 agentmain 方法中ClassFileTransformer 的transform 邏輯,也就是在這裏修改字節碼。
運行結果如下:
可以看到,首先重定義了 Account 類,又主動加載了 TimeStat 類,然後生效,在 operation 字符串後面打印了方法的耗時。
總結
通過對 agentmain 的使用,我們感受到了他的強大,在目標程序絲毫不改動的,甚至連啟動參數都不加的情況下,可以修改類,並且是運行後修改,而且不重新創建類加載器。其主要得益於 JVM 底層的對類的重定義,關於底層代碼解釋,Jvm 大神寒泉子有篇文章 JVM源碼分析之javaagent原理完全解讀 ,詳細分析了 javaagent 的原理。但 agentmain 有一些功能上的限制,比如字段不能修改增減。所以,使用的時候需要權衡,到底使用哪種方式實現熱部署。
說了這麽久的熱部署,其實就是動態或者說運行時修改類,大的方向說有2種方式:
- 使用 agentmain,不需要重新創建類加載器,可直接修改類,但是有很多限制。
- 使用 premain 可以在類第一次加載之前修改,加載之後修改需要重新創建類加載器。或者在自定義的類加載器種修改,但這種方式比較耦合。
無論是哪種,都需要字節碼修改的庫,比如ASM,javassist ,cglib 等,很多。總之,通過java.lang.instrument 包配合字節碼庫,可以很方便的動態修改類,或者進行熱部署。
good luck!!!!!
參考: JVM源碼分析之javaagent原理完全解讀
《實戰Java 虛擬機》
javaagent
Instrumentation 新功能
探秘 Java 熱部署三(Java agent agentmain)