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


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(c);  
     }  
     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的迷宮


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM