天天看點

深入探索 Java 熱部署

在 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,cglib。本文使用的是 asm。先來介紹一下 class 檔案的結構,class 檔案包含了以下幾類資訊,一個是類的基本資訊,包含了通路權限資訊,類名資訊,父類資訊,接口資訊。第二個是類的變量資訊。第三個是方法的資訊。asm 會先加載一個 class 檔案,然後嚴格順序讀取類的各項資訊,使用者可以按照自己的意願定義增強元件修改這些資訊,最後輸出成一個新的 class。

首先看一下如何利用 asm 修改類資訊。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

<code>classwriter cw =</code> <code>new</code> <code>classwriter(classwriter.compute_maxs);</code>

<code>       </code><code>classreader cr =</code> <code>null</code><code>;    </code>

<code>       </code><code>string enhancedclassname = classsource.getenhancedname();</code>

<code>       </code><code>try</code> <code>{</code>

<code>           </code><code>cr =</code> <code>new</code> <code>classreader(</code><code>new</code> <code>fileinputstream(</code>

<code>                   </code><code>classsource.getfile()));</code>

<code>       </code><code>}</code> <code>catch</code> <code>(ioexception e) {</code>

<code>           </code><code>e.printstacktrace();</code>

<code>           </code><code>return</code> <code>null</code><code>;</code>

<code>       </code><code>}</code>

<code>       </code><code>classvisitor cv =</code> <code>new</code> <code>enhancedmodifier(cw,</code>

<code>               </code><code>classname.replace(</code><code>"."</code><code>,</code> <code>"/"</code><code>),</code>

<code>               </code><code>enhancedclassname.replace(</code><code>"."</code><code>,</code> <code>"/"</code><code>));</code>

<code>       </code><code>cr.accept(cv,</code> <code>0</code><code>);</code>

asm 修改位元組碼檔案的流程是一個責任鍊模式,首先使用一個 classreader 讀入位元組碼,然後利用 classvisitor 做個性化的修改,最後利用 classwriter 輸出修改後的位元組碼。

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

第一步,先将原來的類變成接口。

15

16

17

18

<code>   </code><code>public</code> <code>class&lt;?&gt; redefineclass(string classname){</code>

<code>       </code><code>classwriter cw =</code> <code>new</code> <code>classwriter(classwriter.compute_maxs);</code>

<code>       </code><code>classreader cr =</code> <code>null</code><code>;</code>

<code>       </code><code>classsource cs = classfiles.get(classname);</code>

<code>       </code><code>if</code><code>(cs==</code><code>null</code><code>){</code>

<code>           </code><code>cr =</code> <code>new</code> <code>classreader(</code><code>new</code> <code>fileinputstream(cs.getfile()));</code>

<code>       </code><code>classmodifier cm =</code> <code>new</code> <code>classmodifier(cw);</code>

<code>       </code><code>cr.accept(cm,</code> <code>0</code><code>);</code>

<code>       </code><code>byte</code><code>[] code = cw.tobytearray();</code>

<code>       </code><code>return</code> <code>defineclass(classname, code,</code> <code>0</code><code>, code.length);</code>

<code>}</code>

首先 load 原始類的 class 檔案,此處定義了一個增強元件 <code>classmodifier</code>,作用是修改原始類的類型,将它轉換成接口。原始類的所有方法邏輯都會被去掉。

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

19

20

21

22

23

24

<code>   </code><code>// 在 class 檔案發生改變時重新定義這個類</code>

<code>   </code><code>private</code> <code>class&lt;?&gt; redefineclass(string classname, classsource classsource){</code>

<code>       </code><code>classsource.update();</code>

<code>       </code><code>string enhancedclassname = classsource.getenhancedname();      </code>

<code>           </code><code>cr =</code> <code>new</code> <code>classreader(</code>

<code>                   </code><code>new</code> <code>fileinputstream(classsource.getfile()));</code>

<code>       </code><code>enhancedmodifier em =</code> <code>new</code> <code>enhancedmodifier(cw, classname.replace(</code><code>"."</code><code>,</code> <code>"/"</code><code>),</code>

<code>       </code><code>extendmodifier exm =</code> <code>new</code> <code>extendmodifier(em, classname.replace(</code><code>"."</code><code>,</code> <code>"/"</code><code>),</code>

<code>       </code><code>cr.accept(exm,</code> <code>0</code><code>);</code>

<code>       </code><code>classsource.setbytecopy(code);</code>

<code>       </code><code>class&lt;?&gt; clazz = defineclass(enhancedclassname, code,</code> <code>0</code><code>, code.length);</code>

<code>       </code><code>classsource.setclasscopy(clazz);</code>

<code>       </code><code>return</code> <code>clazz;</code>

再次 load 原始類的 class 檔案,此處定義了兩個增強元件,一個是 <code>enhancedmodifier</code>,這個增強元件的作用是改變原有的類名。第二個增強元件是 <code>extendmodifier</code>,這個增強元件的作用是改變原有類的父類,讓這個修改後的派生類能夠實作同一個原始類(此時原始類已經轉成接口了)。

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

java 虛拟機常見的建立對象的方法有兩種,一種是靜态建立,直接 new 一個對象,一種是動态建立,通過反射的方法,建立對象。

由于已經在自定義加載器中更改了原有類的類型,把它從類改成了接口,是以這兩種建立方法都無法成立。我們要做的是将執行個體化原始類的行為變成執行個體化派生類。

對于第一種方法,需要做的是将靜态建立,變為通過 classloader 擷取 class,然後動态建立該對象。

<code>// 原始邏輯  </code>

<code>  </code><code>greeter p =</code> <code>new</code> <code>greeter();</code>

<code>// 改變後的邏輯</code>

<code>  </code><code>igreeter p = (igreeter)myclassloader.getinstance().</code>

<code>  </code><code>findclass(</code><code>"com.example.greeter"</code><code>).newinstance();</code>

這裡又需要用到 asm 來修改 class 檔案了。查找到所有 new 對象的語句,替換成通過 classloader 的形式來擷取對象的形式。

25

26

<code>@override</code>

<code>public</code> <code>void</code> <code>visittypeinsn(</code><code>int</code> <code>opcode, string type) {</code>

<code>    </code><code>if</code><code>(opcode==opcodes.new &amp;&amp; type.equals(classname)){</code>

<code>        </code><code>list&lt;localvariablenode&gt; variables = node.localvariables;</code>

<code>        </code><code>string compiletype =</code> <code>null</code><code>;</code>

<code>        </code><code>for</code><code>(</code><code>int</code> <code>i=</code><code>0</code><code>;i&lt;variables.size();i++){</code>

<code>            </code><code>localvariablenode localvariable = variables.get(i);</code>

<code>            </code><code>compiletype = formtype(localvariable.desc);</code>

<code>            </code><code>if</code><code>(matchtype(compiletype)&amp;&amp;!valiableindexused[i]){</code>

<code>                </code><code>valiableindexused[i] =</code> <code>true</code><code>;</code>

<code>                </code><code>break</code><code>;</code>

<code>            </code><code>}</code>

<code>        </code><code>}</code>

<code>    </code><code>mv.visitmethodinsn(opcodes.invokestatic, classload_type,</code>

<code>        </code><code>"getinstance"</code><code>,</code> <code>"()l"</code><code>+classload_type+</code><code>";"</code><code>);</code>

<code>    </code><code>mv.visitldcinsn(type.replace(</code><code>"/"</code><code>,</code> <code>"."</code><code>));</code>

<code>    </code><code>mv.visitmethodinsn(opcodes.invokevirtual, classload_type,</code>

<code>        </code><code>"findclass"</code><code>,</code> <code>"(ljava/lang/string;)ljava/lang/class;"</code><code>);</code>

<code>    </code><code>mv.visitmethodinsn(opcodes.invokevirtual,</code> <code>"java/lang/class"</code><code>,</code>

<code>        </code><code>"newinstance"</code><code>,</code> <code>"()ljava/lang/object;"</code><code>);</code>

<code>    </code><code>mv.visittypeinsn(opcodes.checkcast, compiletype);</code>

<code>    </code><code>flag =</code> <code>true</code><code>;</code>

<code>    </code><code>}</code> <code>else</code> <code>{</code>

<code>        </code><code>mv.visittypeinsn(opcode, type);</code>

<code>    </code><code>}</code>

<code> </code><code>}</code>

對于第二種建立方法,需要通過修改 <code>class.forname()</code>和 <code>classloader.findclass()</code>的行為,使他們通過自定義加載器加載類。

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

javaagent 的實作很容易,隻要在一個類裡面,定義一個 premain 的方法。

<code>public</code> <code>class</code> <code>reloadagent {</code>

<code>   </code><code>public</code> <code>static</code> <code>void</code> <code>premain(string agentargs, instrumentation inst){</code>

<code>       </code><code>generaltransformer trans =</code> <code>new</code> <code>generaltransformer();</code>

<code>       </code><code>inst.addtransformer(trans);</code>

<code>   </code><code>}</code>

然後編寫一個 manifest 檔案,将 <code>premain-class</code>屬性設定成定義一個擁有 <code>premain</code>方法的類名即可。

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

<code>manifest-version: 1.0</code>

<code> </code><code>premain-class: com.example.reloadagent</code>

<code> </code><code>can-redefine-classes: true</code>

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

深入探索 Java 熱部署

這裡利用 <code>javaagent</code>替換原始位元組碼,阻止原始位元組碼被 java 虛拟機加載。隻需要實作 <code>一個 classfiletransformer</code>的接口,利用這個實作類完成 class 替換的功能。

<code>public</code> <code>byte</code> <code>[] transform(classloader paramclassloader, string paramstring,</code>

<code>     </code><code>class&lt;?&gt; paramclass, protectiondomain paramprotectiondomain,</code>

<code>     </code><code>byte</code> <code>[] paramarrayofbyte)</code> <code>throws</code> <code>illegalclassformatexception {</code>

<code>    </code><code>string classname = paramstring.replace(</code><code>"/"</code><code>,</code> <code>"."</code><code>);</code>

<code>    </code><code>if</code><code>(classname.equals(</code><code>"com.example.test"</code><code>)){</code>

<code>        </code><code>myclassloader cl = myclassloader.getinstance();</code>

<code>        </code><code>cl.definereference(classname,</code> <code>"com.example.greeter"</code><code>);</code>

<code>        </code><code>return</code> <code>cl.getbytecode(classname);</code>

<code>    </code><code>}</code><code>else</code> <code>if</code><code>(classname.equals(</code><code>"com.example.greeter"</code><code>)){</code>

<code>        </code><code>cl.redefineclass(classname);</code>

<code>    </code><code>return</code> <code>null</code><code>;</code>

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

解決 hotswap 是個困難的課題,本文解決的僅僅是讓新執行個體化的對象使用新的邏輯,并不能改變已經執行個體化對象的行為,如果 jvm 能夠重新設計 class 的生命周期,支援運作時重新更新一個 class,hotswap 就會成為 java 的一個閃亮新特性。官方的 jvm 一直沒有解決熱部署這個問題,可能也是由于無法完全克服其中的諸多難點,希望未來的 jdk 能解決這個問題,讓 java 應用對于更新更友好,避免不斷重新開機應用浪費的時間。

特别說明:尊重作者的勞動成果,轉載請注明出處哦~~~http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt231