源碼級深度理解 Java SPI( 三 )


學習過 JVM 的讀者 , 想必都了解過類加載器的雙親委派模型(Parents Delegation Model) 。雙親委派模型要求除了頂層的 BootstrapClassLoader 外 , 其余的類加載器都應有自己的父類加載器 。這里類加載器之間的父子關系一般通過組合(Composition)關系來實現,而不是通過繼承(Inheritance)的關系實現 。
雙親委派機制約定了:一個類加載器首先將類加載請求傳送到父類加載器 , 只有當父類加載器無法完成類加載請求時才嘗試加載 。
雙親委派的好處:使得 Java 類伴隨著它的類加載器,天然具備一種帶有優先級的層次關系 , 從而使得類加載得到統一 , 不會出現重復加載的問題:

  1. 系統類防止內存中出現多份同樣的字節碼
  2. 保證 Java 程序安全穩定運行
例如:java.lang.Object 存放在 rt.jar 中 , 如果編寫另外一個 java.lang.Object 的類并放到 classpath 中,程序可以編譯通過 。因為雙親委派模型的存在,所以在 rt.jar 中的 Object 比在 classpath 中的 Object 優先級更高,因為 rt.jar 中的 Object 使用的是啟動類加載器,而 classpath 中的 Object 使用的是應用程序類加載器 。正因為 rt.jar 中的 Object 優先級更高,因為程序中所有的 Object 都是這個 Object 。
雙親委派的限制:子類加載器可以使用父類加載器已經加載的類,而父類加載器無法使用子類加載器已經加載的 ?!@就導致了雙親委派模型并不能解決所有的類加載器問題 。Java SPI 就面臨著這樣的問題:
  • SPI 的接口是 Java 核心庫的一部分 , 是由 BootstrapClassLoader 加載的;
  • 而 SPI 實現的 Java 類一般是由 AppClassLoader 來加載的 。BootstrapClassLoader 是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫 。它也不能代理給 AppClassLoader,因為它是最頂層的類加載器 。這也解釋了本節開始的問題——為什么加載 SPI 服務時,需要指定類加載器 ClassLoader 呢?因為如果不指定 ClassLoader,則無法獲取 SPI 服務 。
如果不做任何的設置,Java 應用的線程的上下文類加載器默認就是 AppClassLoader 。在核心類庫使用 SPI 接口時,傳遞的類加載器使用線程上下文類加載器,就可以成功的加載到 SPI 實現的類 。線程上下文類加載器在很多 SPI 的實現中都會用到 。
通常可以通過Thread.currentThread().getClassLoader()和 Thread.currentThread().getContextClassLoader() 獲取線程上下文類加載器 。
3.4 Java SPI 的不足Java SPI 存在一些不足:
  • 不能按需加載,需要遍歷所有的實現,并實例化,然后在循環中才能找到我們需要的實現 。如果不想用某些實現類,或者某些類實例化很耗時,它也被載入并實例化了,這就造成了浪費 。
  • 獲取某個實現類的方式不夠靈活,只能通過 Iterator 形式獲?。?不能根據某個參數來獲取對應的實現類 。
  • 多個并發多線程使用 ServiceLoader 類的實例是不安全的 。
四、SPI 應用場景SPI 在 Java 開發中應用十分廣泛 。首先,在 Java 的 java.util.spi package 中就約定了很多 SPI 接口 。下面,列舉一些 SPI 接口:
  • TimeZoneNameProvider: 為 TimeZone 類提供本地化的時區名稱 。
  • DateFormatProvider: 為指定的語言環境提供日期和時間格式 。
  • NumberFormatProvider: 為 NumberFormat 類提供貨幣、整數和百分比值 。
  • Driver: 從 4.0 版開始,JDBC API 支持 SPI 模式 。舊版本使用 Class.forName() 方法加載驅動程序 。
  • PersistenceProvider: 提供 JPA API 的實現 。
  • 等等
除此以外,SPI 還有很多應用,下面列舉幾個經典案例 。
4.1 SPI 應用案例之 JDBC DriverManager作為 Java 工程師,尤其是 CRUD 工程師,相必都非常熟悉 JDBC 。眾所周知,關系型數據庫有很多種,如:MySQL、Oracle、PostgreSQL 等等 。JDBC 如何識別各種數據庫的驅動呢?
4.1.1 創建數據庫連接我們先回顧一下,JDBC 如何創建數據庫連接的呢?
在 JDBC4.0 之前,連接數據庫的時候 , 通常會用 Class.forName(XXX) 方法來加載數據庫相應的驅動,然后再獲取數據庫連接,繼而進行 CRUD 等操作 。
Class.forName("com.mysql.jdbc.Driver")而 JDBC4.0 之后 , 不再需要用Class.forName(XXX) 方法來加載數據庫驅動 , 直接獲取連接就可以了 。顯然,這種方式很方便,但是如何做到的呢?
(1)JDBC 接口:首先,Java 中內置了接口 java.sql.Driver 。
(2)JDBC 接口實現:各個數據庫的驅動自行實現 java.sql.Driver 接口,用于管理數據庫連接 。

推薦閱讀