深入探索 Java 熱部署

developerworks發表於2013-09-25

在 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 修改位元組碼

ASM 修改位元組碼檔案的流程是一個責任鏈模式,首先使用一個 ClassReader 讀入位元組碼,然後利用 ClassVisitor 做個性化的修改,最後利用 ClassWriter 輸出修改後的位元組碼。

之前提過,需要將讀取的 class 檔案的類名做一些修改,載入成一個全新名字的派生類。這裡將之分為了 2 個步驟。

第一步,先將原來的類變成介面。

清單 2. 重定義的原始類

首先 load 原始類的 class 檔案,此處定義了一個增強元件 ClassModifier,作用是修改原始類的型別,將它轉換成介面。原始類的所有方法邏輯都會被去掉。

第二步,生成的派生類都實現這個介面,即原始類,並且複製原始類中的所有方法邏輯。之後如果該類需要更新,會生成一個新的派生類,也會實現這個介面。這樣做的目的是不論如何修改,同一個 class 的派生類都有一個共同的介面,他們之間的轉換變得對外不透明。

清單 3. 定義一個派生類

再次 load 原始類的 class 檔案,此處定義了兩個增強元件,一個是 EnhancedModifier,這個增強元件的作用是改變原有的類名。第二個增強元件是 ExtendModifier,這個增強元件的作用是改變原有類的父類,讓這個修改後的派生類能夠實現同一個原始類(此時原始類已經轉成介面了)。

自定義 classloader 還有一個作用是監聽會發生改變的 class 檔案,classloader 會管理一個定時器,定時依次掃描這些 class 檔案是否改變。

 

改變建立物件的行為

Java 虛擬機器常見的建立物件的方法有兩種,一種是靜態建立,直接 new 一個物件,一種是動態建立,通過反射的方法,建立物件。

由於已經在自定義載入器中更改了原有類的型別,把它從類改成了介面,所以這兩種建立方法都無法成立。我們要做的是將例項化原始類的行為變成例項化派生類。

對於第一種方法,需要做的是將靜態建立,變為通過 classloader 獲取 class,然後動態建立該物件。

清單 4. 替換後的指令集所對應的邏輯

這裡又需要用到 ASM 來修改 class 檔案了。查詢到所有 new 物件的語句,替換成通過 classloader 的形式來獲取物件的形式。

清單 5. 利用 ASM 修改方法體

對於第二種建立方法,需要通過修改 Class.forName()和 ClassLoader.findClass()的行為,使他們通過自定義載入器載入類。

 

使用 JavaAgent 攔截預設載入器的行為

之前實現的類載入器已經解決了熱部署所需要的功能,可是 JVM 啟動時,並不會用自定義的載入器載入 classpath 下的所有 class 檔案,取而代之的是通過應用載入器去載入。如果在其之後用自定義載入器重新載入已經載入的 class,有可能會出現 LinkageError 的 exception。所以必須在應用啟動之前,重新替換已經載入的 class。如果在 jdk1.4 之前,能使用的方法只有一種,改變 jdk 中 classloader 的載入行為,使它指向自定義載入器的載入行為。好在 jdk5.0 之後,我們有了另一種侵略性更小的辦法,這就是 JavaAgent 方法,JavaAgent 可以在 JVM 啟動之後,應用啟動之前的短暫間隙,提供空間給使用者做一些特殊行為。比較常見的應用,是利用 JavaAgent 做面向方面的程式設計,在方法間加入監控日誌等。

JavaAgent 的實現很容易,只要在一個類裡面,定義一個 premain 的方法。

清單 6. 一個簡單的 JavaAgent

然後編寫一個 manifest 檔案,將 Premain-Class屬性設定成定義一個擁有 premain方法的類名即可。

生成一個包含這個 manifest 檔案的 jar 包。

最後需要在執行應用的引數中增加 -javaagent引數 , 加入這個 jar。同時可以為 Javaagent增加引數,下圖中的引數是測試程式碼中 test project 的絕對路徑。這樣在執行應用的之前,會優先執行 premain方法中的邏輯,並且預解析需要載入的 class。

圖 1. 增加執行引數

image001 2013-9-25

 

這裡利用 JavaAgent替換原始位元組碼,阻止原始位元組碼被 Java 虛擬機器載入。只需要實現 一個 ClassFileTransformer的介面,利用這個實現類完成 class 替換的功能。

清單 7. 替換 class

至此,所有的工作大功告成,欣賞一下 hotswap 的結果吧。

圖 2. Test 執行結果

image002 2013-9-25

 

結束語

解決 hotswap 是個困難的課題,本文解決的僅僅是讓新例項化的物件使用新的邏輯,並不能改變已經例項化物件的行為,如果 JVM 能夠重新設計 class 的生命週期,支援執行時重新更新一個 class,hotswap 就會成為 Java 的一個閃亮新特性。官方的 JVM 一直沒有解決熱部署這個問題,可能也是由於無法完全克服其中的諸多難點,希望未來的 Jdk 能解決這個問題,讓 Java 應用對於更新更友好,避免不斷重啟應用浪費的時間。

相關文章