在 Java 開發領域,熱部署一直是一個難以解決的問題,目前的 Java 虛擬機只能實現方法體的修改熱部署,對於整個類的結構修改,仍然須要重啓虛擬機,對類從新加載才能完成更新操做。對於某些大型的應用來講,每次的重啓都須要花費大量的時間成本。雖然 osgi 架構的出現,讓模塊重啓成爲可能,可是若是模塊之間有調用關係的話,這樣的操做依然會讓應用出現短暫的功能性休克。本文將探索如何在不破壞 Java 虛擬機現有行爲的前提下,實現某個單一類的熱部署,讓系統無需重啓就完成某個類的更新。java
首先談一下何爲熱部署(hotswap),熱部署是在不重啓 Java 虛擬機的前提下,能自動偵測到 class 文件的變化,更新運行時 class 的行爲。Java 類是經過 Java 虛擬機加載的,某個類的 class 文件在被 classloader 加載後,會生成對應的 Class 對象,以後就能夠建立該類的實例。默認的虛擬機行爲只會在啓動時加載類,若是後期有一個類須要更新的話,單純替換編譯的 class 文件,Java 虛擬機是不會更新正在運行的 class。若是要實現熱部署,最根本的方式是修改虛擬機的源代碼,改變 classloader 的加載行爲,使虛擬機能監聽 class 文件的更新,從新加載 class 文件,這樣的行爲破壞性很大,爲後續的 JVM 升級埋下了一個大坑。node
另外一種友好的方法是建立本身的 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 文件。ide
利用 ASM 修改 class 文件測試
能夠修改字節碼的框架有不少,好比 ASM,CGLIB。本文使用的是 ASM。先來介紹一下 class 文件的結構,class 文件包含了如下幾類信息:spa
第一個是類的基本信息,包含了訪問權限信息,類名信息,父類信息,接口信息。
第二個是類的變量信息。
第三個是方法的信息。日誌
ASM 會先加載一個 class 文件,而後嚴格順序讀取類的各項信息,用戶能夠按照本身的意願定義加強組件修改這些信息,最後輸出成一個新的 class。code
首先看一下如何利用 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();