1. 程式人生 > >spi,ClassLoader,雙親委託模式

spi,ClassLoader,雙親委託模式

轉載:
https://www.cnblogs.com/hiyujie/p/wo-xueJava1ClassLoader-yu-shuang-qin-wei-tuo-mo-sh.html

1、ClassLoader分類
Java虛擬機器會建立三類ClassLoader,分別如下

名稱 載入 載入路徑 父載入器 實現
BootStrap 虛擬機器的核心類庫 sun.boot.class.path 無 系統
Extension 擴充套件類庫 java.ext.dirs、jre/lib/ext BootStrap Java
System 應用類庫 classpath、java.class.path Extension Java
注:父子載入器並非繼承關係,也就是說子載入器不一定是繼承了父載入器
2、雙親委託模式
其實我覺得把「雙親委託模式」稱為「父載入委託模式」更好理解,「雙」字把我給弄混了。

「雙親委託模式」指的就是某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,如果父類載入器可以完成類載入任務,就成功返回;只有父類載入器無法完成此載入任務時,自己才去載入。


下面是一段ClassLoader的原始碼,很容易可以看出上述規則:

protected synchronized Class loadClass(String name, boolean resolve)
throws ClassNotFoundException{
// 首先檢查該name指定的class是否有被載入
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//如果parent不為null,則呼叫parent的loadClass進行載入
c = parent.loadClass(name, false);
}else{
//parent為null,則呼叫BootstrapClassLoader進行載入
c = findBootstrapClass0(name);
}
}catch(ClassNotFoundException e) {
//如果仍然無法載入成功,則呼叫自身的findClass進行載入
c = findClass(name);
}
}
if (resolve) {
resolveClass©;
}
return c;
}
(1)優點

避免類庫重複載入
安全,將核心類庫與使用者類庫隔離,使用者不能通過載入器替換核心類庫,如String類。
(2)弊端

委託永遠是子載入器去請求父載入器,是單向的,即上層的類載入器無法訪問下層的類載入器所載入的類:


下層類對於上層類是不可見的
舉個例子,假設「BootStrap」中提供了一個介面,及一個建立其例項的工廠方法,但是該介面的實現類在「System」中,那麼就會出現工廠方法無法建立在「System」載入的類的例項的問題。擁有這樣問題的元件有很多,比如JDBC、Xml parser等。

3、如何解決弊端——使用「SPI」
現在引入一個新的名詞「SPI」。

「SPI」 全稱為 (Service Provider Interface) ,是JDK內建的一種服務提供發現機制。 目前有不少框架用它來做服務的擴充套件發現, 簡單來說,它就是一種動態替換髮現的機制。

JDBC本身是Java連線資料庫的一個標準,是進行資料庫連線的抽象層,由Java編寫的一組類和介面組成,介面的實現由各個資料庫廠商來完成,不同廠商可以針對同一介面做出不同的實現,MySQL和PostgreSQL都有不同的實現提供給使用者,而Java的「SPI」機制可以為某個介面尋找服務實現。

3、JDBC舉例
下面以JDBC為例,介紹「SPI」機制。

在JDBC4.0之前,我們開發有連線資料庫的時候,通常會用Class.forName(“com.mysql.jdbc.Driver”)這句先載入資料庫相關的驅動,然後再進行獲取連線等的操作。而JDBC4.0之後不需要用Class.forName(“com.mysql.jdbc.Driver”)來載入驅動,直接獲取連線就可以了,現在這種方式就是使用了Java的「SPI」擴充套件機制來實現。

(1)介面定義

JDBC在java.sql.Driver只定義了介面。


JDBC中定義的介面
(2)廠商實現

這裡以MySQL為例,在mysql-connector-java-6.0.6.jar包裡的META-INF/services目錄下可以找到一個java.sql.Driver檔案,檔案內容是一個類名,這個名叫com.mysql.cj.jdbc.Driver的類就是MySQL針對JDBC中定義的介面的實現。



MySQL對JDBC中定義的介面的實現類
(3)如何使用

在我們的應用裡面,我們就可以直接連線MySQL了。

Connection conn = DriverManager.getConnection(url,username,password);
顯然語句並沒有載入實現類,這裡就涉及到使用「SPI」擴充套件機制來查詢相關驅動了,接下來,我們結合原始碼探究一下這是如何實現的。

4、原始碼解析
關於驅動的查詢其實都在DriverManager中,DriverManager位於java.sql包裡,用來獲取資料庫連線,在DriverManager中有一個靜態程式碼塊如下:


靜態載入
loadInitialDrivers方法用於例項化驅動,由3部分構成:

(1)獲取有關驅動的名稱


drivers儲存驅動的定義
(2)載入並例項化驅動

兩個比較關鍵的地方是ServiceLoader.load, 還有loadedDrivers.iterator,下面結合原始碼介紹一下:

(A)ServiceLoader.load

ServiceLoader封裝了一個自定義載入器loader,還應留意一下下面2個成員,之後會用到:

預設介面路徑:PREFIX
實現類的載入迭代器:lookupIterator

ServiceLoader.load(Driver.class)最後會呼叫建構函式,返回ServiceLoader例項




獲取應用層載入器——SystemClassLoader
每一個執行緒都有自己的ContextClassLoader,預設以SystemClassLoader為ContextClassLoader。通過Thread.currentThread().getContextClassLoader(),可以把一個ClassLoader置於一個執行緒的例項之中,使該ClassLoader成為一個相對共享的例項,這樣即使是啟動類載入器中的程式碼也可以通過這種方式訪問應用類載入器中的類了。


多個載入器通過上下文載入器共享
(B)loadedDrivers.iterator

loadedDrivers.iterator方法返回一個迭代器,這個迭代器是「SPI」機制載入實現類的關鍵,迭代器在iterator()方法內定義:


Iterator的定義,關注hasNext和next方法
「SPI」載入程式碼的是這樣的:


通過一個迭代來載入實現類
執行driversIterator.hasNext時,會呼叫lookupIterator.hasNext去找的實現類的名字。


driversIterator.hasNext方法

lookupIterator.hasNext方法

lookupIterator.hasNext方法,根據全路徑名找實現類的名字
接著會呼叫lookupIterator.next()去載入這個類:


driversIterator.next方法

lookupIterator.next方法

載入實現類
至此,已經將實現類成功載入。

(3)載入驅動

現在就可以根據第1步獲取到的驅動列表來載入實現類了:


呼叫Class.forName載入類
5、「SPI」的弊端
「SPI」通過迴圈載入實現類,顯而易見,它會把所有的類一同載入,無論有沒有用到,這造成了一定的資源浪費:


參考連結
android classloader雙親委託模式
dubbo原始碼解析-spi(一)
Java中SPI機制深入及原始碼解析
走出ClassLoader的迷宮