Java的ThreadContext類加載器的實現(xiàn)
疑惑
以前在看源碼的時候,總是會遇到框架里的代碼使用Thread.currentThread.getContextClassLoader()獲取當前線程的Context類加載器,通過這個Context類加載器去加載類。
我們平時在程序中寫代碼的時候,遇到要動態(tài)加載類的時候,一般使用Class.forName()的方式加載我們需要的類。比如最常見的,當我們進行JDBC編程的時候,我們通過Class.forName()去加載JDBC的驅(qū)動。
try { return Class.forName("oracle.jdbc.driver.OracleDriver"); } catch (ClassNotFoundException e) { // skip }
那么為什么當我們使用Class.forName()的方式去加載類的時候,如果類找不到,我們還要嘗試用Thread.currentThread.getContextLoader()獲取的類加載器去加載類呢?比如我們可能會碰到下面這種代碼:
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()獲取的加載器去加載類。顯然,Class.forName()加載類的時候使用的類加載器可能和Thread.currentThread.getContextLoader()獲取的類加載器是不同的。那么為什么會出現(xiàn)不同呢?
JAVA的類加載器
要理解為什么會用到Thread.currentThread.getContextLoader()獲取的這個類加載器之前,我們先來了解下JVM里使用的類加載器(ClassLoader)。
JVM默認有三種類加載器:
- Bootstrap Class Loader
- Extension Class Loader
- System Class Loader
Bootstrap Class Loader
Bootstrap Class Loader類加載器是JDK自帶的一款類加載器,用于加載JDK內(nèi)部的類。Bootstrap類加載器用于加載JDK中$JAVA_HOME/jre/lib下面的那些類,比如rt.jar包里面的類。Bootstrap類加載器是JVM的一部分,一般采用native代碼編寫。
Extension Class Loader
Extension Class Loader類加載器主要用于加載JDK擴展包里的類。一般$JAVA_HOME/lib/ext下面的包都是通過這個類加載器加載的,這個包下面的類基本上是以javax開頭的。
System Class Loader
System Class Loader類加載器也叫應用程序類加載器(AppClassLoader)。顧名思義,這個類加載器就是用來加載開發(fā)人員自己平時寫的應用代碼的類的。System類加載器是用于加載存放在classpath路徑下的那些應用程序級別的類的。
下面的代碼列舉出了這三個類加載器:
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類加載器永遠返回null值
null # Bootstrap類加載器 sun.misc.Launcher$ExtClassLoader@5e2de80c # Extension類加載器 sun.misc.Launcher$AppClassLoader@18b4aac2 # System類加載器
雙親委派模型
上面介紹的三種類加載器,并不是孤立的,他們之間有一個層次關(guān)系:
三個類加載器之間通過這個層次關(guān)系協(xié)同工作,一起負責類的加載工作。上面的這種層次模型稱為類加載器的“雙親委派”模型。雙親委派模型要求,除了最頂層的Bootstrap類加載器之外,所有的類加載器都必須有一個parent加載器。當類加載器加載類的時候,首先檢查緩存中是否有已經(jīng)被加載的類。如果沒有,則優(yōu)先委托它的父加載器去加載這個類,父加載器執(zhí)行和前面子加載器一樣的工作,直到請求達到頂層的Bootstrap類加載器。如果父加載器不能加載需要的類,那么這個時候才會讓子加載器自己去嘗試加載這個類。工作原理類似于下面這種方式:
我們可以通過JDK里ClassLoader里面的代碼一窺雙親委派機制的實現(xiàn)方式,代碼實現(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; }
采用雙親委派的方式組織類加載器,一個好處是為了安全。如果我們自己定義了一個String類,希望將這個String類替換掉默認Java中的java.lang.String的實現(xiàn)。
我們將自己實現(xiàn)的String類的class文件放到classpath路徑下,當我們使用類加載器去加載我們實現(xiàn)的String類的時候,首先,類加載器會將請求委托給父加載器,通過層層委派,最終由Bootstrap類加載器加載rt.jar包里的String類型,然后一路返回給我們。在這個過程中,我們的類加載器忽略掉了我們放在classpath中自定義的String類。
如果沒有采用雙親委派機制,那么System類加載器可以在classpath路徑中找到String的class文件并加載到程序中,導致JDK中的String實現(xiàn)被覆蓋。所以類加載器的這種工作方式,在一定程度上保證了Java程序可以安全穩(wěn)定的運行。
線程上下文類加載器
上面講了那么多類加載器相關(guān)的內(nèi)容,可還是沒有講到今天的主題,線程上下文類加載器。
到這里,我們已經(jīng)知道Java提供了三種類加載器,并且按照嚴格的雙親委派機制協(xié)同工作。表面上,似乎很完美,但正是這種嚴格的雙親委派機制導致在加載類的時候,存在一些局限性。
當我們更加基礎的框架需要用到應用層面的類的時候,只有當這個類是在我們當前框架使用的類加載器可以加載的情況下我們才能用到這些類。換句話說,我們不能使用當前類加載器的子加載器加載的類。這個限制就是雙親委派機制導致的,因為類加載請求的委派是單向的。
雖然這種情況不多,但是還是會有這種需求。比較典型的,JNDI服務。JNDI提供了查詢資源的接口,但是具體實現(xiàn)由不同的廠商實現(xiàn)。這個時候,JNDI的代碼是由JVM的Bootstrap類加載器加載,但是具體的實現(xiàn)是用戶提供的JDK之外的代碼,所以只能由System類加載器或者其他用戶自定義的類加載器去加載,在雙親委派的機制下,JNDI獲取不到JNDI的SPI的實現(xiàn)。
為了解決這個問題,引入了線程上下文類加載器。通過java.lang.Thread類的setContextClassLoader()設置當前線程的上下文類加載器(如果沒有設置,默認會從父線程中繼承,如果程序沒有設置過,則默認是System類加載器)。有了線程上下文類加載器,應用程序就可以通過java.lang.Thread.setContextClassLoader()將應用程序使用的類加載器傳遞給使用更頂層類加載器的代碼。比如上面的JNDI服務,就可以利用這種方式獲取到可以加載SPI實現(xiàn)的類加載器,獲取需要的SPI實現(xiàn)類。
可以看到,引入線程類加載器實際是對雙親委派機制的破壞,但是卻提供了類加載的靈活性。
解惑
回到開頭,框架的代碼為了加載框架之外用戶實現(xiàn)的類,由于這些類可能沒法通過框架使用的類加載器進行加載,為了繞過類加載器的雙親委派模型,采用Thread.getContextClassLoader()的方式去加載這些類。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Spring框架通過工廠創(chuàng)建Bean的三種方式實現(xiàn)
這篇文章主要介紹了Spring框架通過工廠創(chuàng)建Bean的三種方式實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-03-03Java toString方法重寫工具之ToStringBuilder案例詳解
這篇文章主要介紹了Java toString方法重寫工具之ToStringBuilder案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-08-08SpringBoot使用自定義注解實現(xiàn)數(shù)據(jù)脫敏過程詳細解析
這篇文章主要介紹了SpringBoot自定義注解之脫敏注解詳解,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02java網(wǎng)絡編程之識別示例 獲取主機網(wǎng)絡接口列表
一個客戶端想要發(fā)起一次通信,先決條件就是需要知道運行著服務器端程序的主機的IP地址是多少。然后我們才能夠通過這個地址向服務器發(fā)送信息。2014-01-01SpringBoot整合ShardingSphere5.x實現(xiàn)數(shù)據(jù)加解密功能(最新推薦)
這篇文章主要介紹了SpringBoot整合ShardingSphere5.x實現(xiàn)數(shù)據(jù)加解密功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-06-06lombok注解@Data使用在繼承類上時出現(xiàn)警告的問題及解決
Lombok的@Data注解簡化了實體類代碼,但在子類中使用時會出現(xiàn)警告,指出equals和hashCode方法不會考慮父類屬性,解決方法有兩種:一是在父類上使用@EqualsAndHashCode(callSuper=true)注解;二是通過配置lombok.config文件,均能有效解決警告問題2024-10-10