Java虛擬機JVM學習06 自定義類加載器 父委托機制和命名空間的再討論
創建用戶自定義的類加載器
要創建用戶自定義的類加載器,只需要擴展java.lang.ClassLoader類,然后覆蓋它的findClass(String name)方法即可,該方法根據參數指定的類的名字,返回對應的Class對象的引用。
自定義類加載器的例子
代碼:
package com.mengdd.classloader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; 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; } @Override public Class<?> findClass(String name) throws ClassNotFoundException { // 重寫的時候把protected改為public // 獲取字節數組 byte[] data = this.loadClassData(name); // 將字節數組轉換成Class對象返回 return this.defineClass(name, data, 0, data.length); } /** * 得到class文件的二進制字節數組 * * @param name * @return */ private byte[] loadClassData(String name) { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = null; try { // 將完整類名中的.轉化成\ name = 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 e) { e.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (Exception e2) { } } return data; } // main方法用來測試 public static void main(String[] args) throws Exception { MyClassLoader loader1 = new MyClassLoader("loader1"); // loader1的父加載器是系統類加載器 // 系統類加載器會在classpath指定的目錄中加載類 loader1.setPath("d:\\myapp\\serverlib\\"); MyClassLoader loader2 = new MyClassLoader(loader1, "loader2"); // 將loader1作為loader2的父加載器 loader2.setPath("d:\\myapp\\clientlib\\"); MyClassLoader loader3 = new MyClassLoader(null, "loader3"); // loader3的父加載器是根類加載器 loader3.setPath("d:\\myapp\\otherlib\\"); // 測試加載 test(loader2); test(loader3); System.out.println("test2---------------"); // 測試不同命名空間的類的互相訪問 test2(loader3); } public static void test(ClassLoader loader) throws Exception { Class clazz = loader.loadClass("com.mengdd.classloader.Sample"); Object object = clazz.newInstance(); } public static void test2(ClassLoader loader) throws Exception { Class clazz = loader.loadClass("com.mengdd.classloader.Sample"); Sample object = (Sample) clazz.newInstance(); System.out.println("sample v1: " + object.v1); } }
其中Sample:
package com.mengdd.classloader; public class Sample { public int v1 = 1; public Sample() { System.out.println("Sample is loaded by: " + this.getClass().getClassLoader()); // 主動使用Dog類 new Dog(); } }
Dog類:
package com.mengdd.classloader; public class Dog { public Dog() { System.out.println("Dog is loaded by: " + this.getClass().getClassLoader()); } }
例子演示過程略,嘗試把class文件放在不同的路徑下,看輸出或者報錯結果。
主要結論就是驗證了父親委托機制。
采用loader1的時候由於其父類是系統類加載器(也即應用類加載器),所以如果可以在classpath中找到目標.class文件,則定義類加載器是系統類加載器,輸出類似:
sun.misc.Launcher$AppClassLoader@7448bc3d
每個類加載器都有自己的命名空間,命名空間由該加載器及所有父加載器所加載的類組成。
在Sample類中主動使用了Dog類,當執行Sample類的構造方法中的new Dog()語句時,Java虛擬機需要先加載Dog類,到底用哪個類加載器加載呢?
從打印結果可以看出,Java虛擬機會用Sample類的定義類加載器去加載Dog類,加載過程也同樣采用父親委托機制。
如果Sample類首次主動使用Dog時,Sample類的加載器及它的父加載器都無法加載Dog類,將會拋出找不到文件的異常。
不同類加載器的命名空間關系
同一個命名空間內的類是相互可見的,即可以互相訪問。
子加載器的命名空間包含所有父加載器的命名空間。
因此由子加載器加載的類能看見父加載器加載的類。
例如系統類加載器加載的類能看見根類加載器加載的類。
由父加載器加載的類不能看見子加載器加載的類。
可以理解為:由於子加載器中含有父加載器的引用,所以子加載器的范圍更大。
如果兩個加載器之間沒有直接或間接的父子關系,那么它們各自加載的類相互不可見。
比如這么一種情況:MyClassLoader類由系統類加載器加載,而Sample類由loader3類加載器加載,因此MyClassLoader類看不見Sample類。
在MyClassLoader類的main()方法中使用Sample類,會導致錯誤。
當兩個不同命名空間內的類互相不可見時,可采用Java反射機制來訪問對方實例的屬性和方法,即反射可以突破命名空間的限制。
參考資料
聖思園張龍老師Java SE視頻教程。
ClassLoader類:http://docs.oracle.com/javase/7/docs/api/
相關博文:
Java虛擬機JVM學習05 類加載器的父委托機制:http://www.cnblogs.com/mengdd/p/3562540.html