天天看點

深入了解Java類加載器:線程上下文類加載器1 線程上下文類加載器2 何時使用Thread.getContextClassLoader()? 3 類加載器與Web容器 4 類加載器與OSGi 總結

1 線程上下文類加載器

  線程上下文類加載器(context class loader)是從 JDK 1.2 開始引入的。類 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用來擷取和設定線程的上下文類加載器。如果沒有通過 setContextClassLoader(ClassLoader cl)方法進行設定的話,線程将繼承其父線程的上下文類加載器。Java 應用運作的初始線程的上下文類加載器是系統類加載器。線上程中運作的代碼可以通過此類加載器來加載類和資源。

  前面提到的類加載器的代理模式并不能解決 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 的核心庫。它也不能代理給系統類加載器,因為它是系統類加載器的祖先類加載器。也就是說,類加載器的代理模式無法解決這個問題。

  線程上下文類加載器正好解決了這個問題。如果不做任何的設定,Java 應用的線程的上下文類加載器預設就是系統上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就可以成功的加載到 SPI 實作的類。線程上下文類加載器在很多 SPI 的實作中都會用到。

  Java預設的線程上下文類加載器是系統類加載器(AppClassLoader)。以下代碼摘自sun.misc.Launch的無參構造函數Launch()。

[java]

view plain copy print ?

  1. // Now create the class loader to use to launch the application  
  2. try {  
  3.     loader = AppClassLoader.getAppClassLoader(extcl);  
  4. } catch (IOException e) {  
  5.     throw new InternalError(  
  6. ”Could not create application class loader” );  
  7. }  
  8. // Also set the context class loader for the primordial thread.  
  9. Thread.currentThread().setContextClassLoader(loader);  
// Now create the class loader to use to launch the application
try {
    loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
    throw new InternalError(
"Could not create application class loader" );
}

// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);
           

  使用線程上下文類加載器,可以在執行線程中抛棄雙親委派加載鍊模式,使用線程上下文裡的類加載器加載類。典型的例子有:通過線程上下文來加載第三方庫jndi實作,而不依賴于雙親委派。大部分java application伺服器(jboss, tomcat..)也是采用contextClassLoader來處理web服務。還有一些采用hot swap特性的架構,也使用了線程上下文類加載器,比如 seasar (full stack framework in japenese)。

  線程上下文從根本解決了一般應用不能違背雙親委派模式的問題。使java類加載體系顯得更靈活。随着多核時代的來臨,相信多線程開發将會越來越多地進入程式員的實際編碼過程中。是以,在編寫基礎設施時, 通過使用線程上下文來加載類,應該是一個很好的選擇。

  當然,好東西都有利弊。使用線程上下文加載類,也要注意保證多個需要通信的線程間的類加載器應該是同一個,防止因為不同的類加載器導緻類型轉換異常(ClassCastException)。

  defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)是java.lang.Classloader提供給開發人員,用來自定義加載class的接口。使用該接口,可以動态的加載class檔案。例如在jdk中,URLClassLoader是配合findClass方法來使用defineClass,可以從網絡或硬碟上加載class。而使用類加載接口,并加上自己的實作邏輯,還可以定制出更多的進階特性。

  下面是一個簡單的hot swap類加載器實作。hot swap即熱插拔的意思,這裡表示一個類已經被一個加載器加載了以後,在不解除安裝它的情況下重新再加載它一次。我們知道Java預設的加載器對相同全名的類隻會加載一次,以後直接從緩存中取這個Class object。是以要實作hot swap,必須在加載的那一刻進行攔截,先判斷是否已經加載,若是則重新加載一次,否則直接首次加載它。我們從URLClassLoader繼承,加載類的過程都代理給系統類加載器URLClassLoader中的相應方法來完成。

[java]

view plain copy print ?

  1. package classloader;  
  2. import java.net.URL;  
  3. import java.net.URLClassLoader;  
  4. public class HotSwapClassLoader extends URLClassLoader {  
  5.     public HotSwapClassLoader(URL[] urls) {  
  6.         super(urls);  
  7.     }  
  8.     public HotSwapClassLoader(URL[] urls, ClassLoader parent) {  
  9.         super(urls, parent);  
  10.     }  
  11.     // 下面的兩個重載load方法實作類的加載,仿照ClassLoader中的兩個loadClass()  
  12.     // 具體的加載過程代理給父類中的相應方法來完成  
  13.     public Class<?> load(String name) throws ClassNotFoundException {  
  14.         return load(name, false);  
  15.     }  
  16.     public Class<?> load(String name, boolean resolve) throws ClassNotFoundException {  
  17.         // 若類已經被加載,則重新再加載一次  
  18.         if (null != super.findLoadedClass(name)) {  
  19.             return reload(name, resolve);  
  20.         }  
  21.         // 否則用findClass()首次加載它  
  22.         Class<?> clazz = super.findClass(name);  
  23.         if (resolve) {  
  24.             super.resolveClass(clazz);  
  25.         }  
  26.         return clazz;  
  27.     }  
  28.     public Class<?> reload(String name, boolean resolve) throws ClassNotFoundException {  
  29.         return new HotSwapClassLoader(super.getURLs(), super.getParent()).load(  
  30.                 name, resolve);  
  31.     }  
  32. }  
package classloader;

import java.net.URL;
import java.net.URLClassLoader;

/**
 * 可以重新載入同名類的類加載器實作
 * 放棄了雙親委派的加載鍊模式,需要外部維護重載後的類的成員變量狀态
 */
public class HotSwapClassLoader extends URLClassLoader {

    public HotSwapClassLoader(URL[] urls) {
        super(urls);
    }

    public HotSwapClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    // 下面的兩個重載load方法實作類的加載,仿照ClassLoader中的兩個loadClass()
    // 具體的加載過程代理給父類中的相應方法來完成
    public Class<?> load(String name) throws ClassNotFoundException {
        return load(name, false);
    }

    public Class<?> load(String name, boolean resolve) throws ClassNotFoundException {
        // 若類已經被加載,則重新再加載一次
        if (null != super.findLoadedClass(name)) {
            return reload(name, resolve);
        }
        // 否則用findClass()首次加載它
        Class<?> clazz = super.findClass(name);
        if (resolve) {
            super.resolveClass(clazz);
        }
        return clazz;
    }

    public Class<?> reload(String name, boolean resolve) throws ClassNotFoundException {
        return new HotSwapClassLoader(super.getURLs(), super.getParent()).load(
                name, resolve);
    }
}
           

  兩個重載的load方法參數與ClassLoader類中的兩個loadClass()相似。在load的實作中,用findLoadedClass()查找指定的類是否已經被祖先加載器加載了,若已加載則重新再加載一次,進而放棄了雙親委派的方式(這種方式隻會加載一次)。若沒有加載則用自身的findClass()來首次加載它。

  下面是使用示例:

[java]

view plain copy print ?

  1. package classloader;  
  2. public class A {  
  3.     private B b;  
  4.     public void setB(B b) {  
  5.         this.b = b;  
  6.     }  
  7.     public B getB() {  
  8.         return b;  
  9.     }  
  10. }  
package classloader;

public class A {

    private B b;

    public void setB(B b) {
        this.b = b;
    }

    public B getB() {
        return b;
    }
}
           
[java]

view plain copy print ?

  1. package classloader;  
  2. public class B {  
  3. }  
package classloader;

public class B {

}
           
[java]

view plain copy print ?

  1. package classloader;  
  2. import java.lang.reflect.InvocationTargetException;  
  3. import java.lang.reflect.Method;  
  4. import java.net.MalformedURLException;  
  5. import java.net.URL;  
  6. public class TestHotSwap {  
  7.     public static void main(String args[]) throws MalformedURLException {  
  8.         A a = new A();  // 加載類A  
  9.         B b = new B();  // 加載類B  
  10.         a.setB(b);  // A引用了B,把b對象拷貝到A.b  
  11.         System.out.printf(”A classLoader is %s\n”, a.getClass().getClassLoader());  
  12.         System.out.printf(”B classLoader is %s\n”, b.getClass().getClassLoader());  
  13.         System.out.printf(”A.b classLoader is %s\n”, a.getB().getClass().getClassLoader());  
  14.         try {  
  15.             URL[] urls = new URL[]{ new URL(“file:///C:/Users/JackZhou/Documents/NetBeansProjects/classloader/build/classes/”) };  
  16.             HotSwapClassLoader c1 = new HotSwapClassLoader(urls, a.getClass().getClassLoader());  
  17.             Class clazz = c1.load(”classloader.A”);  // 用hot swap重新加載類A  
  18.             Object aInstance = clazz.newInstance();  // 建立A類對象  
  19.             Method method1 = clazz.getMethod(”setB”, B.class);  // 擷取setB(B b)方法  
  20.             method1.invoke(aInstance, b);    // 調用setB(b)方法,重新把b對象拷貝到A.b  
  21.             Method method2 = clazz.getMethod(”getB”);  // 擷取getB()方法  
  22.             Object bInstance = method2.invoke(aInstance);  // 調用getB()方法  
  23.             System.out.printf(”Reloaded A.b classLoader is %s\n”, bInstance.getClass().getClassLoader());  
  24.         } catch (MalformedURLException | ClassNotFoundException |   
  25.                 InstantiationException | IllegalAccessException |   
  26.                 NoSuchMethodException | SecurityException |   
  27.                 IllegalArgumentException | InvocationTargetException e) {  
  28.             e.printStackTrace();  
  29.         }  
  30.     }  
  31. }  
package classloader;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;

public class TestHotSwap {

    public static void main(String args[]) throws MalformedURLException {
        A a = new A();  // 加載類A
        B b = new B();  // 加載類B
        a.setB(b);  // A引用了B,把b對象拷貝到A.b
        System.out.printf("A classLoader is %s\n", a.getClass().getClassLoader());
        System.out.printf("B classLoader is %s\n", b.getClass().getClassLoader());
        System.out.printf("A.b classLoader is %s\n", a.getB().getClass().getClassLoader());

        try {
            URL[] urls = new URL[]{ new URL("file:///C:/Users/JackZhou/Documents/NetBeansProjects/classloader/build/classes/") };
            HotSwapClassLoader c1 = new HotSwapClassLoader(urls, a.getClass().getClassLoader());
            Class clazz = c1.load("classloader.A");  // 用hot swap重新加載類A
            Object aInstance = clazz.newInstance();  // 建立A類對象
            Method method1 = clazz.getMethod("setB", B.class);  // 擷取setB(B b)方法
            method1.invoke(aInstance, b);    // 調用setB(b)方法,重新把b對象拷貝到A.b
            Method method2 = clazz.getMethod("getB");  // 擷取getB()方法
            Object bInstance = method2.invoke(aInstance);  // 調用getB()方法
            System.out.printf("Reloaded A.b classLoader is %s\n", bInstance.getClass().getClassLoader());
        } catch (MalformedURLException | ClassNotFoundException | 
                InstantiationException | IllegalAccessException | 
                NoSuchMethodException | SecurityException | 
                IllegalArgumentException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}
           

  運作輸出:

[java]

view plain copy print ?

  1. A classLoader is sun.misc.LauncherAppClassLoader</span><span class="annotation">@73d16e93</span><span>&nbsp;&nbsp;</span></span></li><li class=""><span>B&nbsp;classLoader&nbsp;is&nbsp;sun.misc.Launcher AppClassLoader</span><span class="annotation">@73d16e93</span><span>&nbsp;&nbsp;</span></span></li><li class=""><span>B&nbsp;classLoader&nbsp;is&nbsp;sun.misc.Launcher
  2. B classLoader is sun.misc.LauncherAppClassLoader@73d16e93  
  3. A.b classLoader is sun.misc.LauncherAppClassLoader<span class="annotation">@73d16e93</span><span>&nbsp;&nbsp;</span></span></li><li class=""><span>Reloaded&nbsp;A.b&nbsp;classLoader&nbsp;is&nbsp;sun.misc.Launcher AppClassLoader<span class="annotation">@73d16e93</span><span>&nbsp;&nbsp;</span></span></li><li class=""><span>Reloaded&nbsp;A.b&nbsp;classLoader&nbsp;is&nbsp;sun.misc.Launcher
  4. Reloaded A.b classLoader is sun.misc.LauncherAppClassLoader@73d16e93  
A classLoader is [email protected]
B classLoader is [email protected]
A.b classLoader is [email protected]
Reloaded A.b classLoader is [email protected]
           

  HotSwapClassLoader加載器的作用是重新加載同名的類。為了實作hot swap,一個類在加載過後,若重新再加載一次,則新的Class object的狀态會改變,老的狀态資料需要通過其他方式拷貝到重新加載過的類生成的全新Class object執行個體中來。上面A類引用了B類,加載A時也會加載B(如果B已經加載,則直接從緩存中取出)。在重新加載A後,其Class object中的成員b會重置,是以要重新調用setB(b)拷貝一次。你可以注釋掉這行代碼,再運作會抛出java.lang.NullPointerException,訓示A.b為null。

  注意新的A Class object執行個體所依賴的B類Class object,如果它與老的B Class object執行個體不是同一個類加載器加載的, 将會抛出類型轉換異常(ClassCastException),表示兩種不同的類。是以在重新加載A後,要特别注意給它的B類成員b傳入外部值時,它們是否由同一個類加載器加載。為了解決這種問題, HotSwapClassLoader自定義的l/oad方法中,目前類(類A)是由自身classLoader加載的, 而内部依賴的類(類B)還是老對象的classLoader加載的。

2 何時使用Thread.getContextClassLoader()?

  這是一個很常見的問題,但答案卻很難回答。這個問題通常在需要動态加載類和資源的系統程式設計時會遇到。總的說來動态加載資源時,往往需要從三種類加載器裡選擇:系統或程式的類加載器、目前類加載器、以及目前線程的上下文類加載器。在程式中應該使用何種類加載器呢?

  系統類加載器通常不會使用。此類加載器處理啟動應用程式時classpath指定的類,可以通過ClassLoader.getSystemClassLoader()來獲得。所有的ClassLoader.getSystemXXX()接口也是通過這個類加載器加載的。一般不要顯式調用這些方法,應該讓其他類加載器代理到系統類加載器上。由于系統類加載器是JVM最後建立的類加載器,這樣代碼隻會适應于簡單指令行啟動的程式。一旦代碼移植到EJB、Web應用或者Java Web Start應用程式中,程式肯定不能正确執行。

  是以一般隻有兩種選擇,目前類加載器和線程上下文類加載器。目前類加載器是指目前方法所在類的加載器。這個類加載器是運作時類解析使用的加載器,Class.forName(String)和Class.getResource(String)也使用該類加載器。代碼中X.class的寫法使用的類加載器也是這個類加載器。

  線程上下文類加載器在Java 2(J2SE)時引入。每個線程都有一個關聯的上下文類加載器。如果你使用new Thread()方式生成新的線程,新線程将繼承其父線程的上下文類加載器。如果程式對線程上下文類加載器沒有任何改動的話,程式中所有的線程将都使用系統類加載器作為上下文類加載器。Web應用和Java企業級應用中,應用伺服器經常要使用複雜的類加載器結構來實作JNDI(Java命名和目錄接口)、線程池、元件熱部署等功能,是以了解這一點尤其重要。

  為什麼要引入線程的上下文類加載器?将它引入J2SE并不是純粹的噱頭,由于Sun沒有提供充分的文檔解釋說明這一點,這使許多開發者很糊塗。實際上,上下文類加載器為同樣在J2SE中引入的類加載代理機制提供了後門。通常JVM中的類加載器是按照層次結構組織的,目的是每個類加載器(除了啟動整個JVM的原初類加載器)都有一個父類加載器。當類加載請求到來時,類加載器通常首先将請求代理給父類加載器。隻有當父類加載器失敗後,它才試圖按照自己的算法查找并定義目前類。

  有時這種模式并不能總是奏效。這通常發生在JVM核心代碼必須動态加載由應用程式動态提供的資源時。拿JNDI為例,它的核心是由JRE核心類(rt.jar)實作的。但這些核心JNDI類必須能加載由第三方廠商提供的JNDI實作。這種情況下調用父類加載器(原初類加載器)來加載隻有其子類加載器可見的類,這種代理機制就會失效。解決辦法就是讓核心JNDI類使用線程上下文類加載器,進而有效的打通類加載器層次結構,逆着代理機制的方向使用類加載器。

  順便提一下,XML解析API(JAXP)也是使用此種機制。當JAXP還是J2SE擴充時,XML解析器使用目前類加載器方法來加載解析器實作。但當JAXP成為J2SE核心代碼後,類加載機制就換成了使用線程上下文加載器,這和JNDI的原因相似。

  好了,現在我們明白了問題的關鍵:這兩種選擇不可能适應所有情況。一些人認為線程上下文類加載器應成為新的标準。但這在不同JVM線程共享資料來溝通時,就會使類加載器的結構亂七八糟。除非所有線程都使用同一個上下文類加載器。而且,使用目前類加載器已成為預設規則,它們廣泛應用在類聲明、Class.forName等情景中。即使你想盡可能隻使用上下文類加載器,總是有這樣那樣的代碼不是你所能控制的。這些代碼都使用代理到目前類加載器的模式。混雜使用代理模式是很危險的。

  更為糟糕的是,某些應用伺服器将目前類加載器和上下文類加器分别設定成不同的ClassLoader執行個體。雖然它們擁有相同的類路徑,但是它們之間并不存在父子代理關系。想想這為什麼可怕:記住加載并定義某個類的類加載器是虛拟機内部辨別該類的組成部分,如果目前類加載器加載類X并接着執行它,如JNDI查找類型為Y的資料,上下文類加載器能夠加載并定義Y,這個Y的定義和目前類加載器加載的相同名稱的類就不是同一個,使用隐式類型轉換就會造成異常。

  這種混亂的狀況還将在Java中存在很長時間。在J2SE中還包括以下的功能使用不同的類加載器:

  (1)JNDI使用線程上下文類加載器。

  (2)Class.getResource()和Class.forName()使用目前類加載器。

  (3)JAXP使用上下文類加載器。

  (4)java.util.ResourceBundle使用調用者的目前類加載器。

  (5)URL協定處理器使用java.protocol.handler.pkgs系統屬性并隻使用系統類加載器。

  (6)Java序列化API預設使用調用者目前的類加載器。

  這些類加載器非常混亂,沒有在J2SE文檔中給以清晰明确的說明。

  該如何選擇類加載器?

  如若代碼是限于某些特定架構,這些架構有着特定加載規則,則不要做任何改動,讓架構開發者來保證其工作(比如應用伺服器提供商,盡管他們并不能總是做對)。如在Web應用和EJB中,要使用Class.gerResource來加載資源。

  在其他情況下,我們可以自己來選擇最合适的類加載器。可以使用政策模式來設計選擇機制。其思想是将“總是使用上下文類加載器”或者“總是使用目前類加載器”的決策同具體實作邏輯分離開。往往設計之初是很難預測何種類加載政策是合适的,該設計能夠讓你可以後來修改類加載政策。

  考慮使用下面的代碼,這是作者本人在工作中發現的經驗。這兒有一個預設實作,應該可以适應大部分工作場景:

[java]

view plain copy print ?

  1. package classloader.context;  
  2. public class ClassLoadContext {  
  3.     private final Class m_caller;  
  4.     public final Class getCallerClass() {  
  5.         return m_caller;  
  6.     }  
  7.     ClassLoadContext(final Class caller) {  
  8.         m_caller = caller;  
  9.     }  
  10. }  
package classloader.context;

/**
 * 類加載上下文,持有要加載的類
 */
public class ClassLoadContext {

    private final Class m_caller;

    public final Class getCallerClass() {
        return m_caller;
    }

    ClassLoadContext(final Class caller) {
        m_caller = caller;
    }
}
           
[java]

view plain copy print ?

  1. package classloader.context;  
  2. public interface IClassLoadStrategy {  
  3.     ClassLoader getClassLoader(ClassLoadContext ctx);  
  4. }  
package classloader.context;

/**
 * 類加載政策接口
 */
public interface IClassLoadStrategy {

    ClassLoader getClassLoader(ClassLoadContext ctx);
}
           
[java]

view plain copy print ?

  1. public class DefaultClassLoadStrategy implements IClassLoadStrategy {  
  2.     @Override  
  3.     public ClassLoader getClassLoader(final ClassLoadContext ctx) {  
  4.         final ClassLoader callerLoader = ctx.getCallerClass().getClassLoader();  
  5.         final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();  
  6.         ClassLoader result;  
  7.         // If ‘callerLoader’ and ‘contextLoader’ are in a parent-child  
  8.         // relationship, always choose the child:  
  9.         if (isChild(contextLoader, callerLoader)) {  
  10.             result = callerLoader;  
  11.         } else if (isChild(callerLoader, contextLoader)) {  
  12.             result = contextLoader;  
  13.         } else {  
  14.             // This else branch could be merged into the previous one,  
  15.             // but I show it here to emphasize the ambiguous case:  
  16.             result = contextLoader;  
  17.         }  
  18.         final ClassLoader systemLoader = ClassLoader.getSystemClassLoader();  
  19.         // Precaution for when deployed as a bootstrap or extension class:  
  20.         if (isChild(result, systemLoader)) {  
  21.             result = systemLoader;  
  22.         }  
  23.         return result;  
  24.     }  
  25.     // 判斷anotherLoader是否是oneLoader的child  
  26.     private boolean isChild(ClassLoader oneLoader, ClassLoader anotherLoader){  
  27.         //…  
  28.     }  
  29.     // … more methods   
  30. }  
/**
 * 預設的類加載政策,可以适應大部分工作場景
 */
public class DefaultClassLoadStrategy implements IClassLoadStrategy {

    /**
     * 為ctx傳回最合适的類加載器,從系統類加載器、目前類加載器
     * 和目前線程上下文類加載中選擇一個最底層的加載器
     * @param ctx
     * @return 
     */
    @Override
    public ClassLoader getClassLoader(final ClassLoadContext ctx) {
        final ClassLoader callerLoader = ctx.getCallerClass().getClassLoader();
        final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
        ClassLoader result;

        // If 'callerLoader' and 'contextLoader' are in a parent-child
        // relationship, always choose the child:
        if (isChild(contextLoader, callerLoader)) {
            result = callerLoader;
        } else if (isChild(callerLoader, contextLoader)) {
            result = contextLoader;
        } else {
            // This else branch could be merged into the previous one,
            // but I show it here to emphasize the ambiguous case:
            result = contextLoader;
        }
        final ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
        // Precaution for when deployed as a bootstrap or extension class:
        if (isChild(result, systemLoader)) {
            result = systemLoader;
        }

        return result;
    }

    // 判斷anotherLoader是否是oneLoader的child
    private boolean isChild(ClassLoader oneLoader, ClassLoader anotherLoader){
        //...
    }

    // ... more methods 
}
           

  決定應該使用何種類加載器的接口是IClassLoaderStrategy,為了幫助IClassLoadStrategy做決定,給它傳遞了個ClassLoadContext對象作為參數。ClassLoadContext持有要加載的類。

  上面代碼的邏輯很簡單:如調用類的目前類加載器和上下文類加載器是父子關系,則總是選擇子類加載器。對子類加載器可見的資源通常是對父類可見資源的超集,是以如果每個開發者都遵循J2SE的代理規則,這樣做大多數情況下是合适的。

  目前類加載器和上下文類加載器是兄弟關系時,決定使用哪一個是比較困難的。理想情況下,Java運作時不應産生這種模糊。但一旦發生,上面代碼選擇上下文類加載器。這是作者本人的實際經驗,絕大多數情況下應該能正常工作。你可以修改這部分代碼來适應具體需要。一般來說,上下文類加載器要比目前類加載器更适合于架構程式設計,而目前類加載器則更适合于業務邏輯程式設計。

  最後需要檢查一下,以便保證所選類加載器不是系統類加載器的父親,在開發标準擴充類庫時這通常是個好習慣。

  注意作者故意沒有檢查要加載資源或類的名稱。Java XML API成為J2SE核心的曆程應該能讓我們清楚過濾類名并不是好想法。作者也沒有試圖檢查哪個類加載器加載首先成功,而是檢查類加載器的父子關系,這是更好更有保證的方法。

  下面是類加載器的選擇器:

[java]

view plain copy print ?

  1. package classloader.context;  
  2. public abstract class ClassLoaderResolver {  
  3.     private static IClassLoadStrategy s_strategy;  // initialized in <clinit>  
  4.     private static final int CALL_CONTEXT_OFFSET = 3;  // may need to change if this class is redesigned  
  5.     private static final CallerResolver CALLER_RESOLVER;  // set in <clinit>  
  6.     static {  
  7.         try {  
  8.             // This can fail if the current SecurityManager does not allow  
  9.             // RuntimePermission (“createSecurityManager”):  
  10.             CALLER_RESOLVER = new CallerResolver();  
  11.         } catch (SecurityException se) {  
  12.             throw new RuntimeException(“ClassLoaderResolver: could not create CallerResolver: ” + se);  
  13.         }  
  14.         s_strategy = new DefaultClassLoadStrategy();  //預設使用預設加載政策  
  15.     }  
  16.     public static synchronized ClassLoader getClassLoader() {  
  17.         final Class caller = getCallerClass(0); // 擷取執行目前方法的類  
  18.         final ClassLoadContext ctx = new ClassLoadContext(caller);  // 建立類加載上下文  
  19.         return s_strategy.getClassLoader(ctx);  // 擷取最合适的類加載器  
  20.     }  
  21.     public static synchronized IClassLoadStrategy getStrategy() {  
  22.         return s_strategy;  
  23.     }  
  24.     public static synchronized IClassLoadStrategy setStrategy(final IClassLoadStrategy strategy) {  
  25.         final IClassLoadStrategy old = s_strategy;  // 設定類加載政策  
  26.         s_strategy = strategy;  
  27.         return old;  
  28.     }  
  29.     private static final class CallerResolver extends SecurityManager {  
  30.         @Override  
  31.         protected Class[] getClassContext() {  
  32.             return super.getClassContext();  // 擷取當執行棧的所有類,native方法  
  33.         }  
  34.     }  
  35.     private static Class getCallerClass(final int callerOffset) {  
  36.         return CALLER_RESOLVER.getClassContext()[CALL_CONTEXT_OFFSET  
  37.                 + callerOffset];  // 擷取執行棧上某個方法所屬的類  
  38.     }  
  39. }  
package classloader.context;

/**
 * 類加載解析器,擷取最合适的類加載器
 */
public abstract class ClassLoaderResolver {

    private static IClassLoadStrategy s_strategy;  // initialized in <clinit>
    private static final int CALL_CONTEXT_OFFSET = 3;  // may need to change if this class is redesigned
    private static final CallerResolver CALLER_RESOLVER;  // set in <clinit>

    static {
        try {
            // This can fail if the current SecurityManager does not allow
            // RuntimePermission ("createSecurityManager"):
            CALLER_RESOLVER = new CallerResolver();
        } catch (SecurityException se) {
            throw new RuntimeException("ClassLoaderResolver: could not create CallerResolver: " + se);
        }
        s_strategy = new DefaultClassLoadStrategy();  //預設使用預設加載政策
    }

    /**
     * This method selects the best classloader instance to be used for
     * class/resource loading by whoever calls this method. The decision
     * typically involves choosing between the caller's current, thread context,
     * system, and other classloaders in the JVM and is made by the {@link IClassLoadStrategy}
     * instance established by the last call to {@link #setStrategy}.
     * 
     * @return classloader to be used by the caller ['null' indicates the
     * primordial loader]
     */
    public static synchronized ClassLoader getClassLoader() {
        final Class caller = getCallerClass(0); // 擷取執行目前方法的類
        final ClassLoadContext ctx = new ClassLoadContext(caller);  // 建立類加載上下文
        return s_strategy.getClassLoader(ctx);  // 擷取最合适的類加載器
    }

    public static synchronized IClassLoadStrategy getStrategy() {
        return s_strategy;
    }

    public static synchronized IClassLoadStrategy setStrategy(final IClassLoadStrategy strategy) {
        final IClassLoadStrategy old = s_strategy;  // 設定類加載政策
        s_strategy = strategy;
        return old;
    }

    /**
     * A helper class to get the call context. It subclasses SecurityManager
     * to make getClassContext() accessible. An instance of CallerResolver
     * only needs to be created, not installed as an actual security manager.
     */
    private static final class CallerResolver extends SecurityManager {
        @Override
        protected Class[] getClassContext() {
            return super.getClassContext();  // 擷取當執行棧的所有類,native方法
        }

    }

    /*
     * Indexes into the current method call context with a given
     * offset.
     */
    private static Class getCallerClass(final int callerOffset) {
        return CALLER_RESOLVER.getClassContext()[CALL_CONTEXT_OFFSET
                + callerOffset];  // 擷取執行棧上某個方法所屬的類
    }
}
           

  可通過調用 ClassLoaderResolver.getClassLoader() 方法來擷取類加載器對象,并使用其 ClassLoader 的接口如loadClass()等來加載類和資源。此外還可使用下面的 ResourceLoader 接口來取代 ClassLoader 接口:

[java]

view plain copy print ?

  1. package classloader.context;  
  2. import java.net.URL;  
  3. public class ResourceLoader {  
  4.     public static Class<?> loadClass(final String name) throws ClassNotFoundException {  
  5.         //擷取最合适的類加載器  
  6.         final ClassLoader loader = ClassLoaderResolver.getClassLoader();  
  7.         //用指定加載器加載類  
  8.         return Class.forName(name, false, loader);  
  9.     }  
  10.     public static URL getResource(final String name) {  
  11.         //擷取最合适的類加載器  
  12.         final ClassLoader loader = ClassLoaderResolver.getClassLoader();  
  13.         //查找指定的資源  
  14.         if (loader != null) {  
  15.             return loader.getResource(name);  
  16.         } else {  
  17.             return ClassLoader.getSystemResource(name);  
  18.         }  
  19.     }  
  20.     // … more methods …  
  21. }  
package classloader.context;

import java.net.URL;

public class ResourceLoader {

    /**
     * 加載一個類
     * 
     * @param name
     * @return 
     * @throws java.lang.ClassNotFoundException 
     * @see java.lang.ClassLoader#loadClass(java.lang.String)
     */
    public static Class<?> loadClass(final String name) throws ClassNotFoundException {
        //擷取最合适的類加載器
        final ClassLoader loader = ClassLoaderResolver.getClassLoader();
        //用指定加載器加載類
        return Class.forName(name, false, loader);
    }

    /**
     * 加載一個資源
     * 
     * @param name
     * @return 
     * @see java.lang.ClassLoader#getResource(java.lang.String)
     */
    public static URL getResource(final String name) {
        //擷取最合适的類加載器
        final ClassLoader loader = ClassLoaderResolver.getClassLoader();
        //查找指定的資源
        if (loader != null) {
            return loader.getResource(name);
        } else {
            return ClassLoader.getSystemResource(name);
        }
    }

    // ... more methods ...
}
           

  ClassLoadContext.getCallerClass()傳回的類在ClassLoaderResolver或ResourceLoader使用,這樣做的目的是讓其能找到調用類的類加載器(上下文加載器總是能通過Thread.currentThread().getContextClassLoader()來獲得)。注意調用類是靜态獲得的,是以這個接口不需現有業務方法增加額外的Class參數,而且也适合于靜态方法和類初始化代碼。具體使用時,可以往這個上下文對象中添加具體部署環境中所需的其他屬性。

3 類加載器與Web容器

  對于運作在 Java EE容器中的 Web 應用來說,類加載器的實作方式與一般的 Java 應用有所不同。不同的 Web 容器的實作方式也會有所不同。以 Apache Tomcat 來說,每個 Web 應用都有一個對應的類加載器執行個體。該類加載器也使用代理模式,所不同的是它是首先嘗試去加載某個類,如果找不到再代理給父類加載器。這與一般類加載器的順序是相反的。這是 Java Servlet 規範中的推薦做法,其目的是使得 Web 應用自己的類的優先級高于 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查找範圍之内的。這也是為了保證 Java 核心庫的類型安全。

  絕大多數情況下,Web 應用的開發人員不需要考慮與類加載器相關的細節。下面給出幾條簡單的原則:

  (1)每個 Web 應用自己的 Java 類檔案和使用的庫的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目錄下面。

  (2)多個應用共享的 Java 類檔案和 jar 包,分别放在 Web 容器指定的由所有 Web 應用共享的目錄下面。

  (3)當出現找不到類的錯誤時,檢查目前類的類加載器和目前線程的上下文類加載器是否正确。

4 類加載器與OSGi

  OSGi是 Java 上的動态子產品系統。它為開發人員提供了面向服務和基于元件的運作環境,并提供标準的方式用來管理軟體的生命周期。OSGi 已經被實作和部署在很多産品上,在開源社群也得到了廣泛的支援。Eclipse就是基于OSGi 技術來建構的。

  OSGi 中的每個子產品(bundle)都包含 Java 包和類。子產品可以聲明它所依賴的需要導入(import)的其它子產品的 Java 包和類(通過 Import-Package),也可以聲明導出(export)自己的包和類,供其它子產品使用(通過 Export-Package)。也就是說需要能夠隐藏和共享一個子產品中的某些 Java 包和類。這是通過 OSGi 特有的類加載器機制來實作的。OSGi 中的每個子產品都有對應的一個類加載器。它負責加載子產品自己包含的 Java 包和類。當它需要加載 Java 核心庫的類時(以 java開頭的包和類),它會代理給父類加載器(通常是啟動類加載器)來完成。當它需要加載所導入的 Java 類時,它會代理給導出此 Java 類的子產品來完成加載。子產品也可以顯式的聲明某些 Java 包和類,必須由父類加載器來加載。隻需要設定系統屬性 org.osgi.framework.bootdelegation的值即可。

  假設有兩個子產品 bundleA 和 bundleB,它們都有自己對應的類加載器 classLoaderA 和 classLoaderB。在 bundleA 中包含類 com.bundleA.Sample,并且該類被聲明為導出的,也就是說可以被其它子產品所使用的。bundleB 聲明了導入 bundleA 提供的類 com.bundleA.Sample,并包含一個類 com.bundleB.NewSample繼承自 com.bundleA.Sample。在 bundleB 啟動的時候,其類加載器 classLoaderB 需要加載類 com.bundleB.NewSample,進而需要加載類 com.bundleA.Sample。由于 bundleB 聲明了類 com.bundleA.Sample是導入的,classLoaderB 把加載類 com.bundleA.Sample的工作代理給導出該類的 bundleA 的類加載器 classLoaderA。classLoaderA 在其子產品内部查找類 com.bundleA.Sample并定義它,所得到的類 com.bundleA.Sample執行個體就可以被所有聲明導入了此類的子產品使用。對于以 java開頭的類,都是由父類加載器來加載的。如果聲明了系統屬性 org.osgi.framework.bootdelegation=com.example.core.*,那麼對于包 com.example.core中的類,都是由父類加載器來完成的。

  OSGi 子產品的這種類加載器結構,使得一個類的不同版本可以共存在 Java 虛拟機中,帶來了很大的靈活性。不過它的這種不同,也會給開發人員帶來一些麻煩,尤其當子產品需要使用第三方提供的庫的時候。下面提供幾條比較好的建議:

  (1)如果一個類庫隻有一個子產品使用,把該類庫的 jar 包放在子產品中,在 Bundle-ClassPath中指明即可。

  (2)如果一個類庫被多個子產品共用,可以為這個類庫單獨的建立一個子產品,把其它子產品需要用到的 Java 包聲明為導出的。其它子產品聲明導入這些類。

  (3)如果類庫提供了 SPI 接口,并且利用線程上下文類加載器來加載 SPI 實作的 Java 類,有可能會找不到 Java 類。如果出現了 NoClassDefFoundError異常,首先檢查目前線程的上下文類加載器是否正确。通過 Thread.currentThread().getContextClassLoader()就可以得到該類加載器。該類加載器應該是該子產品對應的類加載器。如果不是的話,可以首先通過 class.getClassLoader()來得到子產品對應的類加載器,再通過 Thread.currentThread().setContextClassLoader()來設定目前線程的上下文類加載器。

總結

  類加載器是 Java 語言的一個創新。它使得動态安裝和更新軟體元件成為可能。本文詳細介紹了類加載器的相關話題,包括基本概念、代理模式、線程上下文類加載器、與 Web 容器和 OSGi 的關系等。開發人員在遇到 ClassNotFoundException和 NoClassDefFoundError等異常的時候,應該檢查抛出異常的類的類加載器和目前線程的上下文類加載器,從中可以發現問題的所在。在開發自己的類加載器的時候,需要注意與已有的類加載器組織結構的協調。

參考文獻:

https://www.ibm.com/developerworks/cn/java/j-lo-classloader/

http://www.blogjava.net/lihao336/archive/2009/09/17/295489.html

http://kenwublog.com/structure-of-java-class-loader

轉載自http://blog.csdn.net/zhoudaxia/article/details/35897057

SPI