JDK中的SPI機制
前言
最近學習類載入的過程中,瞭解到JDK提供給我們的一個可擴充套件的介面:java.util.ServiceLoader
,
之前自己不瞭解這個機制,甚是慚愧...
什麼是SPI
SPI全稱為(Service Provider Interface),是JDK內建的一種服務提供發現機制。SPI是一種動態替換髮現的機制,比如有個介面,想執行時動態的給它新增實現,你只需要新增一個實現。我們經常遇到的就是java.sql.Driver介面,其他不同廠商可以針對同一介面做出不同的實現,mysql和postgresql都有不同的實現提供給使用者,而Java的SPI機制可以為某個介面尋找服務實現。
首先放個圖:我們在“呼叫方”和“實現方”之間需要引入“介面”,可以思考一下什麼情況應該把介面放入呼叫方,什麼時候可以把介面歸為實現方。
先來看看介面屬於實現方的情況,這個很容易理解,實現方提供了介面和實現,我們可以引用介面來達到呼叫某實現類的功能,這就是我們經常說的api,它具有以下特徵:
1.是對實現的說明(我可以給你提供什麼)
2.組織上位於實現方所在的包中
3.實現和介面在一個包中
當介面屬於呼叫方時,我們就將其稱為spi,全稱為:service provider interface,spi的規則如下:
1.是對實現的約束(要提供這個功能,實現者需要做那些事情)
2.組織上位於呼叫方所在的包中
3.實現位於獨立的包中(也可認為在提供方中)
簡而言之
API會告訴您特定的類/方法為您執行什麼操作,而SPI則告訴您必須執行哪些操作才能符合要求。通常,API和SPI是分開的。例如,在JDBC中,Driver類是SPI的一部分:如果只想使用JDBC,則不需要直接使用它,但是實現JDBC驅動程式的每個人都必須實現該類。但是,有時它們會重疊
JDK SPI使用說明及示例
要使用SPI比較簡單,只需要按照以下幾個步驟操作即可:
1.在jar包的META-INF/services目錄下建立一個以"介面全限定名"為命名的檔案,內容為實現類的全限定名
2.介面實現類所在的jar包在classpath下
3.主程式通過java.util.ServiceLoader動態狀態實現模組,它通過掃描META-INF/services目錄下的配置檔案找到實現類的全限定名,把類載入到JVM
4.SPI的實現類必須帶一個無參構造方法
舉例1
下例是使用maven引入了mysql的依賴後執行的,MySQL驅動內的截圖:
java.sql.Driver檔案全部內容:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
public class SpiTest {
public static void main(String[] args) {
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
Iterator<Driver> iterator = loader.iterator();
while(iterator.hasNext()) {
Driver driver = iterator.next();
System.out.println("driver is " + driver.getClass() + ", classLoader is " + driver.getClass().getClassLoader());
}
System.out.println("當前上下文類載入器是:" + Thread.currentThread().getContextClassLoader());
System.out.println("ServiceLoader的類載入器是:" + ServiceLoader.class.getClassLoader());
}
}
執行結果:
driver is class com.mysql.jdbc.Driver, classLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
driver is class com.mysql.fabric.jdbc.FabricMySQLDriver, classLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
當前上下文類載入器是:sun.misc.Launcher$AppClassLoader@18b4aac2
ServiceLoader的類載入器是:null
SPI相關的類載入的邏輯
因為ServiceLoader位於java.util.ServiceLoader
,所以這個類是會被啟動類載入器所載入,然後我們分析一下ServiceLoader.load(Driver.class)
的原始碼。
根據類載入的原理:如果一個類由類載入器A載入,那麼這個類的依賴類也會被類載入器A載入(前提是這個依賴類尚未被載入過)。
當執行ServiceLoader.load(Driver.class),如果不使用執行緒上下文類載入器來打破雙親委託模型,那麼該方法的關聯類也會被啟動類載入器載入。
333 public static <S> ServiceLoader<S> load(Class<S> service) {
334 ClassLoader cl = Thread.currentThread().getContextClassLoader();
335 return ServiceLoader.load(service, cl);
336 }
所以我們看到334行獲取了執行緒上下文類載入器,然後呼叫另一個過載的load方法去載入Driver(即所謂的Service)。
繼續跟蹤ServiceLoader.load(service, cl)
的程式碼,會發現它依次執行了如下動作:
1.初始化了一個:ServiceLoader物件,new ServiceLoader<>(service, loader)
2.執行reload()
3.new LazyIterator(service, loader)
這裡所有的loader都是執行緒上下文類載入器,它預設為系統類載入器。
這時,SpiTest中的ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
就執行完了,
跟著執行SpiTest中的Iterator<Driver> iterator = loader.iterator();
這裡當執行iterator.hasNext()
的時候,就會進入到剛才初始化的LazyIterator類中,執行其中的下列方法,
所以這也是為什麼"在jar包的META-INF/services目錄下建立一個以"介面全限定名"為命名的檔案,內容為實現類的全限定名"
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//這裡的PREFIX是一個常量:META-INF/services/
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
仔細閱讀內部類LazyIterator類的原始碼,就可以知道:
1.JDK是怎麼讀取META-INF/services目錄下的內容
2.JDK是怎麼載入這些SPI的類的