在大多數狀況下,系統默認提供的類加載器實現已經能夠知足需求。可是在某些狀況下,您仍是須要爲應用開發出本身的類加載器。好比您的應用經過網絡來傳輸 Java 類的字節代碼,爲了保證安全性,這些字節代碼通過了加密處理。這個時候您就須要本身的類加載器來從某個網絡地址上讀取加密後的字節代碼,接着進行解密和驗證,最後定義出要在 Java 虛擬機中運行的類來。下面將經過兩個具體的實例來講明類加載器的開發。java
要建立自定義的類加載器只須要擴展java.lang.ClassLoader類就能夠,而後覆蓋它的findClass(String name)方法便可。該方法根據參數指定的類的名字,去找對應的class文件。而後返回class對應的對象。下面咱們就根據咱們自定義的類加載器的源碼來具體詳解一下這個自定義的步驟:數組
自定義的類加載器:安全
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; } @Override public 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 - 類數據的長度 */ @Override public 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\\,並設置父加載器爲loader1 MyClassLoader 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(); }
當執行這段代碼的時候。首先讓loader2去加載Sample類文件,固然咱們在執行這段代碼的前提時在各個默認加載器中已經有咱們Sample的class文件。Loader2首先讓父加載器是loader1去加載,而後loader1會讓系統類加載器去加載,系統類加載器會讓擴展類加載器加載,擴展類加載器會讓根類加載器加載,因爲系統類加載器,擴展類加載器,根類加載器的默認路徑中都沒有咱們要的sample類,因此loader2的默認路徑有sample這個類,也就是說loader2會去加載這個sample類。當執行test(loader3)的時候,因爲loader3的默認父加載器是根類加載器,而且根類加載前默認路徑沒有對應的sample.class文件,因此,直接的loader3類加載器就去加載這個類。app
最後要說明的一點是,自定義類加載不光只能從咱們本地加載到class文件,咱們也能夠加載網絡,即基本的場景是:Java字節代碼(.class)文件存放在服務器上,客戶端經過網絡的方式獲取字節代碼並執行。當有版本更新的時候,只須要替換掉服務器上保存的文件便可。經過類加載器能夠比較簡單的實現這種需求。其實他的實現和本地差很少,基本上就是geclassdata方法改變了一些。下面咱們來具體看一下:ide
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 類的方法。而使用接口的作法則是把接口的類放在客戶端中,從服務器上加載實現此接口的不一樣版本的類。在客戶端經過相同的接口來使用這些實現類。this
不一樣類加載器的命名空間關係:加密
1.同一個命名空間內的類是相互可見的。url
2.子加載器的命名空間包含全部父加載器的命名空間。所以子加載器加載的類能看見父加載器加載的類。例如系統類加載器加載的類能看見根類加載器加載的類。
3.由父加載器加載的類不能看見子加載器加載的類。
4.若是兩個加載器之間沒有直接或間接的父子關係,那麼它們各自加載的類相互不可見。
5.當兩個不一樣命名空間內的類相互不可見時,能夠採用Java的反射機制來訪問實例的屬性和方法。