1. 程式人生 > 實用技巧 >【Java虛擬機器11】執行緒上下文類載入器

【Java虛擬機器11】執行緒上下文類載入器

前言

目前學習到的類載入的知識,都是基於【雙親委託機制】的。那麼JDK難道就沒有提供一種打破雙親委託機制的類載入機制嗎?
答案是否定的。
JDK為我們提供了一種打破雙親委託模型的機制:執行緒上下文類載入器

1.引出【執行緒上下文類載入器】

我們來複習一下JDBC執行SQL的程式碼【畫外音:感覺回到了大學自學Java的時候】:

Connection conn = DriverManager.getConnection(url, user, password); 
String sql = "insert into user (name, pwd) values(?,?)";
Statement st = conn.createStatement();
st.executeQuery(sql);

重點關注一下介面Connection,Statement介面,大家都很清楚,這2個介面是JDK提供給各個資料庫廠商的通過JAVA訪問資料庫的介面。
而這些介面是在包java.sql裡面,而這個包是在rt.jar這個包中。所以這些JDK所提供的標準介面的Class檔案是被啟動類載入器所載入的。
而資料庫廠商:如MySQL/Oracle根據Java語言的標準所提供的驅動包,如:mysql-connector-java-5.1.38.jar,這些驅動包一般在專案中都僅僅是在classpath目錄下,
並不能被啟動類載入器載入,只能被系統類載入器載入。

那麼問題來了:Connection conn = DriverManager.getConnection(url, user, password);


方法返回的例項其實是一個com.mysql.jdbc.ConnectionImpl(應該被AppClassLoader載入),
而方法返回值的定義為java.sql.Connection(應該被BootstrapClassLoader載入)。

根據前面學習的名稱空間的邏輯:“由父類載入器載入的類不能看見子載入器載入的類”,按這一規則來說,獲取Connection的程式碼是不能執行成功的。

其實JDK已經想到並幫我們處理了這個問題。
它使用了一種叫做執行緒上下文類載入器的CLassLoader機制,來打破雙親委託機制在某些場景無法滿足擴充套件的需求,實現這種場景的應用。

2.概念

2.1當前類載入器(Current Class Loader)

每個類都會使用自己的類載入器(即載入自身的類載入器)去載入其他類(指的是所依賴的類)
如果ClassX引用了ClassY,那麼ClassX的類載入器就會先去載入ClassY(前提是ClassY尚未被載入過)。

2.2執行緒上下文類載入器(Thread Context Class Loader)

執行緒上下文類載入器是從JDK1.2開始引入的,類Thread中的getContextClassLoader()與setContextClassLoader(ClassLoader cl)分別用來獲取和設值上下文類載入器。
如果沒有通過setContextClassLoader(ClassLoader cl)進行設定的話,執行緒將繼承其父執行緒的上下文類載入器。
JAVA應用執行時的初始執行緒的上下文類載入器是系統類載入器(AppClassLoader)。線上程中執行的程式碼可以通過該類載入器來載入類與資源。

執行緒上下文類載入器就是當前執行緒的Current Class Loader

2.3執行緒上下文類載入器是如何打破雙親委託模型的

SPI (Service Provider Interface)

父classloader可以使用當前執行緒Thread.currentThread().getContextLoader()所指定的classloader載入的類。
這就改變了父classloader不能使用子classloader或是其他沒有直接父子關係的classloader載入類的情況,即改變了雙親委託模型。

在雙親委託模型下,類載入是由下至上的,即下層的載入器會委託上層進行載入,但是對於SPI來說,有些介面是JAVA核心庫所提供的。
而JAVA核心庫是由啟動類載入器來載入的,而這些介面的實現是來自於不同的jar包(廠商的提供),JAVA的啟動類載入器是不會載入其他來源的jar包。
這樣傳統的雙親委託模型就無法滿足SPI的要求。
通過給當前執行緒設定上下文類載入器就可以由設定的上下文類載入器來實現對介面實現類的載入。(打破雙親委託模型限制)

3.預設的執行緒上下文類載入器

3.1 Launcher例項化時,預設設定執行緒上下文類載入器

在上次的文章【Java虛擬機器10】ClassLoader.getSystemClassLoader()流程簡析第二步中,簡要介紹了Launcher例項的初始化過程,其中有一句程式碼當時是一筆帶過的,這次拿出來再看看。

就是在Launcher類構造方法,首先初始化了ExtClassLoader,然後初始化了AppClassLoader,之後呼叫了

    public Launcher() {
        //balabala.....
        Thread.currentThread().setContextClassLoader(this.loader);
        //balabala.....
    }

3.2 ClassLoader.getSystemClassLoader()時,預設設定執行緒上下文類載入器

依然在上一篇文章【Java虛擬機器10】ClassLoader.getSystemClassLoader()流程簡析第四步中,也存在一個設定預設的執行緒上下文類載入器的程式碼。

可以看出,虛擬機器預設的執行緒上下文類載入器為系統類載入器(AppClassLoader)

4.執行緒上下文類載入器的一般使用模式

執行緒上下文類載入器的一般使用模式為:

ClassLoader originClassLoader = Thread.currentThread().getContextClassLoader();
try {
    Thread.currentThread().setContextClassLoader(myClassLoader);
    myMethod();
    //myMethod裡面則呼叫了Thread.currentThread().getContextClassLoader(),用外面設定的載入器(myClassLoader)去完成一些自己希望完成的事情。
} finally {
    Thread.currentThread().setContextClassLoader(originClassLoader);
}

5.總結

  • 如果一個類由類載入器A載入,那麼這個類的依賴類也會被類載入器A載入(前提是這個依賴類尚未被載入過)。
  • ThreadContextClassLoader的存在就是為了破壞雙親委託模型。
  • 當高層提供了統一的介面讓低層去實現,同時又要在高層去載入(或例項化)低層的類的時候,就必須用ThreadContextClassLoader來幫助高層的ClassLoader來找到並載入低層類。