Java的ThreadContext類(lèi)加載器的實(shí)現(xiàn)
疑惑
以前在看源碼的時(shí)候,總是會(huì)遇到框架里的代碼使用Thread.currentThread.getContextClassLoader()獲取當(dāng)前線程的Context類(lèi)加載器,通過(guò)這個(gè)Context類(lèi)加載器去加載類(lèi)。
我們平時(shí)在程序中寫(xiě)代碼的時(shí)候,遇到要?jiǎng)討B(tài)加載類(lèi)的時(shí)候,一般使用Class.forName()的方式加載我們需要的類(lèi)。比如最常見(jiàn)的,當(dāng)我們進(jìn)行JDBC編程的時(shí)候,我們通過(guò)Class.forName()去加載JDBC的驅(qū)動(dòng)。
try {
return Class.forName("oracle.jdbc.driver.OracleDriver");
} catch (ClassNotFoundException e) {
// skip
}
那么為什么當(dāng)我們使用Class.forName()的方式去加載類(lèi)的時(shí)候,如果類(lèi)找不到,我們還要嘗試用Thread.currentThread.getContextLoader()獲取的類(lèi)加載器去加載類(lèi)呢?比如我們可能會(huì)碰到下面這種代碼:
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
// skip
}
ClassLoader ctxClassLoader = Thread.currentThread().getContextClassLoader();
if (ctxClassLoader != null) {
try {
clazz = ctxClassLoader.loadClass(className);
} catch (ClassNotFoundException e) {
// skip
}
}
這里加粗的部分,就是使用了Thread.currentThread.getContextLoader()獲取的加載器去加載類(lèi)。顯然,Class.forName()加載類(lèi)的時(shí)候使用的類(lèi)加載器可能和Thread.currentThread.getContextLoader()獲取的類(lèi)加載器是不同的。那么為什么會(huì)出現(xiàn)不同呢?
JAVA的類(lèi)加載器
要理解為什么會(huì)用到Thread.currentThread.getContextLoader()獲取的這個(gè)類(lèi)加載器之前,我們先來(lái)了解下JVM里使用的類(lèi)加載器(ClassLoader)。
JVM默認(rèn)有三種類(lèi)加載器:
- Bootstrap Class Loader
- Extension Class Loader
- System Class Loader
Bootstrap Class Loader
Bootstrap Class Loader類(lèi)加載器是JDK自帶的一款類(lèi)加載器,用于加載JDK內(nèi)部的類(lèi)。Bootstrap類(lèi)加載器用于加載JDK中$JAVA_HOME/jre/lib下面的那些類(lèi),比如rt.jar包里面的類(lèi)。Bootstrap類(lèi)加載器是JVM的一部分,一般采用native代碼編寫(xiě)。
Extension Class Loader
Extension Class Loader類(lèi)加載器主要用于加載JDK擴(kuò)展包里的類(lèi)。一般$JAVA_HOME/lib/ext下面的包都是通過(guò)這個(gè)類(lèi)加載器加載的,這個(gè)包下面的類(lèi)基本上是以javax開(kāi)頭的。
System Class Loader
System Class Loader類(lèi)加載器也叫應(yīng)用程序類(lèi)加載器(AppClassLoader)。顧名思義,這個(gè)類(lèi)加載器就是用來(lái)加載開(kāi)發(fā)人員自己平時(shí)寫(xiě)的應(yīng)用代碼的類(lèi)的。System類(lèi)加載器是用于加載存放在classpath路徑下的那些應(yīng)用程序級(jí)別的類(lèi)的。
下面的代碼列舉出了這三個(gè)類(lèi)加載器:
public class MainClass {
public static void main(String[] args) {
System.out.println(Integer.class.getClassLoader());
System.out.println(Logging.class.getClassLoader());
System.out.println(MainClass.class.getClassLoader());
}
}
其中獲取Bootstrap類(lèi)加載器永遠(yuǎn)返回null值
null # Bootstrap類(lèi)加載器 sun.misc.Launcher$ExtClassLoader@5e2de80c # Extension類(lèi)加載器 sun.misc.Launcher$AppClassLoader@18b4aac2 # System類(lèi)加載器
雙親委派模型
上面介紹的三種類(lèi)加載器,并不是孤立的,他們之間有一個(gè)層次關(guān)系:

三個(gè)類(lèi)加載器之間通過(guò)這個(gè)層次關(guān)系協(xié)同工作,一起負(fù)責(zé)類(lèi)的加載工作。上面的這種層次模型稱(chēng)為類(lèi)加載器的“雙親委派”模型。雙親委派模型要求,除了最頂層的Bootstrap類(lèi)加載器之外,所有的類(lèi)加載器都必須有一個(gè)parent加載器。當(dāng)類(lèi)加載器加載類(lèi)的時(shí)候,首先檢查緩存中是否有已經(jīng)被加載的類(lèi)。如果沒(méi)有,則優(yōu)先委托它的父加載器去加載這個(gè)類(lèi),父加載器執(zhí)行和前面子加載器一樣的工作,直到請(qǐng)求達(dá)到頂層的Bootstrap類(lèi)加載器。如果父加載器不能加載需要的類(lèi),那么這個(gè)時(shí)候才會(huì)讓子加載器自己去嘗試加載這個(gè)類(lèi)。工作原理類(lèi)似于下面這種方式:

我們可以通過(guò)JDK里ClassLoader里面的代碼一窺雙親委派機(jī)制的實(shí)現(xiàn)方式,代碼實(shí)現(xiàn)在ClassLoader.loadClass()里面
rotected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
采用雙親委派的方式組織類(lèi)加載器,一個(gè)好處是為了安全。如果我們自己定義了一個(gè)String類(lèi),希望將這個(gè)String類(lèi)替換掉默認(rèn)Java中的java.lang.String的實(shí)現(xiàn)。
我們將自己實(shí)現(xiàn)的String類(lèi)的class文件放到classpath路徑下,當(dāng)我們使用類(lèi)加載器去加載我們實(shí)現(xiàn)的String類(lèi)的時(shí)候,首先,類(lèi)加載器會(huì)將請(qǐng)求委托給父加載器,通過(guò)層層委派,最終由Bootstrap類(lèi)加載器加載rt.jar包里的String類(lèi)型,然后一路返回給我們。在這個(gè)過(guò)程中,我們的類(lèi)加載器忽略掉了我們放在classpath中自定義的String類(lèi)。
如果沒(méi)有采用雙親委派機(jī)制,那么System類(lèi)加載器可以在classpath路徑中找到String的class文件并加載到程序中,導(dǎo)致JDK中的String實(shí)現(xiàn)被覆蓋。所以類(lèi)加載器的這種工作方式,在一定程度上保證了Java程序可以安全穩(wěn)定的運(yùn)行。
線程上下文類(lèi)加載器
上面講了那么多類(lèi)加載器相關(guān)的內(nèi)容,可還是沒(méi)有講到今天的主題,線程上下文類(lèi)加載器。
到這里,我們已經(jīng)知道Java提供了三種類(lèi)加載器,并且按照嚴(yán)格的雙親委派機(jī)制協(xié)同工作。表面上,似乎很完美,但正是這種嚴(yán)格的雙親委派機(jī)制導(dǎo)致在加載類(lèi)的時(shí)候,存在一些局限性。
當(dāng)我們更加基礎(chǔ)的框架需要用到應(yīng)用層面的類(lèi)的時(shí)候,只有當(dāng)這個(gè)類(lèi)是在我們當(dāng)前框架使用的類(lèi)加載器可以加載的情況下我們才能用到這些類(lèi)。換句話說(shuō),我們不能使用當(dāng)前類(lèi)加載器的子加載器加載的類(lèi)。這個(gè)限制就是雙親委派機(jī)制導(dǎo)致的,因?yàn)轭?lèi)加載請(qǐng)求的委派是單向的。
雖然這種情況不多,但是還是會(huì)有這種需求。比較典型的,JNDI服務(wù)。JNDI提供了查詢資源的接口,但是具體實(shí)現(xiàn)由不同的廠商實(shí)現(xiàn)。這個(gè)時(shí)候,JNDI的代碼是由JVM的Bootstrap類(lèi)加載器加載,但是具體的實(shí)現(xiàn)是用戶提供的JDK之外的代碼,所以只能由System類(lèi)加載器或者其他用戶自定義的類(lèi)加載器去加載,在雙親委派的機(jī)制下,JNDI獲取不到JNDI的SPI的實(shí)現(xiàn)。
為了解決這個(gè)問(wèn)題,引入了線程上下文類(lèi)加載器。通過(guò)java.lang.Thread類(lèi)的setContextClassLoader()設(shè)置當(dāng)前線程的上下文類(lèi)加載器(如果沒(méi)有設(shè)置,默認(rèn)會(huì)從父線程中繼承,如果程序沒(méi)有設(shè)置過(guò),則默認(rèn)是System類(lèi)加載器)。有了線程上下文類(lèi)加載器,應(yīng)用程序就可以通過(guò)java.lang.Thread.setContextClassLoader()將應(yīng)用程序使用的類(lèi)加載器傳遞給使用更頂層類(lèi)加載器的代碼。比如上面的JNDI服務(wù),就可以利用這種方式獲取到可以加載SPI實(shí)現(xiàn)的類(lèi)加載器,獲取需要的SPI實(shí)現(xiàn)類(lèi)。

可以看到,引入線程類(lèi)加載器實(shí)際是對(duì)雙親委派機(jī)制的破壞,但是卻提供了類(lèi)加載的靈活性。
解惑
回到開(kāi)頭,框架的代碼為了加載框架之外用戶實(shí)現(xiàn)的類(lèi),由于這些類(lèi)可能沒(méi)法通過(guò)框架使用的類(lèi)加載器進(jìn)行加載,為了繞過(guò)類(lèi)加載器的雙親委派模型,采用Thread.getContextClassLoader()的方式去加載這些類(lèi)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Spring框架通過(guò)工廠創(chuàng)建Bean的三種方式實(shí)現(xiàn)
這篇文章主要介紹了Spring框架通過(guò)工廠創(chuàng)建Bean的三種方式實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
SpringBoot 集成JUnit5的詳細(xì)操作過(guò)程
JUnit5是最新的Java單元測(cè)試框架,提供了靈活的測(cè)試支持,它由JUnit Platform、JUnit Jupiter和JUnit Vintage組成,支持不同環(huán)境下的測(cè)試運(yùn)行,SpringBoot從2.2版本開(kāi)始默認(rèn)支持JUnit5,本文介紹了SpringBoot 集成JUnit5的相關(guān)知識(shí),感興趣的朋友跟隨小編一起看看吧2024-10-10
Java toString方法重寫(xiě)工具之ToStringBuilder案例詳解
這篇文章主要介紹了Java toString方法重寫(xiě)工具之ToStringBuilder案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08
SpringBoot使用自定義注解實(shí)現(xiàn)數(shù)據(jù)脫敏過(guò)程詳細(xì)解析
這篇文章主要介紹了SpringBoot自定義注解之脫敏注解詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02
java網(wǎng)絡(luò)編程之識(shí)別示例 獲取主機(jī)網(wǎng)絡(luò)接口列表
一個(gè)客戶端想要發(fā)起一次通信,先決條件就是需要知道運(yùn)行著服務(wù)器端程序的主機(jī)的IP地址是多少。然后我們才能夠通過(guò)這個(gè)地址向服務(wù)器發(fā)送信息。2014-01-01
SpringBoot整合ShardingSphere5.x實(shí)現(xiàn)數(shù)據(jù)加解密功能(最新推薦)
這篇文章主要介紹了SpringBoot整合ShardingSphere5.x實(shí)現(xiàn)數(shù)據(jù)加解密功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-06-06
lombok注解@Data使用在繼承類(lèi)上時(shí)出現(xiàn)警告的問(wèn)題及解決
Lombok的@Data注解簡(jiǎn)化了實(shí)體類(lèi)代碼,但在子類(lèi)中使用時(shí)會(huì)出現(xiàn)警告,指出equals和hashCode方法不會(huì)考慮父類(lèi)屬性,解決方法有兩種:一是在父類(lèi)上使用@EqualsAndHashCode(callSuper=true)注解;二是通過(guò)配置lombok.config文件,均能有效解決警告問(wèn)題2024-10-10

