在執行時刻更新功能模組 (轉)

worldblog發表於2007-12-09
在執行時刻更新功能模組 (轉)[@more@]
本文介紹了一個利用類庫載入器Claoader 實現在執行時刻部分功能模組的,並將其與C/C++中實現同樣功能的動態連結庫方案進行了簡單比較。


在嵌入式的設計中,經常涉及到在執行時刻更新部分功能模組的設計。例如一個用於資料採集與處理的裝置,包括資料採集,資料傳送,命令接收等功能模組,有可能被要求在繼續進行資料採集的同時採用新的資料格式向一個新的資料處理系統傳送資料。在這種情況下,就必須在執行時刻動態的更新資料傳送的功能模組。

在C/C++中,這樣的功能可以很容易的利用動態連結庫來實現。 LoadLibrary和FreeLibrary提供了在執行時刻載入新的功能模組和釋放空間的功能。需要被更新的功能模組被封裝在動態連線庫裡,主程式利用LoadLibrary 函式裝載該動態連結庫,然後其中的功能模組。需要更新某功能模組的時候,首先終止執行該功能模組,利用FreeLibrary 函式解除安裝現有的動態連結庫,透過或者是其他通訊埠將新的動態連結庫傳送到指定目錄下,然後利用再次利用LoadLibrary 函式裝載新的動態連結庫並呼叫其中的新功能模組。(如果需要進一步瞭解動態連結庫的內容,請參閱sources">參考文獻1中的相關部分。)

在Java中,有一個被稱為類庫載入器的抽象類ClassLoader 能夠用來實現類似於LoadLibrary的功能。本文下面的部分著重介紹ClassLoader的一般結構以及利用ClassLoader實現在執行時刻更新部分功能模組的方法。


類庫載入器ClassLoader 是一個負責載入類庫的抽象類。它接受一個類庫的名稱並試圖定位和生成包含有改類庫定義的資料。通常的實現方法是將該類庫的名稱轉化成一個檔名,然後從檔案系統中找到該檔案並讀取其中的內容。(關於類庫載入器的定義,請參閱參考文獻2。)

所有的Java虛擬機器都包括一個內建的類庫載入器。這個內建的類庫載入器被稱為主類庫載入器。主類庫載入器的特殊之處是它只能夠載入在設計時刻已知的類,因此虛擬機器假定由主類庫載入器所載入的類都是可信任的,可以不經過而直接執行。當應用程式需要載入在設計時刻未知的類庫時,就必須使用自定義的類庫載入器。

一個使用者自定義的類庫載入器是抽象類java.lang.ClassLoader 的派生類,其中唯一必須實現的抽象方法是loadClass()。通常來說,loadClass()方法需要實現如下操作:

  1. 確認類庫名稱
  2. 檢查請求載入的類庫是否已經被載入
  3. 檢查請求載入的類庫是否是系統類庫
  4. 嘗試從類庫載入器的區獲取所清求的類庫
  5. 在虛擬機器中定義所請求的類庫
  6. 解析所請求的類庫
  7. 返回所請求的類庫

一個使用者自定義類庫載入器幾乎可以從任何儲存裝置上載入類庫。裝載本地上的類庫當然不在話下,透過超級連線裝載網路上的類庫也很容易。由於類庫載入器的存在,Java虛擬機器並不需要事先知道關於將要執行的類庫的任何細節。由於類庫載入器的功能是如此的強大,出於安全考慮某些Java類庫如applets 等不允許啟用自定義的類庫載入器。

參考文獻3 給出了關於使用者自定義類庫的更詳細描述,同時提供了一個示例程式SimpleClassLoader。在本文下面的例子中,使用該文獻中的SimpleClassLoader作為使用者自定義類庫載入器。


在動態連結庫技術中,LoadLibrary函式負責載入功能模組,FreeLibrary函式負責解除安裝功能模組。新的功能模組與舊的功能模組同名,新的動態連結庫檔案也與舊的動態連結庫檔案同名。當需要更新某個功能模組的時候,使用新的動態連結庫檔案替換舊的動態連結庫檔案,被舊的功能模組所佔用的記憶體空間也同時被釋放。

但是Java並不提供一個類似於類庫解除安裝器(ClassUnloader) 的功能,能夠把已經裝載的功能模組從記憶體裡面清除掉。目前的虛擬機器,大都使用了及時編譯(JIT) 技術,也就是說一個功能模組只有在它第一次被使用的時候才被編譯。經過編譯的可程式碼被放到記憶體裡面,用一個HashTable 做,其關鍵字為與之相對應的類庫名。虛擬機器需要用到某個功能模組的時候,它先到這個HashTable 裡面查詢相應的關鍵字。如果該功能模組已經存在,虛擬機器直接從記憶體裡呼叫經過編譯的可執行程式碼,反之則呼叫類庫載入器裝載新的功能模組並進行編譯。由於沒有模組解除安裝功能,在執行時刻已經被裝載的功能模組是一直存在的。當某功能模組實際上已經被更新(即.class檔案被替換為同名的新檔案)並需要被重新載入的時候,虛擬機器並不會試圖裝載新的功能模組而直接呼叫舊的功能模組。如果試圖利用使用者自定義的類庫載入器強行裝載新的功能模組,則會因為新的功能模組與舊的功能模組同名而導致虛擬機器丟擲連結錯誤: Linkage Error: duplicate class definition。

腦子快的朋友也許已經想出了以下的方法:

SimpleClassLoader scl = new SimpleClassLoader(); o; Class c; c = scl.loadClass("SomeNewClass"); o = c.newInstance(); ((SomeNewClass) o).SomeClassMethod(SomeParam);


但是,這樣的方法實際上是不能夠實現的。首先,SomeNewClass在程式設計的時候尚未存在,這樣的程式是無法透過編譯的。其次,在執行時刻只有使用者自定義的類庫載入器SimpleClassLoader能夠獲取有關SomeNewClass 的定義,虛擬機器的主類庫載入器是無法建立一個SomeNewClass的,因此以上程式的最後一行也會出錯。

參考文獻3 指出有兩個方法可以解決這個問題。一是被裝載的模組是虛擬機器的主類庫載入器已經載入的某個類庫的派生類庫(subclass),一是被載入的模組實現某個已經被系統虛擬機器的主類庫載入器載入的介面(interface)。 在中通常都使用了第一種方法,譬如說所有的applet都是java.applet.Applet的派生類庫,因此在所有的applet中都有類似於public class MyClass extends Applet 的宣告。在這裡我們採用參考文獻3 中介紹的第二種方法,也就是被載入的新模組實現某個預先設計好的介面。

宣告介面UpdatableModule如下:

public interface UpdatableModule { void start(String RunTimeParam) }


由於這個介面在設計時刻已經存在,它可以被虛擬機器的主類庫載入器和將要被載入的新功能模組所呼叫。新功能模組所需要做的,只是實現這個介面中的方法,例如:

public class NewModule_1 implements UpdatableModule { void start(String RunTimeParam) { System.out.println("This is new module 1."); } } public class NewModule_2 implements UpdatableModule { void start(String RunTimeParam) { System.out.println("This is new module 2."); } }


在執行時刻,主程式需要從外部獲得新的功能模組名,利用使用者自定義的類庫載入器載入新的功能模組,生成一個新的功能模組物件,然後透過事先定義好的介面呼叫新的功能模組中的方法。例如:

public class Test { public static void main(String[] args) { SimpleClassLoader scl = new SimpleClassLoader(); String RunTimeModule; Object o; Class c; RunTimeModule = args[0]; c = scl.loadClass(RunTimeModule); o = c.newInstance(); ((UpdatableModule) o).start("No parameter needed."); } }



下面我們介紹一個簡單的資料採集與處理程式。該程式採集當前的系統時間並按照一定的格式輸出到標準輸出裝置,其中的資料處理模組(即資料輸出模組)可以在執行時刻被更新。該程式包括如下功能模組:

  • DataBuffer---------資料緩衝區
  • DataCollector------資料採集模組
  • DataProcessor------資料處理模組介面
  • PrintData_1--------資料輸出模組,實現資料處理模組DataProcessor的介面
  • PrintData_2--------資料輸出模組,實現資料處理模組DataProcessor的介面
  • TestGUI------------測試圖形介面

資料緩衝區DataBuffer存放資料採集模組DataCollector 所採集的資料,它提供了更新資料和查詢資料的方法。

public class DataBuffer { private String data; // 更新資料的方法 public synchronized void UpdateData(String s) { data = s; notifyAll(); } // 查詢資料的方法 public String GetData() { return data; } }


資料採集模組DataCollector是一個執行緒,它每隔5秒鐘採集一次系統時間並更新資料緩衝區DataBuffer。

import java.util.Calendar; public class DataCollector extends thread { private DataBuffer ; // 建構函式 public DataCollector(DataBuffer db) { DB = db; } // 每5秒鐘採集一次系統時間並更新資料緩衝區 public void run() { while (true) { try { DB.UpdateData("" + Calendar.getInstance().getTime()); sleep(5000); } catch (InterruptedException e) {} } } }


資料處理模組介面DataProcessor定義了資料處理模組所需要實現的方法。

public interface DataProcessor { // 資料處理模組所需要實現的方法, 開始處理資料 void start(DataBuffer db); // 資料處理模組所需要實現的方法, 停止處理資料 void stop(); }


資料輸出模組PrintData_1實現資料處理模組DataProcessor的介面。該模組每3秒鐘查詢一次資料緩衝區DataBuffer 中的資料並將其加上標誌"Data 1: "輸出到標準輸出裝置。

public class PrintData_1 implements DataProcessor { private PrintData PD; private boolean run; // 資料處理模組所需要實現的方法, 開始處理資料 public void start(DataBuffer db) { run = true; PD = new PrintData(db); PD.start(); } // 資料處理模組所需要實現的方法, 停止處理資料 public synchronized void stop() { run = false; notifyAll(); } // 進行資料處理的執行緒 class PrintData extends Thread { private DataBuffer DB; private String data; // 建構函式 public PrintData(DataBuffer db) { DB = db; } // 資料處理 public void run() { while (run) { try { data = "Data 1: " + DB.GetData(); System.out.println(data); sleep (3000); } catch (InterruptedException e) {} } } } }


資料輸出模組PrintData_2實現資料處理模組DataProcessor的介面。該模組每2秒鐘查詢一次資料緩衝區DataBuffer 中的資料並將其加上標誌"Data 2: "輸出到標準輸出裝置。

public class PrintData_2 implements DataProcessor { private PrintData PD; private boolean run; // 資料處理模組所需要實現的方法, 開始處理資料 public void start(DataBuffer db) { run = true; PD = new PrintData(db); PD.start(); } // 資料處理模組所需要實現的方法, 停止處理資料 public synchronized void stop() { run = false; notifyAll(); } // 進行資料處理的執行緒 class PrintData extends Thread { private DataBuffer DB; private String data; // 建構函式 public PrintData(DataBuffer db) { DB = db; } // 資料處理 public void run() { while (run) { try { data = "Data 2: " + DB.GetData(); System.out.println(data); sleep (2000); } catch (InterruptedException e) {} } } } }


測試圖形介面TestGUI 建立一個簡單的使用者圖形介面。該程式宣告一個資料緩衝區物件並啟動資料採集執行緒。使用者可以透過該圖形介面輸入執行時刻的資料處理模組名稱,啟動或者是終止資料處理模組,以及退出應用程式。

import java.awt.*; import java.awt.event.*; import com.agnc.loader.*; public class TestGUI extends Frame implements ActionListener { DataBuffer DB; DataCollector DC; TextField ClassNameText; Object o; // 建構函式 public TestGUI() { // 構造圖形介面 add(CreateGui()); addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); // 啟動測試模組 InitTest(); } // 構造圖形介面 public Panel CreateGui() { // 建立按鈕以及文字框 Button b1, b2, b3; b1 = new Button("啟動資料處理模組"); b1.setActionCommand("1"); b1.addActionListener(this); b2 = new Button("終止資料處理模組"); b2.setActionCommand("2"); b2.addActionListener(this); b3 = new Button("退出示例程式"); b3.setActionCommand("3"); b3.addActionListener(this); ClassNameText = new TextField("請輸入資料處理模組名稱"); // 建立控制皮膚 Panel GuiPanel = new Panel(); GuiPanel.setLayout(new GridLayout(2,2)); GuiPanel.add(b1); GuiPanel.add(b2); GuiPanel.add(b3); GuiPanel.add(ClassNameText); return GuiPanel; } // 啟動測試模組 public void InitTest() { // 構造資料緩衝區 DB = new DataBuffer(); // 啟動資料採集執行緒 DC = new DataCollector(DB); DC.start(); } // 事件處理模組 public void actionPerformed(ActionEvent e) { String command = e.getActionCommand(); String name; Class c; if (command == "1") { // 呼叫新的資料處理模組 try { name = ClassNameText.getText(); SimpleClassLoader scl = new SimpleClassLoader(); c = scl.loadClass(name); o = c.newInstance(); ((DataProcessor) o).start(DB); } catch (Exception ex) {} } else if (command == "2") { // 終止當前的資料處理模組 ((DataProcessor) o).stop(); o = null; } else { // 退出示例程式 System.exit(0); } } // 測試程式 public static void main(String[] args) { TestGUI window = new TestGUI(); window.setTitle("OPS Commander"); window.pack(); window.setVisible(true); } }


將以上原始碼編譯以後執行TestGUI ,在測試圖形介面的文字框裡面輸入需要執行的資料處理模組的名稱,然後點選“啟動資料處理模組”按鈕,即可執行指定的資料處理模組,點選“終止資料處理模組”即可終止當前的資料處理模組。在執行時刻,使用者可以根據資料處理介面DataProcessor 編制新的資料處理模組並提交該示例程式執行。因此,本示例程式能夠實現在執行時刻更新功能模組的功能。

本示例程式沒有實現複雜的出錯處理。在執行時刻,使用者需要首先終止當前的資料處理模組,才能夠啟動新的資料處理模組。


到此為止我們已經利用類庫載入器實現了在執行時刻更新程式模組的功能。下面我們將這種方法與C/C++中的動態連結庫做一個簡單的比較。

首先,C/C++ 所使用的動態連結庫是經過編譯的可執行程式碼,可以直接被主程式所呼叫執行。Java的類庫是位元組碼,在第一次被呼叫之前必須經過虛擬機器的及時編譯才能夠被呼叫執行。因此,Java程式的啟動時間要比C/C++ 程式的啟動事件更長。另外,Java程式的執行普遍來說要比C/C++ 程式低20% ~ 30%。

其次,C/C++能夠利用FreeLibrary函式釋放舊的功能模組所佔用的記憶體空間,從而保持了功能模組名和動態連結庫檔名的一致性,同時節省了記憶體空間和空間。而Java並不提供類似的類庫解除安裝功能,被舊的功能模組所佔用的記憶體空間已經不再被使用卻無法被釋放,因此會有相當數量的記憶體被浪費。

由於動態連結庫檔案在其中的功能模組被使用的過程中是一直開啟的,更新動態連結庫檔案的操作只能夠在其中的功能模組已經被終止以後才能夠進行。而Java的新功能模組和舊功能模組使用不同的檔名,可以在執行舊的功能模組的同時和傳輸新的功能模組到指定的位置。通常來說,下載和傳輸檔案要涉及到效率低下的磁碟操作,因此,在實際應用中動態連結庫實現方案下載和傳輸檔案所需要的時間可能比虛擬機器編譯新的功能模組所需要的時間更長。但是如果在下載和傳輸過程中新的動態連結庫檔案採用一個不同的檔名,在需要載入新的功能模組之前刪除舊的動態連結庫檔案並且將新的動態連結庫檔案改名,動態連結庫實現方案所需要的時間就要大大的縮短。

綜上所述,利用C/C++ 和Java都能夠實現在執行時刻更新功能模組的功能。相對來說,基於動態連結庫技術的C/C++ 方案在執行效率,記憶體利用以及操作效率等方面都比基於類庫載入器的Java方案更有優勢,應該作為首選的實現方案。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-990335/,如需轉載,請註明出處,否則將追究法律責任。

相關文章