1. 程式人生 > >深入理解Java類加載器(二):線程上下文類加載器

深入理解Java類加載器(二):線程上下文類加載器

解決 介紹 chan undle 調用父類 攔截 decision native object

摘要:

  博文《深入理解Java類加載器(一):Java類加載原理解析》提到的類加載器的雙親委派模型並不是一個強制性的約束模型,而是Java設計者推薦給開發者的類加載器的實現方式。在Java世界中的大部分類加載器都遵從這個模型,但這個模型並不能解決 Java 應用開發中會遇到的類加載器的全部問題,這便是本文要闡述的內容。


版權聲明:

  本文作者:書呆子Rico
  作者博客地址:http://blog.csdn.net/justloveyou_/


一、線程上下文類加載器

  線程上下文類加載器(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()。

// 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中的相應方法來完成。

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()來首次加載它。下面是使用示例:

package classloader;  

public class A {  

    private B b;  

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

    public B getB() {  
        return b;  
    }  
}  
package classloader;  

public class B {  

}  
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();  
        }  
    }  
}/* Output: 
        A classLoader is sun.misc.Launcher$AppClassLoader@73d16e93  
        B classLoader is sun.misc.Launcher$AppClassLoader@73d16e93  
        A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93  
        Reloaded A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93  
 *///:~

  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加載的。


二. 何時使用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來加載資源。
  在其他情況下,我們可以自己來選擇最合適的類加載器。可以使用策略模式來設計選擇機制。其思想是將“總是使用上下文類加載器”或者“總是使用當前類加載器”的決策同具體實現邏輯分離開。往往設計之初是很難預測何種類加載策略是合適的,該設計能夠讓你可以後來修改類加載策略。
  考慮使用下面的代碼,這是作者本人在工作中發現的經驗。這兒有一個缺省實現,應該可以適應大部分工作場景:

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;  
    }  
}

package classloader.context;


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

    ClassLoader getClassLoader(ClassLoadContext ctx);  
} 
/** 
 * 缺省的類加載策略,可以適應大部分工作場景 
 */  
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核心的歷程應該能讓我們清楚過濾類名並不是好想法。作者也沒有試圖檢查哪個類加載器加載首先成功,而是檢查類加載器的父子關系,這是更好更有保證的方法。

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

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接口:

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參數,而且也適合於靜態方法和類初始化代碼。具體使用時,可以往這個上下文對象中添加具體部署環境中所需的其他屬性。


三. 類加載器與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)當出現找不到類的錯誤時,檢查當前類的類加載器和當前線程的上下文類加載器是否正確。


四. 類加載器與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等異常的時候,應該檢查拋出異常的類的類加載器和當前線程的上下文類加載器,從中可以發現問題的所在。在開發自己的類加載器的時候,需要註意與已有的類加載器組織結構的協調


六. 更多

  更多關於JVM內存模型的結構、Java對象在虛擬機中的創建、定位過程、內存異常分析等相關知識的介紹,請各位看官移步我的博文請移步我的博文[《JVM 內存模型概述》]。


引用:

深入理解Java類加載器(2):線程上下文類加載器
深入探討 Java 類加載器
[轉]線程上下文類加載器

深入理解Java類加載器(二):線程上下文類加載器