在 Java 開發領域,熱部署一直是一個難以解決的問題,目前的 Java 虛拟機隻能實作方法體的修改熱部署,對于整個類的結構修改,仍然需要重新開機虛拟機,對類重新加載才能完成更新操作。對于某些大型的應用來說,每次的重新開機都需要花費大量的時間成本。雖然 osgi 架構的出現,讓子產品重新開機成為可能,但是如果子產品之間有調用關系的話,這樣的操作依然會讓應用出現短暫的功能性休克。本文将探索如何在不破壞 Java 虛拟機現有行為的前提下,實作某個單一類的熱部署,讓系統無需重新開機就完成某個類的更新。
類加載的探索
首先談一下何為熱部署(hotswap),熱部署是在不重新開機 Java 虛拟機的前提下,能自動偵測到 class 檔案的變化,更新運作時 class 的行為。Java 類是通過 Java 虛拟機加載的,某個類的 class 檔案在被 classloader 加載後,會生成對應的 Class 對象,之後就可以建立該類的執行個體。預設的虛拟機行為隻會在啟動時加載類,如果後期有一個類需要更新的話,單純替換編譯的 class 檔案,Java 虛拟機是不會更新正在運作的 class。如果要實作熱部署,最根本的方式是修改虛拟機的源代碼,改變 classloader 的加載行為,使虛拟機能監聽 class 檔案的更新,重新加載 class 檔案,這樣的行為破壞性很大,為後續的 JVM 更新埋下了一個大坑。
另一種友好的方法是建立自己的
classloader
來加載需要監聽的 class,這樣就能控制類加載的時機,進而實作熱部署。本文将具體探索如何實作這個方案。首先需要了解一下 Java 虛拟機現有的加載機制。目前的加載機制,稱為雙親委派,系統在使用一個
classloader
來加載類時,會先詢問目前
classloader
的父類是否有能力加載,如果父類無法實作加載操作,才會将任務下放到該
classloader
來加載。這種自上而下的加載方式的好處是,讓每個
classloader
執行自己的加載任務,不會重複加載類。但是這種方式卻使加載順序非常難改變,讓自定義
classloader
搶先加載需要監聽改變的類成為了一個難題。
不過我們可以換一個思路,雖然無法搶先加載該類,但是仍然可以用自定義 classloader 建立一個功能相同的類,讓每次執行個體化的對象都指向這個新的類。當這個類的 class 檔案發生改變的時候,再次建立一個更新的類,之後如果系統再次發出執行個體化請求,建立的對象講指向這個全新的類。
下面來簡單列舉一下需要做的工作。
建立自定義的 classloader,加載需要監聽改變的類,在 class 檔案發生改變的時候,重新加載該類。
改變建立對象的行為,使他們在建立時使用自定義 classloader 加載的 class。
自定義加載器的實作
自定義加載器仍然需要執行類加載的功能。這裡卻存在一個問題,同一個類加載器無法同時加載兩個相同名稱的類,由于不論類的結構如何發生變化,生成的類名不會變,而 classloader 隻能在虛拟機停止前銷毀已經加載的類,這樣 classloader 就無法加載更新後的類了。這裡有一個小技巧,讓每次加載的類都儲存成一個帶有版本資訊的 class,比如加載 Test.class 時,儲存在記憶體中的類是 Test_v1.class,當類發生改變時,重新加載的類名是 Test_v2.class。但是真正執行加載 class 檔案建立 class 的 defineClass 方法是一個 native 的方法,修改起來又變得很困難。是以面前還剩一條路,那就是直接修改編譯生成的 class 檔案。
利用 ASM 修改 class 檔案
可以修改位元組碼的架構有很多,比如 ASM,CGLIB。本文使用的是 ASM。先來介紹一下 class 檔案的結構,class 檔案包含了以下幾類資訊:
第一個是類的基本資訊,包含了通路權限資訊,類名資訊,父類資訊,接口資訊。
第二個是類的變量資訊。
第三個是方法的資訊。
ASM 會先加載一個 class 檔案,然後嚴格順序讀取類的各項資訊,使用者可以按照自己的意願定義增強元件修改這些資訊,最後輸出成一個新的 class。
首先看一下如何利用 ASM 修改類資訊。
清單 1. 利用 ASM 修改位元組碼
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassReader cr = null;
String enhancedClassName = classSource.getEnhancedName();
try {
cr = new ClassReader(new FileInputStream(
classSource.getFile()));
} catch (IOException e) {
e.printStackTrace();
return null;
}
ClassVisitor cv = new EnhancedModifier(cw,
className.replace(".", "/"),
enhancedClassName.replace(".", "/"));
cr.accept(cv, 0);
ASM 修改位元組碼檔案的流程是一個責任鍊模式,首先使用一個 ClassReader 讀入位元組碼,然後利用 ClassVisitor 做個性化的修改,最後利用 ClassWriter 輸出修改後的位元組碼。
之前提過,需要将讀取的 class 檔案的類名做一些修改,加載成一個全新名字的派生類。這裡将之分為了 2 個步驟。
第一步,先将原來的類變成接口。
清單 2. 重定義的原始類
public Class<?> redefineClass(String className){
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassReader cr = null;
ClassSource cs = classFiles.get(className);
if(cs==null){
return null;
}
try {
cr = new ClassReader(new FileInputStream(cs.getFile()));
} catch (IOException e) {
e.printStackTrace();
return null;
}
ClassModifier cm = new ClassModifier(cw);
cr.accept(cm, 0);
byte[] code = cw.toByteArray