# SPI是如何規避雙親委派機制的? # 1、何為雙親委派機制? > 雙親委派機制是什麼? 雙親委派機制指的是Java中類載入機制的特性。 > 雙親委派機制是作用於什麼地方? 雙親委派機制主要作用於類載入的時候。 > 類載入器 首先需要清晰的知道,雙親委派機制指的是類載入的特性。在瞭解其特性之前,我 ...
SPI是如何規避雙親委派機制的?
1、何為雙親委派機制?
雙親委派機制是什麼?
雙親委派機制指的是Java中類載入機制的特性。
雙親委派機制是作用於什麼地方?
雙親委派機制主要作用於類載入的時候。
類載入器
首先需要清晰的知道,雙親委派機制指的是類載入的特性。在瞭解其特性之前,我們需要先瞭解類載入器有哪些(不考慮自定義載入器的情況)。
載入器 | 解釋 |
---|---|
BootStrap載入器 | 最為頂層的載入器,負責載入System.getProperty("sun.boot.class.path")下的Jar包,主要是jre\lib目錄下的內容。該類載入器為C實現,在Java中無法獲取 |
Ext類載入器 | 擴展類載入器,負責載入System.getProperty("java.ext.dirs")下的Jar包,主要是jre\lib\ext下的內容。在Java中對應ExtClassLoader(註意此處以jdk8為例,jdk11中有所改變)。 |
App類載入器 | 應用類載入器,負責載入System.getProperty("java.class.path")下的Jar包,主要是自身程式載入的包。在Java中對應AppClassLoader(註意此處以jdk8為例,jdk11中有所改變)。 |
類載入器之間的結構如何:
可以看出來,App類載入器是最小的一層,也是我們開發用戶接觸最多的一層,越往上載入的類就越核心。
雙親委派機制是什麼樣的結構?
雙親委派機制其實就是描述類載入器載入類的順序及其特點。
我們開發者需要去載入類的場景每天都在接觸,例如在代碼中new Car(我們自己的類),此時就是需要去載入這個類。在觸發載入類的時候,開發者處於載入器的最低層。那麼就可以看作成:App類載入器去載入Car這個類。
而實際上的載入順序是這樣的:
App類載入器--通知-->Ext類載入器--通知-->BootStrap類載入器
BootStrap類載入器--發現找不到該類,則向下返回-->Ext類載入器--發現找不到該類,繼續向下返回-->App類載入器(當前類載入器如果找不到該類則拋出異常,否則載入成功)
上述為雙親委派機制載入類時的順序,其特點為先向上通知到最頂層,再由最頂層往下嘗試,直到成功載入或到達發送載入類請求的載入器。
這種載入特點最大的作用如下:
安全性:由於Java核心類均有BootStrap載入器、Ext載入器去載入,再加上這種載入類的特性,可以有效防止Java核心類被篡改,正常的Java應用無法修改核心類實現。不僅可以應用在Java核心類中,當我們的應用是插件式時,此方式也可以防止插件中篡改主程式的代碼。
2、SPI是什麼?
上面我們講述了雙親委派機制,現在要講述SPI。
SPI是什麼?
SPI(Service Provider Interface)是JDK內置的一種服務提供發現機制,可以用來啟用框架擴展和替換組件,主要是被框架的開發人員使用。
例如資料庫驅動中java.sql.Driver介面,其他不同廠商可以針對同一介面做出不同的實現,MySQL和PostgreSQL都有不同的實現提供給用戶,而Java的SPI機制可以為某個介面尋找服務實現。Java中SPI機制主要思想是將功能實現剔除到程式之外,這針對與模塊化解耦有很大的作用。
例如下圖:
除資料庫驅動以外,例如日誌框架、Dubbo等也涉及到SPI機制。
在上圖中,例如當我們需要具體Driver實現的時候,直接通過JDK的API:
ServiceLoader<java.sql.Driver> serviceLoader = ServiceLoader.load(java.sql.Driver.class);
for (java.sql.Driver driver : serviceLoader) {
// mysql、pg、oracle、db2等
}
註意,SPI機制存在一些約定,這些約束如下:
-
三方介面需在META-INF/services/${interface_name}文件中列舉實現類,每一個實現類為一行。例如資料庫這,那麼示例如下:
META-INF/services/java.sql.Driver
com.mysql.cj.jdbc.Driver
org.postgresql.Driver
oracle.jdbc.OracleDriver
com.ibm.db2.jcc.DB2Driver
2.定義的實現類必須實現對應介面
3.實現類必須提供無參構造器
3、為什麼說SPI規避了雙親委派機制?
註意,我們前面說了雙親委派機制中,載入器會往上層載入器遞交載入請求,我們已知java.util.ServiceLoader的類載入器為BootStrap載入器。此載入器已經是最頂層,無更加上層的載入器。而按照載入器職責的約定,ServiceLoader所屬類載入器的職責是載入jdk核心類,其是無法載入到用戶的類。例如下圖:
現在的問題是:既然ServiceLoader的類載入器是最頂層的,其載入職責不負責我們自己的類,那麼它是如何載入到類似JDBC這種實現類的呢?
附:ServiceLoader的類載入器是BootStrap類載入器,在程式中是無法獲取到該類的類載入器的。
4、SPI是如何規避雙親委派機制的?
要搞清楚這個問題的原因,得先確認我們使用SPI的入口:
ServiceLoader<Xxxx> serviceLoader = ServiceLoader.load(Xxxx.class);
進入該方法,尋找其實現的方式:
java.util.ServiceLoader#load(java.lang.Class)
註意此處獲取了當前線程的類載入器,而線上程中調用該類方法的是我們用戶自己。那麼這裡就理解為獲取到了用戶的類載入器。
再往該方法中查找,找到該段代碼:
java.util.ServiceLoader#ServiceLoader
註意該段代碼中,cl為上一步獲取到的類載入器,如果發現類載入器不存在,會再次獲取系統預設載入器,這個系統預設載入器在常規情況下是用於載入啟動類的載入器(jdk註釋中解釋),而啟動類則是我們用戶自己定義的類,這裡毋庸置疑也會是應用類載入器。
從上面的代碼中我們總結出來,ServiceLoader獲取了我們的應用類載入器,至此load方法入口基本上沒有其他內容可以細看。
為減輕文章閱讀壓力,直接跳轉到該方法
java.util.ServiceLoader.LazyIterator#nextService
註意這裡的loader是我們前面獲取到的應用類載入器,這個方法中是獲取到了具體需要實例化的實現類,即將對其進行實例化, 在這之前需要先獲取到Class,這裡使用Class.forName(class, false, ClassLoader)方法,這個方法的含義是使用指定的類載入器去載入指定的類。既然這裡的類載入器是應用類載入器,那麼類載入順序自然就又回到了應用類載入器-->擴展類載入器-->BootStrap類載入器-->擴展類載入器-->應用類載入器,能載入到我們想要的類也就不奇怪了。
看到這裡也就明白了為什麼使用SPI仍然能正常載入類了。
SPI的載入機制看起來雖然方面,但仍然有缺點:
1. 無法實現動態載入、卸載的效果,只有最簡單的載入三方類的實現。
1. 由於實現原因,實現類必須提供無參構造器,局限性和擴展性很低
綜合來說,SPI簡單但局限性大,項目中能接受這些缺點就可以放心使用,如接受不了則可以模擬SPI機制自行實現一套載入機制,自己實現起來擴展性和局限性肯定是原生SPI不能比的。
本次內容結束,如發現內容錯誤請留言,會儘快改正。