文章出處

前言

此前我對線程上下文類加載器(ThreadContextLoader)的理解僅僅局限于下面這段話:

Java 提供了很多服務提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

這些 SPI 的接口由 Java 核心庫來提供,而這些 SPI 的實現代碼則是作為 Java 應用所依賴的 jar 包被包含進類路徑(CLASSPATH)里。SPI接口中的代碼經常需要加載具體的實現類。那么問題來了,SPI的接口是Java核心庫的一部分,是由引導類加載器來加載的;SPI的實現類是由系統類加載器來加載的。引導類加載器是無法找到 SPI 的實現類的,因為依照雙親委派模型,BootstrapClassloader無法委派AppClassLoader來加載類。

而線程上下文類加載器破壞了“雙親委派模型”,可以在執行線程中拋棄雙親委派加載鏈模式,使程序可以逆向使用類加載器。

一直困惱我的問題就是,它是如何打破了雙親委派模型?又是如何逆向使用類加載器了?直到今天看了jdbc的加載過程才茅塞頓開,其實挺簡單的,只是一直沒去看代碼導致理解不夠到位。

JDBC案例分析

先來看下JDBC的定義,JDBC(Java Data Base Connectivity)是一種用于執行SQL語句的Java API,可以為多種關系數據庫提供統一訪問,它由一組用Java語言編寫的類和接口組成。JDBC提供了一種基準,據此可以構建更高級的工具和接口,使數據庫開發人員能夠編寫數據庫應用程序。

也就是說JDBC就是java提供的一種SPI,要接入的數據庫供應商必須按照此標準來編寫實現類。

代碼樣例

以mysql為例,先看一下驅動注冊及獲取connection的過程:

// 注冊驅動類Class.forName("com.mysql.jdbc.Driver").getInstance(); String url = "jdbc:mysql://localhost:3306/testdb";    // 通過java庫獲取數據庫連接Connection conn = java.sql.DriverManager.getConnection(url, "name", "password"); 

源碼解讀

Class.forName()加載了com.mysql.jdbc.Driver類,注意該類是java.sql.Driver接口的實現(class Driver extends NonRegisteringDriver implements java.sql.Driver),它們名字相同,在下面的描述中將帶上package名避免混淆。

它將運行其static靜態代碼塊:

static {    try {        java.sql.DriverManager.registerDriver(new Driver());    } catch (SQLException E) {        throw new RuntimeException("Can't register driver!");    }}

registerDriver方法將本類(new com.mysql.jdbc.Driver())注冊到系統的DriverManager中,其實就是add到它的成員常量中,即一個名為registeredDrivers的CopyOnWriteArrayList 。

好,接下來的java.sql.DriverManager.getConnection()才算是進入了正戲。它最終調用了以下方法:

private static Connection getConnection(     String url, java.util.Properties info, Class caller) throws SQLException {     /* 傳入的caller由Reflection.getCallerClass()得到,該方法      * 可獲取到調用本方法的Class類,這兒調用者是java.sql.DriverManager(位于/lib/rt.jar中),      * 也就是說caller.getClassLoader()本應得到Bootstrap啟動類加載器      * 但是在上一篇文章中講到過啟動類加載器無法被程序獲取,所以只會得到null      * 這時問題來了,DriverManager是啟動類加載器加載的,可偏偏又要在這兒加載子類的Class      * 子類是通過jar包的方式放入classpath中的,由AppClassLoader加載      * 因此這兒通過雙親委派方式肯定無法加載成功,因此這兒借助      * ContextClassLoader來加載mysql驅動類(簡直作弊啊!)      * 上一篇文章最后也講到了Thread.currentThread().getContextClassLoader()      * 默認set了AppClassLoader,也就是說把類加載器放到Thread里,那么執行方法時任何地方都可以獲取到它。      */     ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;     synchronized(DriverManager.class) {         // 在獲取線程上下文類加載器時需要同步加鎖         if (callerCL == null) {             callerCL = Thread.currentThread().getContextClassLoader();         }     }     if(url == null) {         throw new SQLException("The url cannot be null", "08001");     }     SQLException reason = null;     // 遍歷剛才放到registeredDrivers里的Driver類     for(DriverInfo aDriver : registeredDrivers) {         // 檢查能否加載Driver類,如果你沒有修改ContextClassLoader,那么默認的AppClassLoader肯定可以加載         if(isDriverAllowed(aDriver.driver, callerCL)) {             try {                 println("    trying " + aDriver.driver.getClass().getName());                 // 調用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"); }

其中線程上下文類加載器的作用已經在上面的注釋中詳細說明了,由于SPI提供了接口,其中用connect()方法獲取連接,數據庫廠商必須實現該方法,然而調用時卻是通過SPI里的DriverManager來加載外部實現類并調用com.mysql.jdbc.Driver.connect()來獲取connection,所以這兒只能拜托Thread中保存的AppClassLoader來加載了,完全破壞了雙親委派模式。

當然我們也可以不用SPI接口,直接調用子類的com.mysql.jdbc.Driver().connect(...)來得到數據庫連接,但不推薦這么做(DriverManager.getConnection()最終就是調用該方法的)。

Tomcat與spring的類加載器案例

接下來將介紹《深入理解java虛擬機》一書中的案例,并解答它所提出的問題。(部分類容來自于書中原文)

Tomcat中的類加載器

在Tomcat目錄結構中,有三組目錄(“/common/*”,“/server/*”和“shared/*”)可以存放公用Java類庫,此外還有第四組Web應用程序自身的目錄“/WEB-INF/*”,把java類庫放置在這些目錄中的含義分別是:

放置在common目錄中:類庫可被Tomcat和所有的Web應用程序共同使用。 放置在server目錄中:類庫可被Tomcat使用,歲所有的Web應用程序都不可見。 放置在shared目錄中:類庫可被所有的Web應用程序共同使用,但對Tomcat自己不可見。 放置在/WebApp/WEB-INF目錄中:類庫僅僅可以被此Web應用程序使用,對Tomcat和其他Web應用程序都不可見。

為了支持這套目錄結構,并對目錄里面的類庫進行加載和隔離,Tomcat自定義了多個類加載器,這些類加載器按照經典的雙親委派模型來實現,如下圖所示

Tomcat中的類加載器

灰色背景的3個類加載器是JDK默認提供的類加載器,這3個加載器的作用前面已經介紹過了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 則是 Tomcat 自己定義的類加載器,它們分別加載 /common/*、/server/*、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 類庫。其中 WebApp 類加載器和 Jsp 類加載器通常會存在多個實例,每一個 Web 應用程序對應一個 WebApp 類加載器,每一個 JSP 文件對應一個 Jsp 類加載器。

從圖中的委派關系中可以看出,CommonClassLoader 能加載的類都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加載的類則與對方相互隔離。WebAppClassLoader 可以使用 SharedClassLoader 加載到的類,但各個 WebAppClassLoader 實例之間相互隔離。而 JasperLoader 的加載范圍僅僅是這個 JSP 文件所編譯出來的那一個 Class,它出現的目的就是為了被丟棄:當服務器檢測到 JSP 文件被修改時,會替換掉目前的 JasperLoader 的實例,并通過再建立一個新的 Jsp 類加載器來實現 JSP 文件的 HotSwap 功能。

Spring加載問題

Tomcat 加載器的實現清晰易懂,并且采用了官方推薦的“正統”的使用類加載器的方式。這時作者提一個問題:如果有 10 個 Web 應用程序都用到了spring的話,可以把Spring的jar包放到 common 或 shared 目錄下讓這些程序共享。Spring 的作用是管理每個web應用程序的bean,getBean時自然要能訪問到應用程序的類,而用戶的程序顯然是放在 /WebApp/WEB-INF 目錄中的(由 WebAppClassLoader 加載),那么被 CommonClassLoader 或 SharedClassLoader 加載的 Spring 如何訪問并不在其加載范圍的用戶程序呢?

解答

看過JDBC的案例后,答案呼之欲出:spring根本不會去管自己被放在哪里,它統統使用線程上下文加載器來加載類,而線程上下文加載器默認設置為了WebAppClassLoader,也就是說哪個WebApp應用調用了spring,spring就去取該應用自己的WebAppClassLoader來加載bean,簡直完美~

源碼分析

有興趣的可以接著看看具體實現。在web.xml中定義的listener為org.springframework.web.context.ContextLoaderListener,它最終調用了org.springframework.web.context.ContextLoader類來裝載bean,具體方法如下(刪去了部分不相關內容):

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {    try {        // 創建WebApplicationContext        if (this.context == null) {            this.context = createWebApplicationContext(servletContext);        }        // 將保存到該webapp的servletContext中              servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);        // 獲取線程上下文類加載器,默認為WebAppClassLoader        ClassLoader ccl = Thread.currentThread().getContextClassLoader();        // 如果spring的jar包放在每個webapp自己的目錄中        // 此時線程上下文類加載器會與本類的類加載器(加載spring的)相同,都是WebAppClassLoader        if (ccl == ContextLoader.class.getClassLoader()) {            currentContext = this.context;        }        else if (ccl != null) {            // 如果不同,也就是上面說的那個問題的情況,那么用一個map把剛才創建的WebApplicationContext及對應的WebAppClassLoader存下來            // 一個webapp對應一個記錄,后續調用時直接根據WebAppClassLoader來取出            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;    }}

具體說明都在注釋中,spring考慮到了自己可能被放到其他位置,所以直接用線程上下文類加載器來解決所有可能面臨的情況。

總結

通過上面的兩個案例分析,我們可以總結出線程上下文類加載器的適用場景:

1. 當高層提供了統一接口讓低層去實現,同時又要是在高層加載(或實例化)低層的類時,必須通過線程上下文類加載器來幫助高層的ClassLoader找到并加載該類。

2. 當使用本類托管類加載,然而加載本類的ClassLoader未知時,為了隔離不同的調用者,可以取調用者各自的線程上下文類加載器代為托管。

簡而言之就是ContextClassLoader默認存放了AppClassLoader的引用,由于它是在運行時被放在了線程中,所以不管當前程序處于何處(BootstrapClassLoader或是ExtClassLoader等),在任何需要的時候都可以用Thread.currentThread().getContextClassLoader()取出應用程序類加載器來完成需要的操作。

就愛閱讀www.92to.com網友整理上傳,為您提供最全的知識大全,期待您的分享,轉載請注明出處。
歡迎轉載:http://www.kanwencang.com/bangong/20161116/54160.html

文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()