SPI機制剖析——基於DriverManager+ServiceLoader的原始碼分析
阿新 • • 發佈:2020-12-20
我的上一篇部落格[類載入器與雙親委派](https://www.cnblogs.com/buptleida/p/14111058.html)中提到,SPI機制是一種上級類載入器呼叫下級類載入器的情形,因此會打破類載入的雙親委派模型。為了深入理解其中的細節,本部落格詳細剖析一下SPI機制,並以JDBC為例,基於原始碼來進行分析。
## SPI
### 原理介紹
SPI(Service Provider Interface),是JDK內建的服務提供發現機制。即JDK內部定義規範的介面,不同廠商基於標準服務介面實現具體的實現類和方法。SPI一般被用來做框架擴充套件的開發。
下面這張圖,很簡明扼要地闡釋了SPI的機理。
![SPI機制](https://qiniu.debrisflow.cn/20201219SPI.png)
與SPI相對應的,是我們耳熟能詳的API。API不需要上圖中“標準服務介面”這一環節,而是呼叫方直接呼叫服務提供方。按照上一篇部落格的分析,“標準服務介面”位於Java核心類庫中,使用boot類載入器進行載入,而boot類載入器是無法獲取“第三方實現類”的位置的。所以,相較於API而言,SPI需要打破雙親委派模型。
### 優缺點
#### 好處
但是,我陷入思考,SPI這樣的模式有什麼好處嗎,或者說API有什麼缺點嗎?
想象一下,如果程式直接呼叫第三方類庫,當第三方類庫發生改動時,應用程式程式碼很可能需要隨之改動。但如果在JDK內部定義標準服務介面,要求第三方廠商實現這些介面,那無論實現類如何改動,只要標準介面不變,都不會影響到應用程式。所以我認為SPI機制的根本目的是為了**“解耦”**。這也就是面向物件中所謂的“介面程式設計”,把裝配的控制權移到程式之外。
許多著名的第三方類庫都採納了SPI機制,JDBC就是其中之一。資料庫廠商會基於標準介面來開發相應的連線庫。如MySQL何PostgreSql的驅動都實現了標準介面:java.sql.Driver。對於應用程式而言,無需關心是MySQL還是PostgreSql,只需要與標準服務介面打交道即可。SPI正是基於這種模式完成了解耦合。
#### 不足
當然,即便如此,SPI依舊是存在缺點和不足的,如下:
1. 不能按需載入。需要遍歷所有的實現,並且進行例項化,某些實現的例項化可能很耗時,這樣會造成浪費;
2. 獲取實現類的方式不夠靈活,只能通過Iterator獲取,不能根據某個引數來獲取實現類;
3. ServiceLoader類的例項執行緒不安全。
## JDBC的SPI機制
首先來看一段使用JDBC的簡單程式碼:
```
@Test
public void testJDBC() throws SQLException, ClassNotFoundException {
String url = "jdbc:mysql://localhost:3307/mls";
String userName = "root";
String password = "123456";
// Class.forName("com.mysql.cj.jdbc.Driver");
Connection con = DriverManager.getConnection(url, userName, password);
Statement statement = con.createStatement();
String sql = "select * from mlsdb where id=1";
ResultSet rs = statement.executeQuery(sql);
while (rs.next()) {
System.out.println(rs.getString("province"));
}
}
```
注意到中間有一行註釋的程式碼`Class.forName("com.mysql.cj.jdbc.Driver");`,其實這一行可寫可不寫。
我的倒數第二篇部落格[類載入時機與過程](https://www.cnblogs.com/buptleida/p/14094708.html)裡提到,Class.forName方法會觸發“初始化”,即觸發類載入的進行。因此如果寫上這行程式碼,此處則是使用APP類載入器載入mysql的jdbc驅動類。
然而,這一句Class.forName不用寫,程式碼也能正常執行。因為載入DriverManager類時,會將MySQL的Driver物件註冊進DriverManager中。具體流程後文會細說。其實這就是SPI思想的一個典型的實現。得益於SPI思想,應用程式中無需指定類似"com.mysql.cj.jdbc.Driver"這種全類名,儘可能地將第三方驅動從應用程式中解耦出來。
下面,通過原始碼來分析驅動載入以及服務發現的過程,主要涉及到DriverManager和ServiceLoader兩個類
### 原始碼分析
DriverManager是用於管理Jdbc驅動的基礎服務類,位於Java.sql包中,因此是由boot類載入器來進行載入。載入該類時,會執行如下程式碼塊:
```
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
```
上述靜態程式碼塊會執行loadInitialDrivers()方法,該方法用於載入各個資料庫驅動。程式碼如下:
```
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new Privileg