Java程式設計師從笨鳥到菜鳥之(九十九)深入java虛擬機器(八)開發自己的類載入器
歡迎閱讀本專題的其他部落格:
在大多數情況下,系統預設提供的類載入器實現已經可以滿足需求。但是在某些情況下,您還是需要為應用開發出自己的類載入器。比如您的應用通過網路來傳輸 Java 類的位元組程式碼,為了保證安全性,這些位元組程式碼經過了加密處理。這個時候您就需要自己的類載入器來從某個網路地址上讀取加密後的位元組程式碼,接著進行解密和驗證,最後定義出要在 Java 虛擬機器中執行的類來。下面將通過兩個具體的例項來說明類載入器的開發。
要建立自定義的類載入器只需要擴充套件java.lang.ClassLoader類就可以,然後覆蓋它的findClass(String name)
自定義的類載入器:
package com.bzu.csh.test;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.InputStream;/** * * @author 曹勝歡 * @version 1.0 * */public class MyClassLoader extends ClassLoader {private String name; // 類載入器的名字private String path = "d:\\"; // 載入類的路徑private final String fileType = ".class"; // class檔案的副檔名public MyClassLoader(String name) {super(); // 讓系統類載入器成為該類載入器的父載入器this.name = name;}public MyClassLoader(ClassLoader parent, String name) {super(parent); // 顯式指定該類載入器的父載入器 this.name = name;}@Overridepublic String toString() {return this.name;}public String getPath() {return path;}public void setPath(String path) {this.path = path;}/** * @param 類檔案的名字 * @return 類檔案中類的class物件 * * 在這裡我們並不需要去顯示的呼叫這裡的findclass方法,在上篇文章中,我們通過檢視 * loadclass的原始碼可以發現,她是在loadclass中被呼叫的,所以這裡我們只需重寫這個方法, * 讓它根據我們的想法去查詢類檔案就ok,他會自動被呼叫 * * * defineClass()將一個 byte 陣列轉換為 Class 類的例項。必須分析 Class,然後才能使用它 * 引數: * name - 所需要的類的二進位制名稱,如果不知道此名稱,則該引數為 null * b - 組成類資料的位元組。off 與 off+len-1 之間的位元組應該具有《Java Virtual Machine Specification》定義的有效類檔案的格式。 * off - 類資料的 b 中的起始偏移量 * len - 類資料的長度 */@Overridepublic Class<?> findClass(String name) throws ClassNotFoundException {byte[] data = this.loadClassData(name);//獲得類檔案的位元組陣列return this.defineClass(name, data, 0, data.length);//}/** * * @param 類檔案的名字 * @return 類檔案的 位元組陣列 * 通過類檔案的名字獲得類檔案的位元組陣列,其實主要就是用 * 輸入輸出流實現。 */private byte[] loadClassData(String name) {InputStream is = null;byte[] data = null;ByteArrayOutputStream baos = null;try {this.name = this.name.replace(".", "\\");is = new FileInputStream(new File(path + name + fileType));baos = new ByteArrayOutputStream();int ch = 0;while (-1 != (ch = is.read())) {baos.write(ch);}data = baos.toByteArray();} catch (Exception ex) {ex.printStackTrace();} finally {try {is.close();baos.close();} catch (Exception ex) {ex.printStackTrace();}}return data;}
我想上面的註釋中已經足夠讓大家明白這個自定義類載入器的原理了。在這我在重複的從上到下的再說一遍,加深一下大家的理解。首先在構造方法中,我們可以通過構造方法給類載入器起一個名字,也可以顯示的指定他的父累加器器,如果沒有顯示的指出父類載入器的話他預設的就是系統類載入器。由於我們繼承了ClassLoader類,所以它自動繼承了父類的loadclass方法。我們以前看過loadclass的原始碼知道,它呼叫了findclass方法去查詢類檔案。所以在這裡我們重寫了ClassLoader的findclass方法。在這個方法中首先呼叫loadClassData方法,通過類檔案的名字獲得類檔案的位元組陣列,其實主要就是用輸入輸出流實現。然後呼叫defineClass()將一個 位元組 陣列轉換為 Class 類的例項。有時候我們手動生成的二進位制碼的class檔案被加密了。所以在我們在利用我們自定義的類載入器的時候還要寫一個解密的方法進行解密,這裡我們就不實現了。
我們實現了自定義類載入器,下一步我們來看一下我們怎麼來應用我們這個自定義的類載入器:
public static void main(String[] args) throws Exception {//建立一個loader1類載入器,設定他的載入路徑為d:\\serverlib\\,設定預設父載入器為系統類載入器MyClassLoader loader1 = new MyClassLoader("loader1");loader1.setPath("d:\\myapp\\serverlib\\");//建立一個loader2類載入器,設定他的載入路徑為d:\\clientlib\\,並設定父載入器為loader1MyClassLoader loader2 = new MyClassLoader(loader1, "loader2");loader2.setPath("d:\\myapp\\clientlib\\");//建立一個loader3類載入器,設定他的載入路徑為d:\\otherlib\\,並設定父載入器為根類載入器 MyClassLoader loader3 = new MyClassLoader(null, "loader3"); loader3.setPath("d:\\myapp\\otherlib\\"); test(loader2); System.out.println("----------"); test(loader3);}public static void test(ClassLoader loader) throws Exception {Class clazz = loader.loadClass("com.bzu.csh.test.Sample");Object object = clazz.newInstance();}
類載入器結構圖
(PS:突然發現WPS自帶的畫圖工具也挺好用,雖然有點難看,哈哈)
當執行這段程式碼的時候。首先讓loader2去載入Sample類檔案,當然我們在執行這段程式碼的前提時在各個預設載入器中已經有我們Sample的class檔案。Loader2首先讓父載入器是loader1去載入,然後loader1會讓系統類載入器去載入,系統類載入器會讓擴充套件類載入器載入,擴充套件類載入器會讓根類載入器載入,由於系統類載入器,擴充套件類載入器,根類載入器的預設路徑中都沒有我們要的sample類,所以loader2的預設路徑有sample這個類,也就是說loader2會去載入這個sample類。當執行test(loader3)的時候,由於loader3的預設父載入器是根類載入器,並且根類載入前預設路徑沒有對應的sample.class檔案,所以,直接的loader3類載入器就去載入這個類。
最後要說明的一點是,自定義類載入不光只能從我們本地載入到class檔案,我們也可以載入網路,即基本的場景是:Java 位元組程式碼(.class)檔案存放在伺服器上,客戶端通過網路的方式獲取位元組程式碼並執行。當有版本更新的時候,只需要替換掉伺服器上儲存的檔案即可。通過類載入器可以比較簡單的實現這種需求。其實他的實現和本地差不多,基本上就是geclassdata方法改變了一些。下面我們來具體看一下:
private byte[] getClassData(String className) {String path = classNameToPath(className);try {URL url = new URL(path);InputStream ins = url.openStream();ByteArrayOutputStream baos = new ByteArrayOutputStream();int bufferSize = 4096;byte[] buffer = new byte[bufferSize];int bytesNumRead = 0;while ((bytesNumRead = ins.read(buffer)) != -1) {baos.write(buffer, 0, bytesNumRead);}return baos.toByteArray();} catch (Exception e) {e.printStackTrace();}return null;}
在通過網路載入了某個版本的類之後,一般有兩種做法來使用它。第一種做法是使用 Java 反射 API。另外一種做法是使用介面。需要注意的是,並不能直接在客戶端程式碼中引用從伺服器上下載的類,因為我們寫的類載入器被載入所用的類載入器和我們載入的網路類不是同一個類載入器,所以客戶端程式碼的類載入器找不到這些類。使用 Java 反射 API 可以直接呼叫 Java 類的方法。而使用介面的做法則是把介面的類放在客戶端中,從伺服器上載入實現此介面的不同版本的類。在客戶端通過相同的介面來使用這些實現類。
不同類載入器的名稱空間關係:
1.同一個名稱空間內的類是相互可見的。
2.子載入器的名稱空間包含所有父載入器的名稱空間。因此子載入器載入的類能看見父載入器載入的類。例如系統類載入器載入的類能看見根類載入器載入的類。
3.由父載入器載入的類不能看見子載入器載入的類。
4.如果兩個載入器之間沒有直接或間接的父子關係,那麼它們各自載入的類相互不可見。
5.當兩個不同名稱空間內的類相互不可見時,可以採用Java的反射機制來訪問例項的屬性和方法。
------------------------------------------------------------------------------------------------------------------------ 廣告:我參加了2012年度IT部落格大賽,希望大家能多多支援http://blog.51cto.com/contest2012/3545281