為什么需要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);
}
}