我學Java(1)——ClassLoader與雙親委託模式以及「SPI」

一、ClassLoader分類

Java虛擬機會建立三類ClassLoader,分別以下html

名稱 加載 加載路徑 父加載器 實現
BootStrap 虛擬機的核心類庫 sun.boot.class.path 系統
Extension 擴展類庫 java.ext.dirs、jre/lib/ext BootStrap Java
System 應用類庫 classpath、java.class.path Extension Java

注:父子加載器並不是繼承關係,也就是說子加載器不必定是繼承了父加載器
java

二、雙親委託模式

其實我以爲把「雙親委託模式」稱爲「父加載委託模式」更好理解,「雙」字把我給弄混了。mysql

「雙親委託模式」指的就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,若是父類加載器能夠完成類加載任務,就成功返回;只有父類加載器沒法完成此加載任務時,本身才去加載。android



下面是一段ClassLoader的源碼,很容易能夠看出上述規則:sql

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(c);  
     }  
     return c;  
}

(1)優勢

  • 避免類庫重複加載
  • 安全,將核心類庫與用戶類庫隔離,用戶不能經過加載器替換核心類庫,如String類。

(2)弊端

委託永遠是子加載器去請求父加載器,是單向的,即上層的類加載器沒法訪問下層的類加載器所加載的類數據庫



下層類對於上層類是不可見的

舉個例子,假設「BootStrap」中提供了一個接口,及一個建立其實例的工廠方法,可是該接口的實現類在「System」中,那麼就會出現工廠方法沒法建立在「System」加載的類的實例的問題。擁有這樣問題的組件有不少,好比JDBC、Xml parser等。安全

三、如何解決弊端——使用「SPI」

如今引入一個新的名詞「SPI」。框架

「SPI」 全稱爲 (Service Provider Interface) ,是JDK內置的一種服務提供發現機制。 目前有很多框架用它來作服務的擴展發現, 簡單來講,它就是一種動態替換髮現的機制。ide

JDBC自己是Java鏈接數據庫的一個標準,是進行數據庫鏈接的抽象層,由Java編寫的一組類和接口組成,接口的實現由各個數據庫廠商來完成,不一樣廠商能夠針對同一接口作出不一樣的實現,MySQL和PostgreSQL都有不一樣的實現提供給用戶,而Java的「SPI」機制能夠爲某個接口尋找服務實現。函數

三、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」擴展機制來查找相關驅動了,接下來,咱們結合源碼探究一下這是如何實現的。

四、源碼解析

關於驅動的查找其實都在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加載類

五、「SPI」的弊端

「SPI」經過循環加載實現類,顯而易見,它會把全部的類一同加載,不管有沒有用到,這形成了必定的資源浪費:



參考連接

android classloader雙親委託模式
dubbo源碼解析-spi(一)
Java中SPI機制深刻及源碼解析
走出ClassLoader的迷宮

相關文章
相關標籤/搜索