文章目錄
- 引子
- 雙親委派模型
- 你真的明白了嗎?
- 雙親委派“不夠用了”
- SPI機制
- 其他瑣碎
引子
有別于 java 提供的 IO 模塊,java 中的classloader主要是用來加載類的,當然除了加載類,也可以加載資源文件。
那么首先我們會問一個問題,有了 IO 為什么要 classloader?這是我們開啟 classloader 大門要弄明白的第一個問題。
java IO 提供了一些常見的功能,比如讀文件、寫文件,操作字符流、字節流,網絡的讀寫,文件系統操作等等功能,不勝枚舉。顯而易見,java IO 提供了一些通用方法。
而 classloader 是 JVM 用來按需動態加載資源的工具。之所以有 classloader 有多方面的考慮,首先要解決程序運行時怎么加載類,需要一套機制,這套機制就是我們常說的雙親委派模型。其次是怎么讀取資源,比如我們想要讀取某個配置文件,或者一張圖片(當然讀取資源文件我們可以直接用 IO 也不是不可以,殊途同歸)。
雙親委派模型
老生常談的話題,不過也值得討論一番。java 內建的classloader主要分為 3 類:
- Bootstrap ClassLoader
- Extension ClassLoader(又叫 Platform ClassLoader)
- Application ClassLoader(又叫 System ClassLoader)
Bootstrap ClassLoader: 是最頂層的ClassLoader,負責加載JRE核心庫,它是用C++實現的,無法通過Java代碼來創建。
Extension ClassLoader:負責加載Java的擴展庫。(本質上還是 java 官方提供的,由 java 實現的類庫)
Application ClassLoader:負責加載用戶類路徑下的類。比如我們自己編寫的類,引入的第三方 jar 包等。
如下圖:我們舉一個例子,假設 JVM 要加載類 A,首先會通過 Application ClassLoader 進行加載,這時首先檢查其緩存中是否已加載此類,如果加載,則返回。如果緩存中沒有類 A,則委托給 Extension ClassLoader進行加載,同樣是先檢查是否有緩存,如果沒有則委托給 Bootstrap ClassLoader 進行加載,同樣是檢查緩存,如果還是沒有,則嘗試掃描 JRE 核心庫是否有該類,如果有,則加載類,否則返回到 Extension ClassLoader,Extension ClassLoader 掃描其負責的擴展庫,如果有,則加載,否則返回到 Application ClassLoader進行加載,Application ClassLoader掃描用戶的類路徑,如果找到該類,則加載,否則則拋出ClassNotFound 異常。
你真的明白了嗎?
回答下面這個問題:如果類 A 是 Extension ClassLoader 加載,而類 A 中又引入了類 B,那么類 B 會怎么被加載呢?還是從 Appcation ClassLoader 開始加載嗎?
答案是否定的,類 B 會從 Extension ClassLoader 開始加載,先委托Bootstrap ClassLoader,如果沒找到,則 Extension ClassLoader自己開始加載,如果找不到,則拋出 ClassNotFound,并不會再返回到 Application ClassLoader 進行加載。為什么要這樣設計?很簡單,留給大家自己思考吧。
雙親委派“不夠用了”
有時候默認的雙親委派不夠用,舉個例子,java 定義了一個數據庫標準接口 JDBC,各個數據庫廠商會實現這個標準接口,即我們所說的數據庫驅動包。大家在學JDBC 的時候應該都寫過類似這種代碼
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/dbName");
大家可以嘗試將第一句刪除掉,你會發現還是可以獲取到Connection,這是為什么呢?DriverManager的包名是 java.sql
,顯然是 jdk的核心包,所以定然不會在其中寫入加載某個具體驅動類的代碼。所以 java 的開發人員就發明了一種新的方法:SPI(Service Provider Interface)。
SPI機制
SPI 的機制很簡單,我們還是以數據庫驅動為例,首先各個驅動廠商開發對應的驅動包,不過動包會有些特殊,如下圖:
在驅動包的 META-INF/services 下會包含與所要實現驅動名稱相同的一個文本文件,文本文件的內容是實現這個驅動的具體類。
然后在執行 DriverManager.getConnection("jdbc:mysql://localhost:3306/dbName");
時有以下代碼:
通過 ServiceLoader.load(Driver.class)去加載驅動。具體怎么加載這里就不說了,無非是掃描上面我們說的META-INF/services目錄下的文件,將所有實現了Driver接口的驅動都注冊進來。那為什么這里就可以加載到了呢?因為我們在執行ServiceLoader.load(Driver.class)方法時,方法內部是通過 Application ClassLoader 進行加載的,自然可以加載到外部的驅動包了。
那么,如果我引入了多個驅動包呢?系統怎么知道我們用的哪一個?如下圖,在 Driver 接口中定義了一個方法:acceptsURL,通過對jdbc:mysql://localhost:3306/dbName這種格式的判斷來決定此驅動是不是用戶想要的驅動。
DriverManager 中調用上面實現的acceptsURL 方法:
其他瑣碎
說了那么多,好像跟我們自己平常開發沒有多少關系。其實我們也可以利用ClassLoader來加載起源,比如我們想讀取一個配置文件。可以用類似ClassLoader.findResource("xxx")
或者this.class.getResource("xx")
。在一些代碼里我們還會看到:ClassLoader cl = Thread.currentThread().getContextClassLoader();
這樣的代碼,這些是在干嘛?后續再跟大家嘮嘮吧。