理解 Java Thread ContextClassLoader(線程上下文類加載器)


為什么需要ContextClassLoader

Java中的類加載機制是雙親委派模型,即按照AppClassLoader → SystemClassLoader → BootstrapClassLoader 的順序,子ClassLoader將一個類加載的任務委托給父ClassLoader(父ClassLoader會再委托給父的父ClassLoader)來完成,只有父ClassLoader無法完成該類的加載時,子ClassLoader才會嘗試自己去加載該類。所以越基礎的類由越上層的ClassLoader進行加載,但如果基礎類又要調用回用戶的代碼,那該怎么辦?

為了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:Thread ContextClassLoader(線程上下文類加載器)。這個ClassLoader可以通過 java.lang.Thread類的setContextClassLoaser()方法進行設置;如果創建線程時沒有設置,則它會從父線程中繼承(見以下Thread的源碼);如果在應用程序的全局范圍內都沒有設置過的話,那這個類加載器默認為AppClassLoader(見以下代碼驗證)。

public class Thread implements Runnable {

    // 這里省略了無關代碼
    
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        // 這里省略了無關代碼
        
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader; // 繼承父線程的 上下文類加載器
            
        // 這里省略了無關代碼       
    }

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
    // 這里省略了無關代碼       

}

package com.bluesky.jvm.classloader;

public class ContextClassLoaderTest {

    public static void main(String[] args) {
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        System.err.println(contextClassLoader); // 輸出:sun.misc.Launcher$AppClassLoader@4e0e2f2a
    }
}

有了Thread ContextClassLoader,就可以實現父ClassLoader讓子ClassLoader去完成一個類的加載任務,即父ClassLoader加載的類中,可以使用ContextClassLoader去加載其無法加載的類)。

Thread ContextClassLoader 在 JDBC Driver 加載中的使用

Java 中所有涉及SPI機制的類加載基本上都是采用這種方式,最常見的就是JDBC Driver的加載。

JDBC是Java提出的一個有關數據庫訪問和操作的一個標准,也就是定義了一系列接口。不同的數據庫廠商(Oracle、MySQL、PostgreSQL等)提供對該接口的實現,即他們提供的Driver驅動包。Java定義的JDBC接口位於JDK的rt.jar中(java.sql包),因此這些接口會由BootstrapClassLoader進行加載;而數據庫廠商提供的Driver驅動包一般由我們自己在應用程序中引入(比如位於CLASSPATH下),這已經超出了BootstrapClassLoader的加載范圍,即這些驅動包中的JDBC接口的實現類無法被BootstrapClassLoader加載,只能由AppClassLoader或自定義的ClassLoader來加載。這樣,SPI機制就沒有辦法實現。要解決這個問題,就需要使用Thread Context Class Loader。

下面就查看下JDK中的DriverManager類的源碼,來看看其中Thread ContextClassLoader的使用。

public class DriverManager {

  // 省略無關代碼

    static {
        loadInitialDrivers(); // 在靜態代碼塊中加載當前環境中的 JDBC Driver
        println("JDBC DriverManager initialized");
    }
   
    private static void loadInitialDrivers() {
        // 省略無關代碼

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                // 通過 ServiceLoader#load 方法來加載 Driver 的實現(如 MySQL、Oracle、PostgreSQL 提供的 Driver 實現)
                // 即 SPI 機制
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

}

DriverManager類在被加載的時候就會執行通過ServiceLoader#load方法來加載數據庫驅動(即Driver接口的實現)。由於每個類都會使用加載自己的ClassLoader去加載其他的類(即它所依賴的類),因此可以簡單考慮以上代碼的類加載過程為:可以想一下,DriverManager類由BootstrapClassLoader加載,DriverManager類依賴於ServiceLoader類,因此BootstrapClassLoader也會嘗試加載ServiceLoaer類,這是沒有問題的;再往下,ServiceLoader的load方法中需要加載數據庫(MySQL等)驅動包中Driver接口的實現類,即ServiceLoader類依賴這些驅動包中的類,此時如果是默認情況下,則還是由BootstrapClassLoader來加載這些類,但驅動包中的Driver接口的實現類是位於CLASSPATH下的,BootstrapClassLoader是無法加載的,這就有問題了。因此,在ServiceLoader#load方法中實際是指明了由ContextClassLoader來加載驅動包中的類:

public final class ServiceLoader<S> implements Iterable<S> {

    // 省略無關代碼

    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 需要注意的是,這里使用的是 當前線程的 ContextClassLoader 來加載實現,這也是 ContextClassLoader 為什么存在的原因。
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    
} 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM