Classloader總結
類加載機制, 線程上下文加載器Thread.setContextClassLoader(), 自定義類加載器。
顧名思義, ClassLoader就是類加載器, 而類加載是java程序運行的第一步, 如果沒有類加載器來加載類,那么再牛逼的java程序也運行不了, 可見類加載器的重要性。理解類加載器的加載機制, 可以很好的幫助我們理解java類的執行過程, 深入理解java的原理, 幫助我們寫出更有效、更高效、更牛逼的程序。
委托機制
java的類加載器采用向上委托機制,需要加載一個類的時候,它的過程如下:
1. 先提交給父加載器去尋找這個類,父加載器再交給它的父加載器, 一直到最頂層的加載器BootstrapClassloader。</p>
2. 如果BootstrapClassloader加載器找到, 那么就直接將加載后的代碼交給發起加載過程的加載器去調用, 如果沒找到,就交給BootstrapClassloader他的子加載器,也就是ExtClassloader去加載。</p>
3. ExtClassloader如果加載成功, 就把加載后的代碼交給發起加載過程的加載器去調用, 如果沒找到,就交給ExtClassloader他的子加載器,也就是AppClassloader去加載。</p>
4. AppClassloader重復BootstrapClassloader、ExtClassloader類似的過程, 直到加載類成功, 或者找不到目標類, 拋出ClassNotFoundException。</p>
整個過程如下圖所示:
其中BootstrapClassloader是JVM提供的初始化類加載器, 它是所有類加載器的根, 隨著jvm啟動而啟動。
加載目錄
如上圖中, 我們看到每個加載器加載類的目錄都是指定好的, 這個指定好的目錄是怎么來的呢? 這里提供一段代碼, 大家可以看輸出結果和上圖中的比較。
@SuppressWarnings("restriction") public static void showClassLoaderPath() { System.out.println("BootstrapClassLoader: "); URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for(URL url : urls){ System.out.println(url.getPath()); } System.out.println("BootstrapClassloader的加載目錄: " + System.getProperty("sun.boot.class.path")); System.out.println("----------------------------"); URLClassLoader extClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader().getParent(); System.out.println(extClassLoader.getClass().getName() + ": "); urls = extClassLoader.getURLs(); for(URL url : urls) { System.out.println(url); } System.out.println("ExtClassloader的加載目錄: " + System.getProperty("java.ext.dirs")); System.out.println("----------------------------"); URLClassLoader appClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader(); System.out.println(appClassLoader.getClass().getName() + ": "); urls = appClassLoader.getURLs(); for(URL url : urls) { System.out.println(url); } System.out.println("AppClassloader的加載目錄: " + System.getProperty("java.class.path")); }
輸出結果如下:
BootstrapClassLoader: /D:/server/java/jdk1.6.0_10/jre/lib/resources.jar /D:/server/java/jdk1.6.0_10/jre/lib/rt.jar /D:/server/java/jdk1.6.0_10/jre/lib/sunrsasign.jar /D:/server/java/jdk1.6.0_10/jre/lib/jsse.jar /D:/server/java/jdk1.6.0_10/jre/lib/jce.jar /D:/server/java/jdk1.6.0_10/jre/lib/charsets.jar /D:/server/java/jdk1.6.0_10/jre/classes BootstrapClassloader的加載目錄: D:\server\java\jdk1.6.0_10\jre\lib\resources.jar;D:\server\java\jdk1.6.0_10\jre\lib\rt.jar;D:\server\java\jdk1.6.0_10\jre\lib\sunrsasign.jar;D:\server\java\jdk1.6.0_10\jre\lib\jsse.jar;D:\server\java\jdk1.6.0_10\jre\lib\jce.jar;D:\server\java\jdk1.6.0_10\jre\lib\charsets.jar;D:\server\java\jdk1.6.0_10\jre\classes ---------------------------- sun.misc.Launcher$ExtClassLoader: file:/D:/server/java/jdk1.6.0_10/jre/lib/ext/dnsns.jar file:/D:/server/java/jdk1.6.0_10/jre/lib/ext/localedata.jar file:/D:/server/java/jdk1.6.0_10/jre/lib/ext/sunjce_provider.jar file:/D:/server/java/jdk1.6.0_10/jre/lib/ext/sunmscapi.jar file:/D:/server/java/jdk1.6.0_10/jre/lib/ext/sunpkcs11.jar ExtClassloader的加載目錄: D:\server\java\jdk1.6.0_10\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext ---------------------------- sun.misc.Launcher$AppClassLoader: file:/D:/workspace/98_myproject/jtest/target/classes/ file:/C:/Users/lenovo/.m2/repository/aopalliance/aopalliance/1.0/aopalliance-1.0.jar file:/C:/Users/lenovo/.m2/repository/ch/qos/logback/logback-classic/1.1.2/logback-classic-1.1.2.jar file:/C:/Users/lenovo/.m2/repository/org/slf4j/slf4j-api/1.7.6/slf4j-api-1.7.6.jar file:/C:/Users/lenovo/.m2/repository/ch/qos/logback/logback-core/1.1.2/logback-core-1.1.2.jar file:/C:/Users/lenovo/.m2/repository/commons-codec/commons-codec/1.9/commons-codec-1.9.jar file:/C:/Users/lenovo/.m2/repository/commons-lang/commons-lang/2.6/commons-lang-2.6.jar file:/C:/Users/lenovo/.m2/repository/commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar file:/C:/Users/lenovo/.m2/repository/commons-configuration/commons-configuration/1.10/commons-configuration-1.10.jar file:/C:/Users/lenovo/.m2/repository/org/apache/httpcomponents/fluent-hc/4.3.3/fluent-hc-4.3.3.jar file:/C:/Users/lenovo/.m2/repository/org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar file:/C:/Users/lenovo/.m2/repository/org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar file:/C:/Users/lenovo/.m2/repository/org/apache/httpcomponents/httpclient-cache/4.3.3/httpclient-cache-4.3.3.jar file:/C:/Users/lenovo/.m2/repository/org/apache/httpcomponents/httpmime/4.3.3/httpmime-4.3.3.jar file:/C:/Users/lenovo/.m2/repository/org/quartz-scheduler/quartz/2.2.1/quartz-2.2.1.jar file:/C:/Users/lenovo/.m2/repository/postgresql/postgresql/9.1-901-1.jdbc4/postgresql-9.1-901-1.jdbc4.jar file:/C:/Users/lenovo/.m2/repository/commons-dbutils/commons-dbutils/1.5/commons-dbutils-1.5.jar file:/C:/Users/lenovo/.m2/repository/c3p0/c3p0/0.9.1.2/c3p0-0.9.1.2.jar file:/C:/Users/lenovo/.m2/repository/junit/junit/4.12-beta-2/junit-4.12-beta-2.jar file:/C:/Users/lenovo/.m2/repository/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar AppClassloader的加載目錄: D:\workspace\98_myproject\jtest\target\classes;C:\Users\lenovo\.m2\repository\aopalliance\aopalliance\1.0\aopalliance-1.0.jar;C:\Users\lenovo\.m2\repository\ch\qos\logback\logback-classic\1.1.2\logback-classic-1.1.2.jar;C:\Users\lenovo\.m2\repository\org\slf4j\slf4j-api\1.7.6\slf4j-api-1.7.6.jar;C:\Users\lenovo\.m2\repository\ch\qos\logback\logback-core\1.1.2\logback-core-1.1.2.jar;C:\Users\lenovo\.m2\repository\commons-codec\commons-codec\1.9\commons-codec-1.9.jar;C:\Users\lenovo\.m2\repository\commons-lang\commons-lang\2.6\commons-lang-2.6.jar;C:\Users\lenovo\.m2\repository\commons-logging\commons-logging\1.1.3\commons-logging-1.1.3.jar;C:\Users\lenovo\.m2\repository\commons-configuration\commons-configuration\1.10\commons-configuration-1.10.jar;C:\Users\lenovo\.m2\repository\org\apache\httpcomponents\fluent-hc\4.3.3\fluent-hc-4.3.3.jar;C:\Users\lenovo\.m2\repository\org\apache\httpcomponents\httpclient\4.3.3\httpclient-4.3.3.jar;C:\Users\lenovo\.m2\repository\org\apache\httpcomponents\httpcore\4.3.2\httpcore-4.3.2.jar;C:\Users\lenovo\.m2\repository\org\apache\httpcomponents\httpclient-cache\4.3.3\httpclient-cache-4.3.3.jar;C:\Users\lenovo\.m2\repository\org\apache\httpcomponents\httpmime\4.3.3\httpmime-4.3.3.jar;C:\Users\lenovo\.m2\repository\org\quartz-scheduler\quartz\2.2.1\quartz-2.2.1.jar;C:\Users\lenovo\.m2\repository\postgresql\postgresql\9.1-901-1.jdbc4\postgresql-9.1-901-1.jdbc4.jar;C:\Users\lenovo\.m2\repository\commons-dbutils\commons-dbutils\1.5\commons-dbutils-1.5.jar;C:\Users\lenovo\.m2\repository\c3p0\c3p0\0.9.1.2\c3p0-0.9.1.2.jar;C:\Users\lenovo\.m2\repository\junit\junit\4.12-beta-2\junit-4.12-beta-2.jar;C:\Users\lenovo\.m2\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar
可以看到和圖中所示的各個類加載器加載范圍是一致的, 同時我們也可以看到, jvm提供的加載器所加載的目錄所對應的系統屬性值。
依賴順序
那么問題來了。 假如我們的項目有個依賴包A被放到了jre/lib/ext目錄下, 而這個依賴包依賴的另一個依賴包B放在項目目錄, 這個時候我們可以加在成功嗎?
答案是不可以的。
因為java的類加載機制是向上委托, 而不是向下委托, 也就是說ExtClassloader可以調用BootstrapClassLoader加載的類, AppClassLoader可以調用ExtClassloader和BootstrapClassLoader加載的類, 而這個過程反過來是不行的。
在我們的這個問題中, 依賴包A被ExtClassloader加載, B被AppClassloader加載, 這個時候A要引用B包中的類, 按照我們上面講的機制是不可以的。 當然它并不是絕對的, java提供了一種繞開上述機制的方法, 下面我們會講到。
反向依賴
那么怎么可以突破向上委托這種機制呢?
JDK 1.2提供了一個叫線程上下文類加載器, 對應代碼就是java.lang.Thread中的方法getContextClassLoader()和setContextClassLoader(ClassLoader cl)用來獲取和設置線程的上下文類加載器。如果沒有通過setContextClassLoader(ClassLoader cl)方法進行設置的話,線程將繼承其父線程的上下文類加載器。Java 應用運行的初始線程的上下文類加載器是系統類加載器。
那么什么情況下以上類加載機制會失效呢?
Java 提供了很多服務提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實現。 常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的接口由 Java 核心庫來提供,如 JAXP 的 SPI 接口定義包含在 javax.xml.parsers包中。這些 SPI 的實現代碼很可能是作為 Java 應用所依賴的 jar 包被包含進來,可以通過類路徑(CLASSPATH)來找到,如實現了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代碼經常需要加載具體的實現類。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory類中的 newInstance()方法用來生成一個新的 DocumentBuilderFactory的實例。這里的實例的真正的類是繼承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實現所提供的。如在 Apache Xerces 中,實現的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而問題在于,SPI 的接口是 Java 核心庫的一部分,是由引導類加載器來加載的;SPI 實現的 Java 類一般是由系統類加載器來加載的。引導類加載器是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫。它也不能代理給系統類加載器,因為它是系統類加載器的祖先類加載器。也就是說,類加載器的代理模式無法解決這個問題。 引用自: http://www.ibm.com/developerworks/cn/java/j-lo-classloader/線程上下文類加載器正好解決了這個問題。如果不做任何的設置,Java 應用的線程的上下文類加載器默認就是系統上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就可以成功的加載到 SPI 實現的類。線程上下文類加載器在很多 SPI 的實現中都會用到。
自定義類加載器
大多數情況下jdk提供的類加載器已經足夠我們使用, 但是在某些特殊情況下, 需要我們編寫自定義的類加載器來實現特定功能, 如OSGI框架。如我們在網絡上傳輸類文件的時候對方加密了, 我們就要編寫對應解密的類加載器來加載對方發過來的類。
自定義類加載器主要需要注意的就是繼承ClassLoader, 并實現其中的findClass方法, 如下:
public class MyClassLoader extends ClassLoader{ @Override protected Class<?> findClass(String name) throws ClassNotFoundException {} }
這里再引用網絡上其他人寫好的一個類加載器示例來幫助大家更好的掌握自定義類加載器。
public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); 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 (IOException e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } }