tomcat用到很多ClassLoader相關的代碼,如果缺乏這方面的背景知識,閱讀源碼會遇到很多障礙,所以本文首先總結一下這方面的內容,和tomcat源碼的關係不大
1 標準的ClassLoader體系
1.1 bootstrap
bootstrap classloader是由JVM啓動的,用於加載%JAVA_HOME%/jre/lib/下的JAVA平臺自身的類(比如rt.jar中的類等)。這個classloader位於JAVA類加載器鏈的頂端,是用C/C++開發的,而且JAVA應用中沒有任何途徑可以獲取到這個實例,它是JDK實現的一部分
1.2 extension
entension classloader用於加載%JAVA_HOME%/jre/lib/ext/下的類,它的實現類是sun.misc.Launcher$ExtClassLoader,是一個內部類
基本上,我們開發的JAVA應用都不太需要關注這個類
1.3 system
system classloader是jvm啓動時,根據classpath參數創建的類加載器(如果沒有顯式指定classpath,則以當前目錄作爲classpath)。在普通的JAVA應用中,它是最重要的類加載器,因爲我們寫的所有類,通常都是由它加載的。這個類加載器的實現類是sun.misc.Launch$AppClassLoader
用ClassLoader.getSystemClassLoader(),可以得到這個類加載器
1.4 custom
一般情況下,對於普通的JAVA應用,ClassLoader體系就到system爲止了。平時編程時,甚至都不會感受到classloader的存在
但是對於其他一些應用,比如web server,插件加載器等,就必須和ClassLoader打交道了。這時候默認的類加載器不能滿足需求了(類隔離、運行時加載等需求),需要自定義類加載器,並掛載到ClassLoader鏈中(默認會掛載到system classloader下面)
2 雙親委派模型
從上面的圖可以看到,classloader鏈,是一個自上而下的樹形結構。一般來說,java中的類加載,是遵循雙親委派模型的,即:
當一個classloader要加載一個類時,首先會委託給它的parent classloader來加載,如果parent找不到,纔會自己加載。如果最後也找不到,則會拋出熟悉的ClassNotFoundException
這個模型,是在最基礎的抽象類ClassLoader裏確定的:
- protected synchronized Class<?> loadClass(String name, boolean resolve)
- throws ClassNotFoundException
- {
- // First, check if the class has already been loaded
- Class c = findLoadedClass(name);
- if (c == null) {
- try {
- if (parent != null) {
- c = parent.loadClass(name, false);
- } else {
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- // ClassNotFoundException thrown if class not found
- // from the non-null parent class loader
- }
- if (c == null) {
- // If still not found, then invoke findClass in order
- // to find the class.
- c = findClass(name);
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
自定義ClassLoader的時候,一般來說,需要做的並不是覆蓋loadClass()方法,這樣的話就“破壞”了雙親委派模型;需要做的只是實現findClass()方法即可
不過,從上面的代碼也可以看出,雙親委派模型只是一種“建議”,並沒有強制保障的措施。如果自定義的ClassLoader無視此規定,直接自行加載,不將請求委託給parent,當然也是沒問題的
在實際情況中,雙親委派模型被“破壞”也是很常見的。比如在tomcat裏,webappx classloader就不會委託給上層的common classloader,而是先委託給system,然後自己加載,最後才委託給common;再比如說在OSGi裏,更是有意完全打破了這個規則
當然,對於普通的JAVA應用開發來說,需要自定義classloader的場景本來就不多,需要去違反雙親委派模型的場景,更是少之又少
3 自定義ClassLoader
3.1 自定義ClassLoader的一般做法
從上面的代碼可以看到,自定義ClassLoader很簡單,只要繼承抽象類ClassLoader,再實現findClass()方法就可以了
3.2 自定義ClassLoader的場景
事實上,需要實現新的ClassLoader的場景是很少的
注意:需要增加一個自定義ClassLoader的場景很多;但是,需要自己實現一個新的ClassLoader子類的場景不多。這是兩回事,不可混淆
比如,即使在tomcat裏,也沒有自行實現新的ClassLoader子類,只是創建了URLClassLoader的實例,作爲custom classloader
3.3 ClassLoader的子類
在JDK中已經提供了若干ClassLoader的子類,在需要的時候,可以直接創建實例並使用。其中最常用的是URLClassLoader,用於讀取一個URL下的資源,從中加載Class
- @Deprecated
- public class StandardClassLoader
- extends URLClassLoader
- implements StandardClassLoaderMBean {
- public StandardClassLoader(URL repositories[]) {
- super(repositories);
- }
- public StandardClassLoader(URL repositories[], ClassLoader parent) {
- super(repositories, parent);
- }
- }
可以看到,tomcat就是在URLClassLoader的基礎上,包裝了StandardClassLoader,實際上並沒有任何功能上的區別
3.4 設置parent
在抽象類ClassLoader中定義了一個parent字段,保存的是父加載器。但是這個字段是private的,並且沒有setter方法
這就意味着只能在構造方法中,一次性地設置parent classloader。如果沒有設置的話,則會默認將system classloader設置爲parent,這也是在ClassLoader類中確定的:
- protected ClassLoader(ClassLoader parent) {
- this(checkCreateClassLoader(), parent);
- }
- protected ClassLoader() {
- this(checkCreateClassLoader(), getSystemClassLoader());
- }
4 ClassLoader隱性傳遞
“隱性傳遞”這個詞是我亂造的,在網上和註釋中沒有找到合適的描述的詞
試想這樣一種場景:在應用中需要加載100個類,其中70個在classpath下,默認由system來加載,這部分不需要額外處理;另外30個類,由自定義classloader加載,比如在tomcat裏:
- Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
- Object startupInstance = startupClass.newInstance();
如上,org.apache.catalina.startup.Catalina是由自定義類加載器加載的,需要額外編程來處理(如果是system加載的,直接new就可以了)
如果30個類,都要通過這種方式來加載,就太麻煩了。不過classloader有一個特性,就是“隱性傳遞”,即:
如果一個ClassA是由某個ClassLoader加載的,那麼ClassA中依賴的需要加載的類,默認也會由同一個ClassLoader加載
這個機制是由JVM保證的,對於程序員來說是透明的
5 current classloader
5.1 定義
與前面說的extension、system等不同,在運行時並不存在一個實際的“current classloader”,只是一個抽象的概念。指的是一個類“當前的”類加載器。一個對象實例所屬的Class,是由哪一個ClassLoader加載的,這個ClassLoader就是這個對象實例的current classloader
獲得的方法是:
- this.getClass().getClassLoader();
5.2 實例
current classloader概念的意義,主要在於它會影響Class.forName()方法的表現,貼一段代碼進行說明:
- public class Test {
- public void tryForName() {
- System.out.println("current classloader: "
- + this.getClass().getClassLoader());
- try {
- Class.forName("net.kyfxbl.test.cl.Target");
- System.out.println("load class success");
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- }
- }
- }
這個類調用了Class.forName()方法,試圖加載net.kyfxbl.test.cl.Target類(Target是一個空類,作爲加載目標,不重要)。這個類在運行時能否加載Target成功,取決於它的current classloader,能不能加載到Target
首先,將Test和Target打成jar包,放到classpath之外,jar包中內容如下:
然後在工程中刪除Target類(classpath中加載不到Target了)
在Main中用system 加載Test,此時Test的current classloader是system,加載Target類失敗
- public static void main(String[] args) {
- Test t = new Test();
- t.tryForName();
- }
然後,這次用自定義的classloader來加載
- public static void main(String[] args) throws Exception {
- ClassLoader cl = createClassLoader();
- Class<?> startupClass = cl.loadClass("net.kyfxbl.test.cl.Test");
- Object startupInstance = startupClass.newInstance();
- String methodName = "tryForName";
- Class<?>[] paramTypes = new Class[0];
- Object[] paramValues = new Object[0];
- Method method = startupInstance.getClass().getMethod(methodName,
- paramTypes);
- method.invoke(startupInstance, paramValues);
- }
- private static ClassLoader createClassLoader() throws MalformedURLException {
- String filePath = "c://hehe.jar";
- File file = new File(filePath);
- URL url = file.toURI().toURL();
- URL[] urls = new URL[] { url };
- ClassLoader myClassLoader = new URLClassLoader(urls);
- return myClassLoader;
- }
在想象中,這次Test的current classloader應該變成URLClassLoader,並且加載Target成功。但是還是失敗了
這是因爲前面說過的“雙親委派模型”,URLClassLoader的parent是system classloader,由於工程裏的Test類沒有刪除,所以classpath裏還是能找到Test類,所以Test類的current classloader依然是system classloader,和第一次一樣
接下來把工程裏的Test類也刪除,這次就成功了
5.3 Class.forName()
前面說的是單個參數的forName()方法,默認使用current ClassLoader
除此之外,Class類還定義了3個參數的forName()方法,方法簽名如下:
- public static Class<?> forName(String name, boolean initialize,
- ClassLoader loader)
- throws ClassNotFoundException
這個方法的最後一個參數,可以傳遞一個ClassLoader,會用這個ClassLoader進行加載。這個方法很重要
比如像JNDI,主體類是在JDK包裏,由bootstrap加載。而SPI的實現類,則是由廠商提供,一般在classpath裏。那麼在JNDI的主體類裏,要加載SPI的實現類,直接用Class.forName()方法肯定是不行的,這時候就要用到3個參數的Class.forName()方法了
6 ContextClassLoader
6.1 獲取ClassLoader的API
前面說過,已經有2種方式可以獲取到ClassLoader的引用
一種是ClassLoader.getSystemClassLoader(),獲取的是system classloader
另一種是getClass().getClassLoader(),獲取的是current classloader
這2種API都只能獲取classloader,沒有辦法用來傳遞
6.2 傳遞ClassLoader
每一個thread,都有一個contextClassLoader,並且有getter和setter方法,用來在線程之間傳遞ClassLoader
有2條默認的規則:
首先,contextClassLoader默認是繼承的,在父線程中創建子線程,那麼子線程會繼承父線程的contextClassLoader
其次,主線程,也就是執行main()方法的那個線程,默認的contextClassLoader是system classloader
6.3 例子
對上面例子中的Test和Main稍微改一下(Test和Target依然打到jar包裏,然後從工程中刪除,避免被system classloader加載到)
- public class Test {
- public void tryForName() {
- System.out.println("current classloader: "
- + getClass().getClassLoader());
- System.out.println("thread context classloader: "
- + Thread.currentThread().getContextClassLoader());
- try {
- Class.forName("net.kyfxbl.test.cl.Target");
- System.out.println("load class success");
- } catch (Exception exc) {
- exc.printStackTrace();
- }
- }
- }
- public static void main(String[] args) throws Exception {
- ClassLoader cl = createClassLoader();
- Class<?> startupClass = cl.loadClass("net.kyfxbl.test.cl.Test");
- final Object startupInstance = startupClass.newInstance();
- new Thread(new Runnable() {
- @Override
- public void run() {
- String methodName = "tryForName";
- Class<?>[] paramTypes = new Class[0];
- Object[] paramValues = new Object[0];
- try {
- Method method = startupInstance.getClass().getMethod(
- methodName, paramTypes);
- method.invoke(startupInstance, paramValues);
- } catch (Exception exc) {
- exc.printStackTrace();
- }
- }
- }).start();
- }
這次的tryForName()方法在一個子線程中被調用,並依次打印出current classloader和contextClassLoader,如圖:
可以看到,子線程繼承了父線程的contextClassLoader
同時可以注意到,contextClassLoader對Class.forName()方法沒有影響,contextClassLoader只是起到在線程之間傳遞ClassLoader的作用
6.4 題外話
從這個例子還可以看出,一個方法在運行時的表現,在編譯期是無法確定的
在運行時的表現,有時候取決於方法所在的類是被哪個ClassLoader加載;有時候取決於是運行在單線程環境下,還是多線程環境下
這在編譯期是不可知的,所以在編程的時候,要考慮運行時的情況。比如所謂“線程安全”的類,並不是說它“一定”會運行在多線程環境下,而是說它“可以”運行在多線程環境下
7 總結
本文大致總結了ClassLoader的背景知識。掌握了背景,再閱讀tomcat的源碼,基本就不會遇到ClassLoader方面的困難
本文介紹了ClassLoader的標準體系、雙親委派模型、自定義ClassLoader的方法、以及current classloader和contextClassLoader的概念
其中最重要的是current classloader和contextClassLoader
用於獲取ClassLoader的API主要有3種:
ClassLoader.getSystemClassLoader();
Class.getClassLoader();
Thread.getContextClassLoader();
第一個是靜態方法,返回的永遠是system classloader,也就是說,沒什麼用
後面2個都是實例方法,一個是返回實例所屬的類的ClassLoader;另一個返回當前線程的contextClassLoader,具體的結果都要在運行時才能確定
其中,contextClassLoader可以起到傳遞ClassLoader的作用,所以特別重要