Java的類加載機(jī)制的核心是雙親委派模型论咏,雙親委派模型(不存在自定義類加載器的情況下)加載某個(gè)類時(shí)會(huì)先委托父加載器尋找目標(biāo)類勤哗,找不到再委托上層父加載器加載抡爹,如果所有父加載器在自己的加載類路徑下都找不到目標(biāo)類,則在自己的類加載路徑中查找并載入目標(biāo)類芒划。
Java里有如下幾種類加載器
引導(dǎo)類加載器:負(fù)責(zé)加載支撐JVM運(yùn)行的位于JRE的lib目錄下的核心類庫(kù)冬竟,比如
rt.jar、charsets.jar等
擴(kuò)展類加載器:負(fù)責(zé)加載支撐JVM運(yùn)行的位于JRE的lib目錄下的ext擴(kuò)展目錄中的JAR
類包
應(yīng)用程序類加載器:負(fù)責(zé)加載ClassPath路徑下的類包民逼,主要就是加載你自己寫的那
些類或引用的非核心類庫(kù)與
自定義加載器:負(fù)責(zé)加載用戶自定義路徑下的類包
SPI的全名是Service Provider Interface泵殴,SPI:
Java 提供了很多服務(wù)提供者接口(Service Provider Interface,SPI)拼苍,允許第三方為這些接口提供實(shí)現(xiàn)笑诅。常見的 SPI 有 JDBC调缨、JCE、JNDI吆你、JAXP 和 JBI 等弦叶。
這些 SPI 的接口由 Java 核心庫(kù)來(lái)提供,而這些 SPI 的實(shí)現(xiàn)代碼則是作為 Java 應(yīng)用所依賴的 jar 包被包含進(jìn)類路徑(CLASSPATH)里妇多。SPI接口中的代碼經(jīng)常需要加載具體的實(shí)現(xiàn)類伤哺。那么問(wèn)題來(lái)了,SPI的接口是Java核心庫(kù)的一部分者祖,是由啟動(dòng)類加載器(Bootstrap Classloader)來(lái)加載的立莉;SPI的實(shí)現(xiàn)類是由系統(tǒng)類加載器(System ClassLoader)來(lái)加載的。引導(dǎo)類加載器是無(wú)法找到 SPI 的實(shí)現(xiàn)類的七问,因?yàn)橐勒针p親委派模型蜓耻,BootstrapClassloader無(wú)法委派AppClassLoader來(lái)加載類。
先看看SPI的實(shí)現(xiàn)方式械巡,以JDBC為例:
在mysql驅(qū)動(dòng)包中刹淌,存在META-INF/services/java.sql.Driver文件,內(nèi)容就是mysql驅(qū)動(dòng)包里對(duì)java.sql.Driver接口的實(shí)現(xiàn)類全類名讥耗。
項(xiàng)目中引入mysql驅(qū)動(dòng)包后芦鳍,直接通過(guò)DriverManager就可以獲得mysql的Connection對(duì)象。
根據(jù)使用方式葛账,我們提出幾個(gè)問(wèn)題。
1.META-INF/services/java.sql.Driver這個(gè)文件是做什么用的皮仁?
mysql的驅(qū)動(dòng)包被加載的過(guò)程是什么籍琳?
-
它是如何打破雙親委派機(jī)制的?
這幾個(gè)問(wèn)題需要通過(guò)jdk的源碼實(shí)現(xiàn)來(lái)回答贷祈,首先看DriverManager的靜態(tài)代碼塊趋急,它會(huì)在DriverManager類被加載時(shí)執(zhí)行:static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
再看loadInitialDrivers方法的實(shí)現(xiàn):
private static void loadInitialDrivers() {
String drivers;
try {
// 先讀取系統(tǒng)屬性
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 通過(guò)SPI加載驅(qū)動(dòng)類
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
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;
}
});
// 繼續(xù)加載系統(tǒng)屬性中的驅(qū)動(dòng)類
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);
// 使用AppClassloader加載
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
其中與SPI核心相關(guān)的內(nèi)容已經(jīng)給出注釋,其余內(nèi)容是從system參數(shù)中加載驅(qū)動(dòng),因?yàn)槲覀儧](méi)有設(shè)置系統(tǒng)參數(shù)势誊,所以相關(guān)邏輯不會(huì)執(zhí)行呜达,關(guān)鍵看以下代碼:
ServiceLoader.load(Driver.class);
方法實(shí)現(xiàn)如下(按照方法堆棧調(diào)用依次貼出實(shí)現(xiàn)):
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
可以看到ServiceLoader使用了一個(gè)ClassLoader是Thread.currentThread().getContextClassLoader(),這個(gè)ClassLoader如果沒(méi)有專門的設(shè)置,返回的是AppClassLoader對(duì)象粟耻,這就是SPI打破雙親委派機(jī)制的關(guān)鍵所在查近,因?yàn)閖ava.sql.Driver接口是java核心包的類,所以根據(jù)雙親委派它應(yīng)該由BootstrapClassLoader來(lái)加載挤忙,但是ServiceLoader在實(shí)現(xiàn)SPI機(jī)制的過(guò)程中霜威,使用AppClassLoader來(lái)加載,所以它才能成功加載到mysql的驅(qū)動(dòng)類册烈。但是此處只是返回了ServiceLoader對(duì)象戈泼,不能說(shuō)明加載mysql驅(qū)動(dòng)真正使用的是AppClassLoader,我們繼續(xù)看,剩余的步驟是拿到ServiceLoader的遍歷器對(duì)象,然后進(jìn)行了遍歷大猛,可以看到上面的reload方法中初始化了一個(gè)LazyIterator對(duì)象扭倾,我們來(lái)看LazyIterator類中的關(guān)鍵代碼(hasNext方法會(huì)調(diào)用):
先簡(jiǎn)單回復(fù)圖片中的一個(gè)疑點(diǎn),loader對(duì)象為什么是URLClassLoader對(duì)象而不是AppClassLoader對(duì)象挽绩,這是因?yàn)橥ㄟ^(guò)IDEA運(yùn)行java程序膛壹,斷點(diǎn)功能等需要IDEA通過(guò)額外的技術(shù)實(shí)現(xiàn),所以使用的classloader是經(jīng)過(guò)處理的琼牧,我已經(jīng)測(cè)試用system.printIn.out打印Thread.currentThread().getContextClassLoader(),在IDEA的運(yùn)行結(jié)果是URLClassLoader對(duì)象恢筝,但是通過(guò)java命令運(yùn)行的結(jié)果是AppClassLoader對(duì)象,有疑問(wèn)的小伙伴可以自行測(cè)試一下巨坊。
通過(guò)斷點(diǎn)調(diào)試可以看到fullName等于META-INF/services/java.sql.Driver撬槽,這里與第一個(gè)問(wèn)題的目錄結(jié)構(gòu)相呼應(yīng),也就解釋了為什么要用那樣的目錄接口與文件命名趾撵,fullName是由常量PREFIX和service.getName()的拼裝的侄柔,進(jìn)而說(shuō)明,ServiceLoader類是SPI的通用實(shí)現(xiàn)類占调,它不僅僅可以加載java.sql.Driver,傳入響應(yīng)的接口暂题,在指定目錄下創(chuàng)建接口全限定類名文件,并寫入第三方實(shí)現(xiàn)類究珊,同樣可以加載薪者,它是java SPI機(jī)制的通用實(shí)現(xiàn)。
下面需要看最核心的加載邏輯(next()方法會(huì)調(diào)用):
熟悉的Class.forName剿涮,關(guān)鍵看參數(shù)言津,加載的類是com.mysql.jdbc.Driver,使用的ClassLoader是URLClassLoader對(duì)象(實(shí)際運(yùn)行無(wú)特殊處理是使用AppClassLoader對(duì)象)取试。
以上分析與debug調(diào)試過(guò)程已經(jīng)充分說(shuō)明了JDBC對(duì)SPI的實(shí)際應(yīng)用過(guò)程悬槽。額外補(bǔ)充對(duì)DriverManager.getConnection方法的源碼分析。
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//.....省略非關(guān)鍵內(nèi)容
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
//....省略非關(guān)鍵內(nèi)容
}
可以看到關(guān)鍵邏輯是遍歷registeredDrivers拿到driver對(duì)象瞬浓,嘗試通過(guò)連接信息獲取連接初婆,如果連接不為空,返回連接對(duì)象猿棉。說(shuō)明DriverManager可能同時(shí)持有多種數(shù)據(jù)庫(kù)驅(qū)動(dòng)類磅叛,會(huì)使用連接信息逐一嘗試,連接成功后會(huì)返回∪蓿現(xiàn)在的問(wèn)題轉(zhuǎn)化為registeredDrivers里驅(qū)動(dòng)對(duì)象是在什么時(shí)候放入的宪躯,通過(guò)IDEA的方法反調(diào)不難找到如下代碼:
可以看到
mysql的驅(qū)動(dòng)類的靜態(tài)代碼塊中調(diào)用了DriverManager#registerDriver方法,將自己注冊(cè)到了registeredDrivers中位迂。