线程上下文类加载
前言
此前我對(duì)線程上下文類(lèi)加載器(ThreadContextLoader)的理解僅僅局限于下面這段話(huà):
Java?提供了很多服務(wù)提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實(shí)現(xiàn)。常見(jiàn)的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
這些 SPI 的接口由 Java 核心庫(kù)來(lái)提供,而這些 SPI 的實(shí)現(xiàn)代碼則是作為 Java 應(yīng)用所依賴(lài)的 jar 包被包含進(jìn)類(lèi)路徑(CLASSPATH)里。SPI接口中的代碼經(jīng)常需要加載具體的實(shí)現(xiàn)類(lèi)。那么問(wèn)題來(lái)了,SPI的接口是Java核心庫(kù)的一部分,是由引導(dǎo)類(lèi)加載器來(lái)加載的;SPI的實(shí)現(xiàn)類(lèi)是由系統(tǒng)類(lèi)加載器來(lái)加載的。引導(dǎo)類(lèi)加載器是無(wú)法找到 SPI 的實(shí)現(xiàn)類(lèi)的,因?yàn)橐勒针p親委派模型,BootstrapClassloader無(wú)法委派AppClassLoader來(lái)加載類(lèi)。
而線程上下文類(lèi)加載器破壞了“雙親委派模型”,可以在執(zhí)行線程中拋棄雙親委派加載鏈模式,使程序可以逆向使用類(lèi)加載器。
一直困惱我的問(wèn)題就是,它是如何打破了雙親委派模型?又是如何逆向使用類(lèi)加載器了?直到今天看了jdbc的加載過(guò)程才茅塞頓開(kāi),其實(shí)挺簡(jiǎn)單的,只是一直沒(méi)去看代碼導(dǎo)致理解不夠到位。
JDBC案例分析
先來(lái)看下JDBC的定義,JDBC(Java Data Base Connectivity)是一種用于執(zhí)行SQL語(yǔ)句的Java API,可以為多種關(guān)系數(shù)據(jù)庫(kù)提供統(tǒng)一訪問(wèn),它由一組用Java語(yǔ)言編寫(xiě)的類(lèi)和接口組成。JDBC提供了一種基準(zhǔn),據(jù)此可以構(gòu)建更高級(jí)的工具和接口,使數(shù)據(jù)庫(kù)開(kāi)發(fā)人員能夠編寫(xiě)數(shù)據(jù)庫(kù)應(yīng)用程序。
也就是說(shuō)JDBC就是java提供的一種SPI,要接入的數(shù)據(jù)庫(kù)供應(yīng)商必須按照此標(biāo)準(zhǔn)來(lái)編寫(xiě)實(shí)現(xiàn)類(lèi)。
代碼樣例
以mysql為例,先看一下驅(qū)動(dòng)注冊(cè)及獲取connection的過(guò)程:
?| 1 2 3 4 5 | // 注冊(cè)驅(qū)動(dòng)類(lèi) Class.forName("com.mysql.jdbc.Driver").getInstance(); String url = "jdbc:mysql://localhost:3306/testdb";??? // 通過(guò)java庫(kù)獲取數(shù)據(jù)庫(kù)連接 Connection conn = java.sql.DriverManager.getConnection(url, "name", "password"); |
源碼解讀">源碼解讀
Class.forName()加載了com.mysql.jdbc.Driver類(lèi),注意該類(lèi)是java.sql.Driver接口的實(shí)現(xiàn)(class Driver extends NonRegisteringDriver implements java.sql.Driver),它們名字相同,在下面的描述中將帶上package名避免混淆。
它將運(yùn)行其static靜態(tài)代碼塊:
?| 1 2 3 4 5 6 7 | <code><code><code>static { ????try { ????????java.sql.DriverManager.registerDriver(new Driver()); ????} catch (SQLException E) { ????????throw new RuntimeException("Can't register driver!"); ????} }</code></code></code> |
registerDriver方法將本類(lèi)(new com.mysql.jdbc.Driver())注冊(cè)到系統(tǒng)的DriverManager中,其實(shí)就是add到它的成員常量中,即一個(gè)名為registeredDrivers的CopyOnWriteArrayList 。
好,接下來(lái)的java.sql.DriverManager.getConnection()才算是進(jìn)入了正戲。它最終調(diào)用了以下方法:
?| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | <code><code><code><code><code><code><code>private static Connection getConnection( ?????String url, java.util.Properties info, Class<!--?--> caller) throws SQLException { ?????/* 傳入的caller由Reflection.getCallerClass()得到,該方法 ??????* 可獲取到調(diào)用本方法的Class類(lèi),這兒調(diào)用者是java.sql.DriverManager(位于/lib/rt.jar中), ??????* 也就是說(shuō)caller.getClassLoader()本應(yīng)得到Bootstrap啟動(dòng)類(lèi)加載器 ??????* 但是在上一篇文章中講到過(guò)啟動(dòng)類(lèi)加載器無(wú)法被程序獲取,所以只會(huì)得到null ??????* 這時(shí)問(wèn)題來(lái)了,DriverManager是啟動(dòng)類(lèi)加載器加載的,可偏偏又要在這兒加載子類(lèi)的Class ??????* 子類(lèi)是通過(guò)jar包的方式放入classpath中的,由AppClassLoader加載 ??????* 因此這兒通過(guò)雙親委派方式肯定無(wú)法加載成功,因此這兒借助 ??????* ContextClassLoader來(lái)加載mysql驅(qū)動(dòng)類(lèi)(簡(jiǎn)直作弊啊!) ??????* 上一篇文章最后也講到了Thread.currentThread().getContextClassLoader() ??????* 默認(rèn)set了AppClassLoader,也就是說(shuō)把類(lèi)加載器放到Thread里,那么執(zhí)行方法時(shí)任何地方都可以獲取到它。 ??????*/ ?????ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; ?????synchronized(DriverManager.class) { ?????????// 在獲取線程上下文類(lèi)加載器時(shí)需要同步加鎖 ?????????if (callerCL == null) { ?????????????callerCL = Thread.currentThread().getContextClassLoader(); ?????????} ?????} ?????if(url == null) { ?????????throw new SQLException("The url cannot be null", "08001"); ?????} ?????SQLException reason = null; ?????// 遍歷剛才放到registeredDrivers里的Driver類(lèi) ?????for(DriverInfo aDriver : registeredDrivers) { ?????????// 檢查能否加載Driver類(lèi),如果你沒(méi)有修改ContextClassLoader,那么默認(rèn)的AppClassLoader肯定可以加載 ?????????if(isDriverAllowed(aDriver.driver, callerCL)) { ?????????????try { ?????????????????println("??? trying " + aDriver.driver.getClass().getName()); ?????????????????// 調(diào)用com.mysql.jdbc.Driver.connect方法獲取連接 ?????????????????Connection con = aDriver.driver.connect(url, info); ?????????????????if (con != null) { ?????????????????????// Success! ?????????????????????return (con); ?????????????????} ?????????????} catch (SQLException ex) { ?????????????????if (reason == null) { ?????????????????????reason = ex; ?????????????????} ?????????????} ?????????} else { ?????????????println("??? skipping: " + aDriver.getClass().getName()); ?????????} ?????} ?????throw new SQLException("No suitable driver found for "+ url, "08001"); ?}</code></code></code></code></code></code></code> |
其中線程上下文類(lèi)加載器的作用已經(jīng)在上面的注釋中詳細(xì)說(shuō)明了,由于SPI提供了接口,其中用connect()方法獲取連接,數(shù)據(jù)庫(kù)廠商必須實(shí)現(xiàn)該方法,然而調(diào)用時(shí)卻是通過(guò)SPI里的DriverManager來(lái)加載外部實(shí)現(xiàn)類(lèi)并調(diào)用com.mysql.jdbc.Driver.connect()來(lái)獲取connection,所以這兒只能拜托Thread中保存的AppClassLoader來(lái)加載了,完全破壞了雙親委派模式。
當(dāng)然我們也可以不用SPI接口,直接調(diào)用子類(lèi)的com.mysql.jdbc.Driver().connect(...)來(lái)得到數(shù)據(jù)庫(kù)連接,但不推薦這么做(DriverManager.getConnection()最終就是調(diào)用該方法的)。
Tomcat與spring的類(lèi)加載器案例
接下來(lái)將介紹《深入理解java虛擬機(jī)》一書(shū)中的案例,并解答它所提出的問(wèn)題。(部分類(lèi)容來(lái)自于書(shū)中原文)
Tomcat中的類(lèi)加載器
在Tomcat目錄結(jié)構(gòu)中,有三組目錄(“/common/*”,“/server/*”和“shared/*”)可以存放公用Java類(lèi)庫(kù),此外還有第四組Web應(yīng)用程序自身的目錄“/WEB-INF/*”,把java類(lèi)庫(kù)放置在這些目錄中的含義分別是:
放置在common目錄中:類(lèi)庫(kù)可被Tomcat和所有的Web應(yīng)用程序共同使用。 放置在server目錄中:類(lèi)庫(kù)可被Tomcat使用,歲所有的Web應(yīng)用程序都不可見(jiàn)。 放置在shared目錄中:類(lèi)庫(kù)可被所有的Web應(yīng)用程序共同使用,但對(duì)Tomcat自己不可見(jiàn)。 放置在/WebApp/WEB-INF目錄中:類(lèi)庫(kù)僅僅可以被此Web應(yīng)用程序使用,對(duì)Tomcat和其他Web應(yīng)用程序都不可見(jiàn)。為了支持這套目錄結(jié)構(gòu),并對(duì)目錄里面的類(lèi)庫(kù)進(jìn)行加載和隔離,Tomcat自定義了多個(gè)類(lèi)加載器,這些類(lèi)加載器按照經(jīng)典的雙親委派模型來(lái)實(shí)現(xiàn),如下圖所示
灰色背景的3個(gè)類(lèi)加載器是JDK默認(rèn)提供的類(lèi)加載器,這3個(gè)加載器的作用前面已經(jīng)介紹過(guò)了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 則是 Tomcat 自己定義的類(lèi)加載器,它們分別加載 /common/*、/server/*、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 類(lèi)庫(kù)。其中 WebApp 類(lèi)加載器和 Jsp 類(lèi)加載器通常會(huì)存在多個(gè)實(shí)例,每一個(gè) Web 應(yīng)用程序?qū)?yīng)一個(gè) WebApp 類(lèi)加載器,每一個(gè) JSP 文件對(duì)應(yīng)一個(gè) Jsp 類(lèi)加載器。
從圖中的委派關(guān)系中可以看出,CommonClassLoader 能加載的類(lèi)都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加載的類(lèi)則與對(duì)方相互隔離。WebAppClassLoader 可以使用 SharedClassLoader 加載到的類(lèi),但各個(gè) WebAppClassLoader 實(shí)例之間相互隔離。而 JasperLoader 的加載范圍僅僅是這個(gè) JSP 文件所編譯出來(lái)的那一個(gè) Class,它出現(xiàn)的目的就是為了被丟棄:當(dāng)服務(wù)器檢測(cè)到 JSP 文件被修改時(shí),會(huì)替換掉目前的 JasperLoader 的實(shí)例,并通過(guò)再建立一個(gè)新的 Jsp 類(lèi)加載器來(lái)實(shí)現(xiàn) JSP 文件的 HotSwap 功能。
Spring加載問(wèn)題
Tomcat 加載器的實(shí)現(xiàn)清晰易懂,并且采用了官方推薦的“正統(tǒng)”的使用類(lèi)加載器的方式。這時(shí)作者提一個(gè)問(wèn)題:如果有 10 個(gè) Web 應(yīng)用程序都用到了spring的話(huà),可以把Spring的jar包放到 common 或 shared 目錄下讓這些程序共享。Spring 的作用是管理每個(gè)web應(yīng)用程序的bean,getBean時(shí)自然要能訪問(wèn)到應(yīng)用程序的類(lèi),而用戶(hù)的程序顯然是放在 /WebApp/WEB-INF 目錄中的(由 WebAppClassLoader 加載),那么被 CommonClassLoader 或 SharedClassLoader 加載的 Spring 如何訪問(wèn)并不在其加載范圍的用戶(hù)程序呢?
解答
看過(guò)JDBC的案例后,答案呼之欲出:spring根本不會(huì)去管自己被放在哪里,它統(tǒng)統(tǒng)使用線程上下文加載器來(lái)加載類(lèi),而線程上下文加載器默認(rèn)設(shè)置為了WebAppClassLoader,也就是說(shuō)哪個(gè)WebApp應(yīng)用調(diào)用了spring,spring就去取該應(yīng)用自己的WebAppClassLoader來(lái)加載bean,簡(jiǎn)直完美~
源碼分析
有興趣的可以接著看看具體實(shí)現(xiàn)。在web.xml中定義的listener為org.springframework.web.context.ContextLoaderListener,它最終調(diào)用了org.springframework.web.context.ContextLoader類(lèi)來(lái)裝載bean,具體方法如下(刪去了部分不相關(guān)內(nèi)容):
?| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <code><code><code><code><code><code><code><code><code><code><code><code><code>public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { ????try { ????????// 創(chuàng)建WebApplicationContext ????????if (this.context == null) { ????????????this.context = createWebApplicationContext(servletContext); ????????} ????????// 將保存到該webapp的servletContext中????? ????????servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); ????????// 獲取線程上下文類(lèi)加載器,默認(rèn)為WebAppClassLoader ????????ClassLoader ccl = Thread.currentThread().getContextClassLoader(); ????????// 如果spring的jar包放在每個(gè)webapp自己的目錄中 ????????// 此時(shí)線程上下文類(lèi)加載器會(huì)與本類(lèi)的類(lèi)加載器(加載spring的)相同,都是WebAppClassLoader ????????if (ccl == ContextLoader.class.getClassLoader()) { ????????????currentContext = this.context; ????????} ????????else if (ccl != null) { ????????????// 如果不同,也就是上面說(shuō)的那個(gè)問(wèn)題的情況,那么用一個(gè)map把剛才創(chuàng)建的WebApplicationContext及對(duì)應(yīng)的WebAppClassLoader存下來(lái) ????????????// 一個(gè)webapp對(duì)應(yīng)一個(gè)記錄,后續(xù)調(diào)用時(shí)直接根據(jù)WebAppClassLoader來(lái)取出 ????????????currentContextPerThread.put(ccl, this.context); ????????} ????????return this.context; ????} ????catch (RuntimeException ex) { ????????logger.error("Context initialization failed", ex); ????????throw ex; ????} ????catch (Error err) { ????????logger.error("Context initialization failed", err); ????????throw err; ????} }</code></code></code></code></code></code></code></code></code></code></code></code></code> |
具體說(shuō)明都在注釋中,spring考慮到了自己可能被放到其他位置,所以直接用線程上下文類(lèi)加載器來(lái)解決所有可能面臨的情況。
總結(jié)
通過(guò)上面的兩個(gè)案例分析,我們可以總結(jié)出線程上下文類(lèi)加載器的適用場(chǎng)景:
1.?當(dāng)高層提供了統(tǒng)一接口讓低層去實(shí)現(xiàn),同時(shí)又要是在高層加載(或?qū)嵗?#xff09;低層的類(lèi)時(shí),必須通過(guò)線程上下文類(lèi)加載器來(lái)幫助高層的ClassLoader找到并加載該類(lèi)。
2.?當(dāng)使用本類(lèi)托管類(lèi)加載,然而加載本類(lèi)的ClassLoader未知時(shí),為了隔離不同的調(diào)用者,可以取調(diào)用者各自的線程上下文類(lèi)加載器代為托管。
簡(jiǎn)而言之就是ContextClassLoader默認(rèn)存放了AppClassLoader的引用,由于它是在運(yùn)行時(shí)被放在了線程中,所以不管當(dāng)前程序處于何處(BootstrapClassLoader或是ExtClassLoader等),在任何需要的時(shí)候都可以用Thread.currentThread().getContextClassLoader()取出應(yīng)用程序類(lèi)加載器來(lái)完成需要的操作。
轉(zhuǎn)載于:https://www.cnblogs.com/tianyublog/p/8241097.html
總結(jié)
- 上一篇: 04 - JavaSE之异常处理
- 下一篇: OSPF 报文 链路状态请求报文 LSR