【Java】ServiceLoader機制深入
ServiceLoader
是在 jdk1.6開始引入的,它主要的功能是用來完成對SPI的provider的載入.
簡單理解其功能就是,根據給定的介面,找到當前介面所有實現的類.
現在給出一個這種機制的應用場景示例: jdbc的DriverManger,下面是一個程式碼片段
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
Connection conn = null;
...
try {
conn =
DriverManager.getConnection ("jdbc:mysql://localhost/test?" +
"user=test&password=test");
// Do something with the Connection
...
} catch (SQLException ex) {
// handle any errors
System.out.println("SQLException: " + ex.getMessage());
System.out.println("SQLState: " + ex.getSQLState ());
System.out.println("VendorError: " + ex.getErrorCode());
}
上面這段程式碼本身沒有很複雜,這裡我們需要了解的是DriverManager是如何正確的獲取到對應支援該url的Connection,下面是DriverManager的getConnection部分原始碼:
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
從上面的程式碼當中我們可以瞭解到DriverManager其實是通過不斷嘗試內部已經註冊的Driver來獲取對應的Connection,直到成功得到Connection為止,這部分還是比較好理解,這時候你會有新的疑問,
這個registeredDrivers
是怎麼註冊的?(因為DriverManager本身是不知道當前應用環境有多少個實現,它是不可能通過new具體的例項來完成的)
對於registeredDrivers
是完成它的初始化工作的,可以進一步看DriverManager這類在初始化的時候完成了相關的實現類載入,下面是載入的原始碼片段:
/**
* 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");
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//核心的操作
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
...省略
}
從上面的程式碼可以看到,首先它通過讀取系統引數來了解使用者指定的driver實現,繼而進一步通過ServiceLoader
這個類完成其它的Driver
的實現類.
現在的問題又進一步轉移到ServiceLoader
是如何找到當前系統的Driver
的實現類的.
通過ServiceLoader
的原始碼我們瞭解到,ServiceLoader查詢對應的實現是通過一個統一規範的方式來查詢,如果實現了Driver
這個介面,你就得按ServiceLoader指定的方式來暴露你的實現.
具體的暴露方式是:
- 在最終的jar檔案的
META-INF/services/
路徑下,配置當前jar實現介面的資訊檔案; - 檔案的名稱就是實現介面的名稱,比如當前這個例子,這對應的檔名詞是
java.sql.Driver
; - 檔案裡面的內容是具體的實現類完整的包資訊,比如:mysql的對應實現是
com.mysql.cj.jdbc.Driver
, 所以檔案裡面的內容也要和這個保持一致;另外需要注意的是,檔案裡面的內容可以是多行資訊,這種情況是你提供了多種實現.
下面我們去看一下mysql的driver檔案裡面配置資訊來核實一下我們的理解:
接下來,我們可以通過一個簡單的例子來感受一下:
測試專案使用maven來構建
1 定義抽象介面
專案結構如下截圖:
下面是部分檔案的定義
1 Driver.java
package com.together.learning.java.spi;
/**
* @author jiangjian
*/
public interface Driver {
String getVendorName();
}
2 pom.xml檔案
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.together</groupId>
<artifactId>driver-api</artifactId>
<version>1.0-SNAPSHOT</version>
</project>
2 實現抽象介面
專案結構如下截圖:
下面是部分檔案的定義
1 ZhangshanDriverImpl.java
package com.zhangsan.driver;
import com.together.learning.java.spi.Driver;
/**
* @author jiangjian
*/
public class ZhangshanDriverImpl implements Driver {
public String getVendorName() {
return "zhangsan corp";
}
}
2 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zhangsan</groupId>
<artifactId>driver-api-impl</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.together</groupId>
<artifactId>driver-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
3 com.together.learning.java.spi.Driver檔案內容
com.zhangsan.driver.ZhangshanDriverImpl
3 測試ServiceLoader
專案結構如下截圖:
下面是部分檔案的定義
1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.together</groupId>
<artifactId>service-loader-test</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.together</groupId>
<artifactId>driver-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.zhangsan</groupId>
<artifactId>driver-api-impl</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
2 Test.java
package com.together.test;
import com.together.learning.java.spi.Driver;
import java.util.ServiceLoader;
/**
* @author jiangjian
*/
public class Test {
public static void main(String[] args) {
ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
for(Driver driver : drivers) {
System.out.println(driver.getVendorName());
}
}
}
至此,ServiceLoader大致的功能介紹完了,有興趣的可以進一步閱讀ServiceLoader原始碼,裡面的LazyIterator
這種實現還是值得學習和借鑑的.