熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

Java類的熱替換——概念、設計與實現

2022-06-13   來源: Java核心技術 

  對於許多關鍵性業務或者龐大的 Java 系統來說如果必須暫停系統服務才能進行系統升級既會大大影響到系統的可用性同時也增加了系統的管理和維護成本因此如果能夠方便地在不停止系統業務的情況下進行系統升級則可以很好地解決上述問題在本文中我們將基於實例對構建在線升級 Java 系統的基礎技術和設計原則進行了深入的講解相信讀者能夠根據文中的技術構建出自己的在線升級系統來

  Java ClassLoader 技術剖析

  在本文中我們將不對 Java ClassLoader 的細節進行過於詳細的講解而是關注於和構建在線升級系統相關的基礎概念關於 ClassLoader 的詳細細節許多資料可以參考有興趣的讀者可以自行研讀

  要構建在線升級系統一個重要的技術就是能夠實現 Java 類的熱替換 —— 也就是在不停止正在運行的系統的情況下進行類(對象)的升級替換而 Java 的 ClassLoader 正是實現這項技術的基礎

  在 Java 中類的實例化流程分為兩個部分類的加載和類的實例化類的加載又分為顯式加載和隱式加載大家使用 new 關鍵字創建類實例時其實就隱式地包含了類的加載過程對於類的顯式加載來說比較常用的是 ClassforName其實它們都是通過調用 ClassLoader 類的 loadClass 方法來完成類的實際加載工作的直接調用 ClassLoader 的 loadClass 方法是另外一種不常用的顯式加載類的技術

  
Java 類加載器層次結構圖

  ClassLoader 在加載類時有一定的層次關系和規則在 Java 中有四種類型的類加載器分別為BootStrapClassLoaderExtClassLoaderAppClassLoader 以及用戶自定義的 ClassLoader這四種類加載器分別負責不同路徑的類的加載並形成了一個類加載的層次結構

  BootStrapClassLoader 處於類加載器層次結構的最高層負責 sunbootclasspath 路徑下類的加載默認為 jre/lib 目錄下的核心 API 或 Xbootclasspath 選項指定的 jar 包ExtClassLoader 的加載路徑為 javaextdirs默認為 jre/lib/ext 目錄或者 Djavaextdirs 指定目錄下的 jar 包加載AppClassLoader 的加載路徑為 javaclasspath默認為環境變量 CLASSPATH 中設定的值也可以通過 classpath 選型進行指定用戶自定義 ClassLoader 可以根據用戶的需要定制自己的類加載過程在運行期進行指定類的動態實時加載

  這四種類加載器的層次關系圖如 圖 所示一般來說這四種類加載器會形成一種父子關系高層為低層的父加載器在進行類加載時首先會自底向上挨個檢查是否已經加載了指定類如果已經加載則直接返回該類的引用如果到最高層也沒有加載過指定類那麼會自頂向下挨個嘗試加載直到用戶自定義類加載器如果還不能成功就會拋出異常Java 類的加載過程如 圖 所示

  
Java 類的加載過程

  每個類加載器有自己的名字空間對於同一個類加載器實例來說名字相同的類只能存在一個並且僅加載一次不管該類有沒有變化下次再需要加載時它只是從自己的緩存中直接返回已經加載過的類引用

  我們編寫的應用類默認情況下都是通過 AppClassLoader 進行加載的當我們使用 new 關鍵字或者 ClassforName 來加載類時所要加載的類都是由調用 new 或者 ClassforName 的類的類加載器(也是 AppClassLoader)進行加載的要想實現 Java 類的熱替換首先必須要實現系統中同名類的不同版本實例的共存通過上面的介紹我們知道要想實現同一個類的不同版本的共存我們必須要通過不同的類加載器來加載該類的不同版本另外為了能夠繞過 Java 類的既定加載過程我們需要實現自己的類加載器並在其中對類的加載過程進行完全的控制和管理

  編寫自定義的 ClassLoader

  為了能夠完全掌控類的加載過程我們的定制類加載器需要直接從 ClassLoader 繼承首先我們來介紹一下 ClassLoader 類中和熱替換有關的的一些重要方法

  findLoadedClass 每個類加載器都維護有自己的一份已加載類名字空間其中不能出現兩個同名的類凡是通過該類加載器加載的類無論是直接的還是間接的都保存在自己的名字空間中該方法就是在該名字空間中尋找指定的類是否已存在如果存在就返回給類的引用否則就返回 null這裡的直接是指存在於該類加載器的加載路徑上並由該加載器完成加載間接是指由該類加載器把類的加載工作委托給其他類加載器完成類的實際加載

  getSystemClassLoader Java 中新增的方法該方法返回系統使用的 ClassLoader可以在自己定制的類加載器中通過該方法把一部分工作轉交給系統類加載器去處理

  defineClass 該方法是 ClassLoader 中非常重要的一個方法它接收以字節數組表示的類字節碼並把它轉換成 Class 實例該方法轉換一個類的同時會先要求裝載該類的父類以及實現的接口類

  loadClass 加載類的入口方法調用該方法完成類的顯式加載通過對該方法的重新實現我們可以完全控制和管理類的加載過程

  resolveClass 鏈接一個指定的類這是一個在某些情況下確保類可用的必要方法詳見 Java 語言規范中執行一章對該方法的描述

  了解了上面的這些方法下面我們來實現一個定制的類加載器來完成這樣的加載流程我們為該類加載器指定一些必須由該類加載器直接加載的類集合在該類加載器進行類的加載時如果要加載的類屬於必須由該類加載器加載的集合那麼就由它直接來完成類的加載否則就把類加載的工作委托給系統的類加載器完成

  在給出示例代碼前有兩點內容需要說明一下要想實現同一個類的不同版本的共存那麼這些不同版本必須由不同的類加載器進行加載因此就不能把這些類的加載工作委托給系統加載器來完成因為它們只有一份為了做到這一點就不能采用系統默認的類加載器委托規則也就是說我們定制的類加載器的父加載器必須設置為 null該定制的類加載器的實現代碼如下

  class CustomCL extends ClassLoader {

  private String basedir; // 需要該類加載器直接加載的類文件的基目錄

  private HashSet dynaclazns; // 需要由該類加載器直接加載的類名

  public CustomCL(String basedir String[] clazns) {

  super(null); // 指定父類加載器為 null

  thisbasedir = basedir;

  dynaclazns = new HashSet();

  loadClassByMe(clazns);

  }

  private void loadClassByMe(String[] clazns) {

  for (int i = ; i < claznslength; i++) {

  loadDirectly(clazns[i]);

  dynaclaznsadd(clazns[i]);

  }

  }

  private Class loadDirectly(String name) {

  Class cls = null;

  StringBuffer sb = new StringBuffer(basedir);

  String classname = namereplace( FileseparatorChar) + class;

  sbappend(Fileseparator + classname);

  File classF = new File(sbtoString());

  cls = instantiateClass(namenew FileInputStream(classF)

  classFlength());

  return cls;

  }

  private Class instantiateClass(String nameInputStream finlong len){

  byte[] raw = new byte[(int) len];

  finread(raw);

  finclose();

  return defineClass(namerawrawlength);

  }

  protected Class loadClass(String name boolean resolve)

  throws ClassNotFoundException {

  Class cls = null;

  cls = findLoadedClass(name);

  if(!ntains(name) && cls == null)

  cls = getSystemClassLoader()loadClass(name);

  if (cls == null)

  throw new ClassNotFoundException(name);

  if (resolve)

  resolveClass(cls);

  return cls;

  }

  }

  在該類加載器的實現中所有指定必須由它直接加載的類都在該加載器實例化時進行了加載當通過 loadClass 進行類的加載時如果該類沒有加載過並且不屬於必須由該類加載器加載之列都委托給系統加載器進行加載理解了這個實現距離實現類的熱替換就只有一步之遙了我們在下一小節對此進行詳細的講解

  實現 Java 類的熱替換

  在本小節中我們將結合前面講述的類加載器的特性並在上小節實現的自定義類加載器的基礎上實現 Java 類的熱替換首先我們把上小節中實現的類加載器的類名 CustomCL 更改為 HotswapCL以明確表達我們的意圖

  現在來介紹一下我們的實驗方法為了簡單起見我們的包為默認包沒有層次並且省去了所有錯誤處理要替換的類為 Foo實現很簡單僅包含一個方法 sayHello

  清單 待替換的示例類

  public class Foo{

  public void sayHello() {

  Systemoutprintln(hello world! (version one));

  }

  }

  在當前工作目錄下建立一個新的目錄 swap把編譯好的 Fooclass 文件放在該目錄中接下來要使用我們前面編寫的 HotswapCL 來實現該類的熱替換具體的做法為我們編寫一個定時器任務每隔 秒鐘執行一次其中我們會創建新的類加載器實例加載 Foo 類生成實例並調用 sayHello 方法接下來我們會修改 Foo 類中 sayHello 方法的打印內容重新編譯並在系統運行的情況下替換掉原來的 Fooclass我們會看到系統會打印出更改後的內容定時任務的實現如下(其它代碼省略請讀者自行補齊)

  public void run(){

  try {

  // 每次都創建出一個新的類加載器

  HowswapCL cl = new HowswapCL(/swap new String[]{Foo});

  Class cls = clloadClass(Foo);

  Object foo = clsnewInstance();

  Method m = foogetClass()getMethod(sayHello new Class[]{});

  minvoke(foo new Object[]{});

  }  catch(Exception ex) {

  exprintStackTrace();

  }

  }

  編譯運行我們的系統會出現如下的打印

  
熱替換前的運行結果

  好現在我們把 Foo 類的 sayHello 方法更改為

  public void sayHello() {

  Systemoutprintln(hello world! (version two));

  }

  在系統仍在運行的情況下編譯並替換掉 swap 目錄下原來的 Fooclass 文件我們再看看屏幕的打印奇妙的事情發生了新更改的類在線即時生效了我們已經實現了 Foo 類的熱替換屏幕打印如下

  
熱替換後的運行結果

  敏銳的讀者可能會問為何不用把 foo 轉型為 Foo直接調用其 sayHello 方法呢?這樣不是更清晰明了嗎?下面我們來解釋一下原因並給出一種更好的方法

  如果我們采用轉型的方法代碼會變成這樣Foo foo = (Foo)clsnewInstance(); 讀者如果跟隨本文進行試驗的話會發現這句話會拋出 ClassCastException 異常為什麼嗎?因為在 Java 中即使是同一個類文件如果是由不同的類加載器實例加載的那麼它們的類型是不相同的在上面的例子中 cls 是由 HowswapCL 加載的而 foo 變量類型聲名和轉型裡的 Foo 類卻是由 run 方法所屬的類的加載器(默認為 AppClassLoader)加載的因此是完全不同的類型所以會拋出轉型異常

  那麼通過接口調用是不是就行了呢?我們可以定義一個 IFoo 接口其中聲名 sayHello 方法Foo 實現該接口也就是這樣IFoo foo = (IFoo)clsnewInstance(); 本來該方法也會有同樣的問題的因為外部聲名和轉型部分的 IFoo 是由 run 方法所屬的類加載器加載的而 Foo 類定義中 implements IFoo 中的 IFoo 是由 HotswapCL 加載的因此屬於不同的類型轉型還是會拋出異常的但是由於我們在實例化 HotswapCL 時是這樣的

  HowswapCL cl = new HowswapCL(/swap new String[]{Foo});

  其中僅僅指定 Foo 類由 HotswapCL 加載而其實現的 IFoo 接口文件會委托給系統類加載器加載因此轉型成功采用接口調用的代碼如下

  清單 采用接口調用的代碼

  public void run(){

  try {

  HowswapCL cl = new HowswapCL(/swap new String[]{Foo});

  Class cls = clloadClass(Foo);

  IFoo foo = (IFoo)clsnewInstance();

  foosayHello();

  } catch(Exception ex) {

  exprintStackTrace();

  }

  }

  確實簡潔明了了很多在我們的實驗中每當定時器調度到 run 方法時我們都會創建一個新的 HotswapCL 實例在產品代碼中無需如此僅當需要升級替換時才去創建一個新的類加載器實例

  在線升級系統的設計原則

  在上小節中我們給出了一個 Java 類熱替換的實例掌握了這項技術就具備了實現在線升級系統的基礎但是對於一個真正的產品系統來說升級本省就是一項非常復雜的工程如果要在線升級就會更加復雜其中實現類的熱替換只是最後一步操作在線升級的要求會對系統的整體設計帶來深遠的影響下面我們來談談在線升級系統設計方面的一些原則

  在系統設計一開始就要考慮系統的哪些部分是需要以後在線升級的哪些部分是穩定的

  雖然我們可以把系統設計成任何一部分都是可以在線升級的但是其成本是非常高昂的也沒有必要因此明確地界定出系統以後需要在線升級的部分是明智之舉這些部分常常是系統業務邏輯規則算法等等

  設計出規范一致的系統狀態轉換方法

  替換一個類僅僅是在線升級系統所要做的工作中的一個步驟為了使系統能夠在升級後正常運行就必須保持升級前後系統狀態的一致性因此在設計時要考慮需要在線升級的部分所涉及的系統狀態有哪些把這些狀態設計成便於獲取設置和轉換的並用一致的方式來進行

  明確出系統的升級控制協議

  這個原則是關於系統在線升級的時機和流程控制的不考慮系統的當前運行狀態就貿然進行升級是一項非常危險的活動因此在系統設計中就要考慮並預留出系統在線升級的控制點並定義清晰明確的升級協議來協調控制多個升級實體的升級次序以確保系統在升級的任何時刻都處在一個確定的狀態下

  考慮到升級失敗時的回退機制

  即使我們做了非常缜密細致的設計還是難以從根本上保證系統升級一定是成功的對於大型分布式系統來說尤其如此因此在系統設計時要考慮升級失敗後的回退機制

  好了本小節我們簡單介紹了在線升級系統設計時的幾個重要的原則下一小節我們將給出一個簡單的實例來演示一下如何來實現一個在線升級系統

  在線升級系統實例

  首先我們來簡單介紹一下這個實例的結構組成和要完成的工作在我們的例子中主要有三個實體一個是升級控制實體兩個是工作實體都基於 ActiveObject 實現

  升級控制實體以 RMI 的方式對外提供了一個管理命令接口用以接收外部的在線升級命令工作實體有兩個消息隊列一個用以接收分配給它的任務(我們用定時器定時給它發送任務命令消息)我們稱其為任務隊列另一個用於和升級控制實體交互協作完成升級過程我們稱其為控制隊列工作實體中的任務很簡單就是使用我們前面介紹的 Foo 類簡單地打印出一個字符串不過這次字符串作為狀態保存在工作實體中動態設置給 Foo 類的實例的升級的協議流程如下

  當升級控制實體接收到來自 RMI 的在線升級命令時它會向兩個工作實體的任務隊列中發送一條准備升級消息然後等待回應當工作實體在任務隊列中收到准備升級消息時會立即給升級控制實體發送一條准備就緒消息然後切換到控制隊列等待進一步的升級指令升級控制實體收齊這兩個工作實體發來的准備就緒消息後就給這兩個工作實體的控制隊列各發送一條開始升級消息然後等待結果工作實體收到開始升級消息後進行實際的升級工作也就是我們前面講述的熱替換類然後給升級控制實體發送升級完畢消息升級控制實體收到來自兩個工作實體的升級完畢消息後會給這兩個工作實體的控制隊列各發送一條繼續工作消息工作實體收到繼續工作消息後切換到任務隊列繼續工作升級過程結束

  主要的代碼片段如下(略去命令消息的定義和執行細節)

  清單 主要的代碼片段

  // 升級控制實體關鍵代碼

  class UpgradeController extends ActiveObject{

  int nready  = ;

  int nfinished = ;

  Worker[] workers;

  

  // 收到外部升級命令消息時會觸發該方法被調用

  public void askForUpgrade() {

  for(int i=; i<workerslength; i++)

  workers[i]getTaskQueue()enqueue(new PrepareUpgradeCmd(workers[i]));

  }

  // 收到工作實體回應的准備就緒命令消息時會觸發該方法被調用

  public void readyForUpgrade(String worker_name) {

  nready++;

  if(nready == workerslength){

  for(int i=; i<workerslength; i++)

  workers[i]getControlQueue()enqueue(new

  StartUpgradeCmd(workers[i]));

  }

  }

  // 收到工作實體回應的升級完畢命令消息時會觸發該方法被調用

  public void finishUpgrade(String worker_name) {

  nfinished++;

  if(nfinished == workerslength){

  for(int i=; i<workerslength; i++)

  workers[i]getControlQueue()enqueue(new

  ContineWorkCmd(workers[i]));

  }

  }

  

  }

  // 工作實體關鍵代碼

  class Worker extends ActiveObject{

  UpgradeController ugc;

  HotswapCL hscl;

  IFoo foo;

  String state = hello world!;

  

  // 收到升級控制實體的准備升級命令消息時會觸發該方法被調用

  public void prepareUpgrade() {

  switchToControlQueue();

  ugcgetMsgQueue()enqueue(new ReadyForUpdateCMD(ugcthis));

  }

  // 收到升級控制實體的開始升級命令消息時會觸發該方法被調用

  public void startUpgrade(String worker_name) {

  doUpgrade();

  ugcgetMsgQueue()enqueue(new FinishUpgradeCMD(ugcthis));

  }

  // 收到升級控制實體的繼續工作命令消息時會觸發該方法被調用

  public void continueWork(String worker_name) {

  switchToTaskQueue();

  }

  // 收到定時命令消息時會觸發該方法被調用

  public void doWork() {

  foosayHello();

  }

  // 實際升級動作

  private void doUpgrade() {

  hscl = new HowswapCL(/swap new String[]{Foo});

  Class cls = hsclloadClass(Foo);

  foo = (IFoo)clsnewInstance();

  fooSetState(state);

  }

  }

  //IFoo 接口定義

  interface IFoo {

  void SetState(String);

  void sayHello();

  }

  在Foo 類第一個版本的實現中只是把設置進來的字符串直接打印出來在第二個版本中會先把設置進來的字符串變為大寫然後打印出來例子很簡單旨在表達規則或者算法方面的升級變化另外我們並沒有提及諸如消息超時升級失敗等方面的異常情況這在實際產品開發中是必須要考慮的

  在本文中我們對 Java 在線升級系統中設計的基礎技術類的熱替換進行了詳細的講解此外還給出了在線升級系統設計時的一些主要指導原則為了使讀者更好地理解這些技術和原則我們在最後給出了一個在線升級系統的實例值得注意的是構建在線升級系統不僅僅是一個技術問題還牽扯到很多管理方面的因素比如如何管理部署系統中的可在線升級部分和不可在線升級部分以降低系統的管理維護成本等希望本文在讀者構建自己的在線升級系統時能夠提供一些幫助


From:http://tw.wingwit.com/Article/program/Java/hx/201311/26326.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.