類的熱替換是Java在線升級系統設計中的基礎技術,從文中給出的實例來看,構建在線升級系統不僅僅是一個技術問題,還牽扯到很多管理方面的因素,比如:如何管理、部署系統中的可在線升級部分和不可在線升級部分以降低系統的管理、維護成本等.
對於許多關鍵性業務或者龐大的Java系統來說,如果必須暫停系統服務才能進行系統升級,既會大大影響到系統的可用性,同時也增加了系統的管理和維護成本.因此,如果能夠方便地在不停止系統業務的情況下進行系統升級,則可以很好地解決上述問題.
JavaClassLoader技術剖析
要構建在線升級系統,一個重要的技術就是能夠實現Java類的熱替換——也就是在不停止正在運行的系統的情況下進行類(對象)的升級替換.而Java的ClassLoader正是實現這項技術的基礎.
在Java中,類的實例化流程分為兩個部分:類的載入和類的實例化.類的載入又分為顯式載入和隱式載入.大家使用new關鍵字創建類實例時,其實就隱式地包含了類的載入過程.對於類的顯式載入來說,比較常用的是Class.forName.其實,它們都是通過調用ClassLoader類的loadClass方法來完成類的實際載入工作的.直接調用ClassLoader的loadClass方法是另外一種不常用的顯式載入類的技術.
圖1.Java類載入器層次結構圖
ClassLoader在載入類時有一定的層次關係和規則.在Java中,有四種類型的類載入器,分別為:BootStrapClassLoader、ExtClassLoader、AppClassLoader以及用戶自定義的ClassLoader.這四種類載入器分別負責不同路徑的類的載入,並形成了一個類載入的層次結構.
BootStrapClassLoader處於類載入器層次結構的最高層,負責sun.boot.class.path路徑下類的載入,默認為jre/lib目錄下的核心API或-Xbootclasspath選項指定的jar包.ExtClassLoader的載入路徑為java.ext.dirs,默認為jre/lib/ext目錄或者-Djava.ext.dirs指定目錄下的jar包載入.AppClassLoader的載入路徑為java.class.path,默認為環境變數CLASSPATH中設定的值.也可以通過-classpath選型進行指定.用戶自定義ClassLoader可以根據用戶的需要定製自己的類載入過程,在運行期進行指定類的動態實時載入.
這四種類載入器的層次關係圖如圖1所示.一般來說,這四種類載入器會形成一種父子關係,高層為低層的父載入器.在進行類載入時,會自底向上挨個檢查是否已經載入了指定類,如果已經載入則直接返回該類的引用.如果到最高層也沒有載入過指定類,那麼會自頂向下挨個嘗試載入,直到用戶自定義類載入器,如果還不能成功,就會拋出異常.Java類的載入過程如圖2所示.
圖2.Java類的加過程
每個類載入器有自己的名字空間,對於同一個類載入器實例來說,名字相同的類只能存在一個,並且僅載入一次.不管該類有沒有變化,下次再需要載入時,它只是從自己的緩存中直接返回已經載入過的類引用.
我們編寫的應用類默認情況下都是通過AppClassLoader進行載入的.當我們使用new關鍵字或者Class.forName來載入類時,所要載入的類都是由調用new或者Class.forName的類的類載入器(也是AppClassLoader)進行載入的.要想實現Java類的熱替換,必須要實現系統中同名類的不同版本實例的共存,通過上面的介紹我們知道,要想實現同一個類的不同版本的共存,我們必須要通過不同的類載入器來載入該類的不同版本.另外,為了能夠繞過Java類的既定載入過程,我們需要實現自己的類載入器,並在其中對類的載入過程進行完全的控制和管理.
編寫自定義的ClassLoader
為了能夠完全掌控類的載入過程,我們的定製類載入器需要直接從ClassLoader繼承.我們來介紹一下ClassLoader類中和熱替換有關的的一些重要方法.
◆findLoadedClass:每個類載入器都維護有自己的一份已載入類名字空間,其中不能出現兩個同名的類.凡是通過該類載入器載入的類,無論是直接的還是間接的,都保存在自己的名字空間中,該方法就是在該名字空間中尋找指定的類是否已存在,如果存在就返回給類的引用,否則就返回null.這裡的直接是指,存在於該類載入器的載入路徑上並由該載入器完成載入,間接是指,由該類載入器把類的載入工作委託給其他類載入器完成類的實際載入.
◆getSystemClassLoader:Java2中新增的方法.該方法返回系統使用的ClassLoader.可以在自己定製的類載入器中通過該方法把一部分工作轉交給系統類載入器去處理.
◆defineClass:該方法是ClassLoader中非常重要的一個方法,它接收以位元組數組表示的類位元組碼,並把它轉換成Class實例,該方法轉換一個類的同時,會先要求裝載該類的父類以及實現的介面類.
◆loadClass:載入類的入口方法,調用該方法完成類的顯式載入.通過對該方法的重新實現,我們可以完全控制和管理類的載入過程.
◆resolveClass:鏈接一個指定的類.這是一個在某些情況下確保類可用的必要方法,詳見Java語言規範中「執行」一章對該方法的描述.
了解了上面的這些方法,下面我們來實現一個定製的類載入器來完成這樣的載入流程:我們為該類載入器指定一些必須由該類載入器直接載入的類集合,在該類載入器進行類的載入時,如果要載入的類屬於必須由該類載入器載入的集合,那麼就由它直接來完成類的載入,否則就把類載入的工作委託給系統的類載入器完成.
在給出示例代碼前,有兩點內容需要說明一下:1、要想實現同一個類的不同版本的共存,那麼這些不同版本必須由不同的類載入器進行載入,因此就不能把這些類的載入工作委託給系統載入器來完成,它們只有一份.2、為了做到這一點,就不能採用系統默認的類載入器委託規則,也就是說我們定製的類載入器的父載入器必須設置為null.該定製的類載入器的實現代碼如下:
1.class CustomCL extends ClassLoader {
2.
3. private String basedir; // 需要該類載入器直接載入的類文件的基目錄
4. private HashSet dynaclazns; // 需要由該類載入器直接載入的類名
5.
6. public CustomCL(String basedir, String[] clazns) {
7. super(null); // 指定父類載入器為 null
8. this.basedir = basedir;
9. dynaclazns = new HashSet();
10. loadClassByMe(clazns);
11. }
12.
13. private void loadClassByMe(String[] clazns) {
14. for (int i = 0; i < clazns.length; i ) {
15. loadDirectly(clazns[i]);
16. dynaclazns.add(clazns[i]);
17. }
18. }
19.
20. private Class loadDirectly(String name) {
21. Class cls = null;
22. StringBuffer sb = new StringBuffer(basedir);
23. String classname = name.replace('.', File.separatorChar) ".class";
24. sb.append(File.separator classname);
25. File classF = new File(sb.toString());
26. cls = instantiateClass(name,new FileInputStream(classF),
27. classF.length());
28. return cls;
29. }
30.
31. private Class instantiateClass(String name,InputStream fin,long len){
32. byte[] raw = new byte[(int) len];
33. fin.read(raw);
34. fin.close();
35. return defineClass(name,raw,0,raw.length);
36. }
37.
38. protected Class loadClass(String name, boolean resolve)
39. throws ClassNotFoundException {
40. Class cls = null;
41. cls = findLoadedClass(name);
42. if(!this.dynaclazns.contains(name) && cls == null)
43. cls = getSystemClassLoader().loadClass(name);
44. if (cls == null)
45. throw new ClassNotFoundException(name);
46. if (resolve)
47. resolveClass(cls);
48. return cls;
49. }
50.
51.}
在該類載入器的實現中,所有指定必須由它直接載入的類都在該載入器實例化時進行了載入,當通過loadClass進行類的載入時,如果該類沒有載入過,並且不屬於必須由該類載入器載入之列都委託給系統載入器進行載入.
實現Java類的熱替換
現在來介紹一下我們的實驗方法,為了簡單起見,我們的包為默認包,沒有層次,並且省去了所有錯誤處理.要替換的類為Foo,實現很簡單,僅包含一個方法sayHello:
1.public class Foo{
2. public void sayHello() {
3. System.out.println("hello world! (version one)");
4. }
5.}
在當前工作目錄下建立一個新的目錄swap,把編譯好的Foo.class文件放在該目錄中.接下來要使用我們前面編寫的HotswapCL來實現該類的熱替換.具體的做法為:我們編寫一個定時器任務,每隔2秒鐘執行一次.其中,我們會創建新的類載入器實例載入Foo類,生成實例,並調用sayHello方法.接下來,我們會修改Foo類中sayHello方法的列印內容,重新編譯,並在系統運行的情況下替換掉原來的Foo.class,我們會看到系統會列印出更改后的內容.定時任務的實現如下(其它代碼省略,請讀者自行補齊):
6.public void run(){
7. try {
8. // 每次都創建出一個新的類載入器
9. HowswapCL cl = new HowswapCL("../swap", new String[]{"Foo"});
10. Class clcls = cl.loadClass("Foo");
11. Object foo = cls.newInstance();
12.
13. Method m = foo.getClass().getMethod("sayHello", new Class[]{});
14. m.invoke(foo, new Object[]{});
15.
16. } catch(Exception ex) {
17. ex.printStackTrace();
18. }
19.}
編譯、運行我們的系統,會出現如下的列印:
好,現在我們把Foo類的sayHello方法更改為:
20.public void sayHello() {
21. System.out.println("hello world! (version two)");
22.}
在系統仍在運行的情況下,編譯,並替換掉swap目錄下原來的Foo.class文件,我們再看看屏幕的列印,奇妙的事情發生了,新更改的類在線即時生效了,我們已經實現了Foo類的熱替換.屏幕列印如下:
圖4.熱替換后的運行結果
敏銳的讀者可能會問,為何不用把foo轉型為Foo,直接調用其sayHello方法呢?這樣不是更清晰明了嗎?下面我們來解釋一下原因,並給出一種更好的方法.如果我們採用轉型的方法,代碼會變成這樣:Foofoo=(Foo)cls.newInstance();讀者如果跟隨本文進行試驗的話,會發現這句話會拋出ClassCastException異常,為什麼嗎?在Java中,即使是同一個類文件,如果是由不同的類載入器實例載入的,那麼它們的類型是不相同的.在上面的例子中cls是由HowswapCL載入的,而foo變數類型聲名和轉型里的Foo類卻是由run方法所屬的類的載入器(默認為AppClassLoader)載入的,因此是完全不同的類型,會拋出轉型異常.
那麼通過介面調用是不是就行了呢?我們可以定義一個IFoo介面,其中聲名sayHello方法,Foo實現該介面.也就是這樣:IFoofoo=(IFoo)cls.newInstance();本來該方法也會有同樣的問題的,外部聲名和轉型部分的IFoo是由run方法所屬的類載入器載入的,而Foo類定義中implementsIFoo中的IFoo是由HotswapCL載入的,因此屬於不同的類型轉型還是會拋出異常的,但是由於我們在實例化HotswapCL時是這樣的:
23.HowswapCLcl=newHowswapCL("../swap",newString[]{"Foo"});
其中僅僅指定Foo類由HotswapCL載入,而其實現的IFoo介面文件會委託給系統類載入器載入,因此轉型成功,採用介面調用的代碼如下:
24.public void run(){
25. try {
26. HowswapCL cl = new HowswapCL("../swap", new String[]{"Foo"});
27. Class clcls = cl.loadClass("Foo");
28. IFoo foo = (IFoo)cls.newInstance();
29. foo.sayHello();
30. } catch(Exception ex) {
31. ex.printStackTrace();
32. }
33.}
確實,簡潔明了了很多,在我們的實驗中,每當定時器調度到run方法時,我們都會創建一個新的HotswapCL實例,在產品代碼中,無需如此,僅當需要升級替換時才去創建一個新的類載入器實例.
在線升級系統的設計原則
在上小節中,我們給出了一個Java類熱替換的實例,掌握了這項技術,就具備了實現在線升級系統的基礎.但是,對於一個真正的產品系統來說,升級本省就是一項非常複雜的工程,如果要在線升級,就會更加複雜.其中,實現類的熱替換隻是一步操作,在線升級的要求會對系統的整體設計帶來深遠的影響.下面我們來談談在線升級系統設計方面的一些原則:
◆在系統設計一開始,就要考慮系統的哪些部分是需要以後在線升級的,哪些部分是穩定的
雖然我們可以把系統設計成任何一部分都是可以在線升級的,但是其成本是非常高昂的,也沒有必要.因此,明確地界定出系統以後需要在線升級的部分是明智之舉.這些部分常常是系統業務邏輯規則、演算法等等.
◆設計出規範一致的系統狀態轉換方法
替換一個類僅僅是在線升級系統所要做的工作中的一個步驟,為了使系統能夠在升級后正常運行,就必須保持升級前後系統狀態的一致性.因此,在設計時要考慮需要在線升級的部分所涉及的系統狀態有哪些,把這些狀態設計成便於獲取、設置和轉換的,並用一致的方式來進行.
◆明確出系統的升級控制協議
這個原則是關於系統在線升級的時機和流程式控制制的,不考慮系統的當前運行狀態就貿然進行升級是一項非常危險的活動.因此在系統設計中,就要考慮並預留出系統在線升級的控制點,並定義清晰、明確的升級協議來協調、控制多個升級實體的升級次序,以確保系統在升級的任何時刻都處在一個確定的狀態下.
◆考慮到升級失敗時的回退機制
即使我們做了非常縝密細緻的設計,還是難以從根本上保證系統升級一定是成功的,對於大型分散式系統來說尤其如此.因此在系統設計時,要考慮升級失敗后的回退機制.
在線升級系統實例
,我們來簡單介紹一下這個實例的結構組成和要完成的工作.在我們的例子中,主要有三個實體,一個是升級控制實體,兩個是工作實體,都基於ActiveObject實現,通過命令消息進行通信(關於ActiveObject的詳細信息,可以參見作者的另外一篇文章「構建Java併發模型框架」).
升級控制實體以RMI的方式對外提供了一個管理命令介面,用以接收外部的在線升級命令.工作實體有兩個消息隊列,一個用以接收分配給它的任務(我們用定時器定時給它發送任務命令消息),我們稱其為任務隊列;另一個用於和升級控制實體交互,協作完成升級過程,我們稱其為控制隊列.工作實體中的任務很簡單,就是使用我們前面介紹的Foo類簡單地列印出一個字元串,不過這次字元串作為狀態保存在工作實體中,動態設置給Foo類的實例的.升級的協議流程如下:
當升級控制實體接收到來自RMI的在線升級命令時,它會向兩個工作實體的任務隊列中發送一條準備升級消息,然後等待回應.當工作實體在任務隊列中收到準備升級消息時,會立即給升級控制實體發送一條準備就緒消息,然後切換到控制隊列等待進一步的升級指令.升級控制實體收齊這兩個工作實體發來的準備就緒消息后,就給這兩個工作實體的控制隊列各發送一條開始升級消息,然後等待結果.工作實體收到開始升級消息后,進行實際的升級工作,也就是我們前面講述的熱替換類.然後,給升級控制實體發送升級完畢消息.升級控制實體收到來自兩個工作實體的升級完畢消息后,會給這兩個工作實體的控制隊列各發送一條繼續工作消息,工作實體收到繼續工作消息后,切換到任務隊列繼續工作,升級過程結束.主要的代碼片段如下(略去命令消息的定義和執行細節):
1.// 升級控制實體關鍵代碼
2.class UpgradeController extends ActiveObject{
3. int nready = 0;
4. int nfinished = 0;
5. Worker[] workers;
6. ......
7. // 收到外部升級命令消息時,會觸發該方法被調用
8. public void askForUpgrade() {
9. for(int i=0; i<workers.length; i )
10. workers[i].getTaskQueue().enqueue(new PrepareUpgradeCmd(workers[i]));
11. }
12.
13. // 收到工作實體回應的準備就緒命令消息時,會觸發該方法被調用
14. public void readyForUpgrade(String worker_name) {
15. nready ;
16. if(nready == workers.length){
17. for(int i=0; i<workers.length; i )
18. workers[i].getControlQueue().enqueue(new
19. StartUpgradeCmd(workers[i]));
20. }
21. }
22.
23. // 收到工作實體回應的升級完畢命令消息時,會觸發該方法被調用
24. public void finishUpgrade(String worker_name) {
25. nfinished ;
26. if(nfinished == workers.length){
27. for(int i=0; i<workers.length; i )
28. workers[i].getControlQueue().enqueue(new
29. ContineWorkCmd(workers[i]));
30.
31. }
32. }
33.
34. ......
35.
36.}
37.
38.// 工作實體關鍵代碼
39.class Worker extends ActiveObject{
40. UpgradeController ugc;
41. HotswapCL hscl;
42. IFoo foo;
43. String state = "hello world!";
44.
45. ......
46.
47. // 收到升級控制實體的準備升級命令消息時,會觸發該方法被調用
48. public void prepareUpgrade() {
49. switchToControlQueue();
50. ugc.getMsgQueue().enqueue(new ReadyForUpdateCMD(ugc,this));
51. }
52.
53. // 收到升級控制實體的開始升級命令消息時,會觸發該方法被調用
54. public void startUpgrade(String worker_name) {
55. doUpgrade();
56. ugc.getMsgQueue().enqueue(new FinishUpgradeCMD(ugc,this));
57. }
58.
59. // 收到升級控制實體的繼續工作命令消息時,會觸發該方法被調用
60. public void continueWork(String worker_name) {
61. switchToTaskQueue();
62. }
63.
64. // 收到定時命令消息時,會觸發該方法被調用
65. public void doWork() {
66. foo.sayHello();
67. }
68.
69. // 實際升級動作
70. private void doUpgrade() {
71. hscl = new HowswapCL("../swap", new String[]{"Foo"});
72. Class cls = hscl.loadClass("Foo");
73. foo = (IFoo)cls.newInstance();
74. foo.SetState(state);
75. }
76.}
77.
78.//IFoo 介面定義
79.interface IFoo {
80. void SetState(String);
81. void sayHello();
82.}
在Foo類第一個版本的實現中,只是把設置進來的字元串直接列印出來.在第二個版本中,會先把設置進來的字元串變為大寫,然後列印出來.例子很簡單,旨在表達規則或者演算法方面的升級變化.另外,我們並沒有提及諸如:消息超時、升級失敗等方面的異常情況,這在實際產品開發中是必須要考慮的.
[火星人 ] Java類中熱替換的概念、設計與實現已經有859次圍觀