關於破壞雙親委派機制

說明:
最近在重讀《深入理解Java虛擬機》,看到破壞雙親委派這一塊內容時,通過對JDBC驅動加載過程源碼debug,突然茅塞頓開,收穫不少,以前僅僅只是知道概念,特此記錄一下

也看了一些其他博主的文章,雖然最後還是搞明白了,但是我覺得應該能更好的引入進去,而不是直接懟JDBC連接。


以下這段出自:《深入理解Java虛擬機》第三版 7.4.3節 破壞雙親委派模型

這並非是不可能出現的事情,一個典型的例子便是JNDI服務,JNDI現在已經是Java的標準服務, 它的代碼由啓動類加載器來完成加載(在JDK 1.3時加入到rt.jar的),肯定屬於Java中很基礎的類型 了。但JNDI存在的目的就是對資源進行查找和集中管理,它需要調用由其他廠商實現並部署在應用程 序的ClassPath下的JNDI服務提供者接口(Service Provider Interface,SPI)的代碼,現在問題來了,啓 動類加載器是絕不可能認識、加載這些代碼的,那該怎麼辦?

爲了解決這個困境,Java的設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器 (Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContext-ClassLoader()方 法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局範圍內 都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。

有了線程上下文類加載器,程序就可以做一些“舞弊”的事情了。JNDI服務使用這個線程上下文類 加載器去加載所需的SPI服務代碼,這是一種父類加載器去請求子類加載器完成類加載的行爲,這種行 爲實際上是打通了雙親委派模型的層次結構來逆向使用類加載器,已經違背了雙親委派模型的一般性 原則,但也是無可奈何的事情。Java中涉及SPI的加載基本上都採用這種方式來完成,例如JNDI、 JDBC、JCE、JAXB和JBI等。不過,當SPI的服務提供者多於一個的時候,代碼就只能根據具體提供 者的類型來硬編碼判斷,爲了消除這種極不優雅的實現方式,在JDK 6時,JDK提供了 java.util.ServiceLoader類,以M ETA-INF/services中的配置信息,輔以責任鏈模式,這纔算是給SPI的加 載提供了一種相對合理的解決方案。

誠然,看完後並不清楚具體是怎麼操作的,恰好裏面涉及了SPI,興趣就來了,想一探究竟,就拿裏面列舉到的我們常見的JDBC來研究一下。

首先需要了解一下JVM類加載機制,他們3個實際上是相互作用,互相影響的。我在其它博文看到有人問爲什麼不能讓Application Class Loader直接加載第三方的driver實現類,Bootstrap Class Loader加載JDK需要加載的DriverManager類,也完全符合雙親委派,這樣不就不需要TCCL(ThreadContextClassLoader)了?

其實,他這個問題的產生就是對類加載機制不太清楚,類加載機制裏有一條全盤負責,如果他明白這個,可能就不會產生那個問題了。這個點到即止,後面看源碼講。

一、JVM類加載機制

1.1 全盤負責

所謂全盤負責,就是當一個類加載器負責加載某個Class時,該Class所依賴和引用其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入。

1.2 雙親委派

所謂的雙親委派,則是先讓父類加載器試圖加載該Class,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委託給父加載器,依次遞歸,如果父加載器可以完成類加載任務,就成功返回;只有父加載器無法完成此加載任務時,才自己去加載。

1.3 緩存機制

緩存機制。緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區中搜尋該Class,只有當緩存區中不存在該Class對象時,系統纔會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩衝區中。這就是爲很麼修改了Class後,必須重新啓動JVM,程序所做的修改纔會生效的原因。

二、簡單認識雙親委派

要破壞它,總得要先明白他是什麼吧?

2.1 三層類加載器

絕大多數Java程序都會使用到以下幾個系統提供的類加載器來進行加載。

2.1.1 啓動類加載器(Bootstrap Class Loader)

這個類加載器負責加載存放在 <JAVA_HOME>\lib目錄,或者被-Xbootclasspath參數所指定的路徑中存放的,而且是Java虛擬機能夠 識別的(按照文件名識別,如rt .jar、t ools.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類 庫加載到虛擬機的內存中。啓動類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時, 如果需要把加載請求委派給引導類加載器去處理,那直接使用null代替即可,代碼清單7-9展示的就是 j a v a . l a n g. C l a s s L o a d e r . ge t C l a s s L o a d e r ( ) 方 法 的 代 碼 片 段 , 其 中 的 注 釋 和 代 碼 實 現 都 明 確 地 說 明 了 以 n u l l 值 來代表引導類加載器的約定規則。

2.1.2 擴展類加載器(Extension Class Loader)

這個類加載器是在類sun.misc.Launcher$ExtClassLoader 中以Java代碼的形式實現的。它負責加載<JAVA_HOM E>\lib\ext目錄中,或者被java.ext.dirs系統變量所 指定的路徑中所有的類庫。根據“擴展類加載器”這個名稱,就可以推斷出這是一種Java系統類庫的擴 展機制,JDK的開發團隊允許用戶將具有通用性的類庫放置在ext目錄裏以擴展Java SE的功能,在JDK 9之後,這種擴展機制被模塊化帶來的天然的擴展能力所取代。由於擴展類加載器是由Java代碼實現 的,開發者可以直接在程序中使用擴展類加載器來加載Class文件。

2.1.3 應用程序類加載器(Application Class Loader)

這個類加載器由sun.misc.Launcher$Ap p ClassLoader來實現。由於應用程序類加載器是ClassLoader類中的getSy stem- ClassLoader()方法的返回值,所以有些場合中也稱它爲“系統類加載器”。它負責加載用戶類路徑 (ClassPath)上所有的類庫,開發者同樣可以直接在代碼中使用這個類加載器。如果應用程序中沒有 自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

2.2 雙親委派模型的工作過程

如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加 載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的 加載請求最終都應該傳送到最頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請 求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去完成加載。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Zr9dZGf6-1593256026153)(evernotecid://2EC93C29-EBC0-48C7-9B33-CAA251F1491C/appyinxiangcom/29806953/ENResource/p186)]

ok,基本的知識點已經鋪墊完畢,關於破壞雙親委派前文開篇就講了,知識點着重需要了解到的就是Bootstrap Class Loader、Application Class Loader這兩個類加載器的負責範圍,這關係到後文中爲什麼要引入TCCL(ThreadContextClassLoader)的原因。以及剛纔提到的加載機制。

三、創建JDBC連接

3.1 邏輯的簡單介紹

	public static void main(String[] args) throws SQLException {
		String url = "jdbc:mysql://localhost:3306/base-service";
		// 通過java庫獲取數據庫連接
		Connection conn = java.sql.DriverManager.getConnection(url, "root", "123456");
	}

從Java1.6開始自帶的jdbc4.0版本已支持SPI服務加載機制,只要mysql的jar包在類路徑中,就可以註冊mysql驅動。所以,可以不用Class.forName() 去實例化 Driver 的實現類了。

在我們看源碼前,這裏需要明白一點:java.sql.DriverManager這個類是JDK自帶的,它所在的位置如下
在這裏插入圖片描述
需要補充一下Java 的SPI機制

SPI具體約定
Java SPI的具體約定爲:當服務的提供者提供了服務接口的一種實現之後,在jar包的META-INF/services/目錄裏同時創建一個以服務接口命名的文件。該文件裏就是實現該服務接口的具體實現類。而當外部程序裝配這個模塊的時候,就能通過該jar包META-INF/services/裏的配置文件找到具體的實現類名,並裝載實例化,完成模塊的注入。基於這樣一個約定就能很好的找到服務接口的實現類,而不需要再代碼裏制定。jdk提供服務實現查找的一個工具類:java.util.ServiceLoader

src.zip這個包就在JAVA_HOME/lib/中,本文2.1.1 中就提到過,Bootstrap Class Loader負責加載的區域就是<JAVA_HOME>/lib目錄。

所以說,java.sql.DriverManager這個類的加載應該是由Bootstrap Class Loader類加載器負責的。同時根據JVM類加載機制的全盤負責機制,該Class所依賴和引用其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入。

java.sql.DriverManager初始化的過程中,Driver的接口是JDK的,而它的實現類卻是由第三方廠商提供的,通過SPI機制實現加載,注入。這個類不在JAVA_HOME/lib/中,而是位於jar包的META-INF/services/目錄裏,所以說這塊的類加載實際上Bootstrap Class Loader是無法進行加載的,他需要Application Class Loader來完成加載,但原則上這是不被允許的,因爲破壞了雙親委派機制。這裏java團隊就引入了線程上下文類加載器 (Thread Context ClassLoader),由他來幫助Bootstrap Class Loader完成類加載。它加載完後,因爲緩存機制的存在,其他的類加載器就不需要再去加載它了。

大概邏輯就是這樣,下面debug源碼看一下。

3.2 JDBC連接建立的源碼分析

3.2.1 JDBC DriverManager 初始化

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

進入到loadInitialDrivers();裏面

private static void loadInitialDrivers() {
        ...
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
			//SPI機制,這個Driver.class是JDK定義好的接口
			//這是要初始化ServiceLoader這個SPI服務發現
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                    //實例化第三方的接口實現,這個實質上就是在執行Class.forName(),進去一看便知
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
		...
    }

看一下ServiceLoader.load(Driver.class);幹了什麼,這裏第一次出現了TCCL,獲取到了當前線程的上下文類加載器->AppClassLoader,傳入的接口爲:java.sql.Driver
在這裏插入圖片描述
之後我們進入這裏,看看這個代碼塊做了什麼

					while(driversIterator.hasNext()) {
                    //
                        driversIterator.next();
                    }

driversIterator.hasNext()裏可以清晰地看到,fullName拼出了:META-INF/services/java.sql.Driver,看到這個路徑應該很敏感,因爲SPI的約定要求:當服務的提供者提供了服務接口的一種實現之後,在jar包的META-INF/services/目錄裏同時創建一個以服務接口命名的文件,底下的loader也是AppClassLoader
在這裏插入圖片描述
這裏補充一下,這個loader就是在SericeLoader初始化時賦值的,獲取的是系統類加載器也就是AppClassLoader

回顧一下:現在初始化了ServiceLoader,當前的TCCL的類加載器是AppClassLoader,獲取到了第三方服務實現的位置:META-INF/services/java.sql.Driver。那麼下一步肯定是要去使用AppClassLoader加載META-INF/services/java.sql.Driver了。

接着看先前代碼塊裏的driversIterator.next();步驟,Class.forName(DriverName, false, loader)代碼所在的類在java.util.ServiceLoader類中,而ServiceLoader.class又加載在BootrapLoader中,其次DriverName的路徑不在<JAVA_HOME>/lib中,因此傳給 forName 的 loader 必然不能是BootrapLoader,通過debug看到這裏的loader依然是當前的TCCL–> AppClassLoaderClass.forName(DriverName, false, loader)這是JDK1.6之前使用JDBC手動創建連接時常用的手法,現在通過java的SPI機制,實現了自動化加載,我們可以看到這裏的cn的值是:com.mysql.cj.jdbc.Driver,這個路徑實際上是被寫在META-INF/services/java.sql.Driver裏的。在這之後,DriverManager就被加載完畢了,在之後就是建立連接了。
在這裏插入圖片描述
整個SPI都是在破壞雙親委派,父類加載器去請求子類加載器完成類加載的行爲,這種行 爲實際上是打通了雙親委派模型的層次結構來逆向使用類加載器,已經違背了雙親委派模型的一般性原則。不過,換來了更加靈活的編碼,很值得的。

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

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

引用其他優秀博文裏的一句話總結(我覺得寫得比較通俗些):

JDK提供了一種幫你(第三方實現者)加載服務(如數據庫驅動、日誌庫)的便捷方式,只要你遵循約定(把類名寫在/META-INF裏),那當我啓動時我會去掃描所有jar包裏符合約定的類名,再調用forName加載,但我的ClassLoader是沒法加載的,那就把它加載到當前執行線程的TCCL裏,後續你想怎麼操作(驅動實現類的static代碼塊)就是你的事了。

其實,還是不好言語出來其中的邏輯,還是需要自己去看源碼,一步一步看具體實現,明白之後就會豁然開朗的

參考部分

1.https://blog.csdn.net/yangcheng33/article/details/52631940
2.《深入理解Java虛擬機》第三版

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章