在專案開發中,經常會使用到資料庫驅動,我們連線的資料庫可能是MySQL
也有可能是Oracle
,但是不管使用什麼資料庫都是引入資料庫驅動設定相應的地址、使用者、密碼資訊就可以使用而不用修改業務程式碼。
這是因為在JDK
中提供了一個java.sql.Driver
介面。各個資料庫廠商只需要實現這個介面,當我們引入相應驅動,連線資料庫的時候,就會使用廠商提供的實現,那麼又是如何知道廠商實現的類路徑的呢??
SPI
全名Service Provider interface
,翻譯過來就是「服務提供介面」,再說簡單就是提供某一個服務的介面, 提供給服務開發者或者服務生產商來進行實現。
Java SPI
是JDK
內建的一種動態載入擴充套件點的實現。
這個機制在一般的業務程式碼中很少用到(個人接觸到的業務沒有用到過),但是再底層框架中卻被大量使用,包括JDBC
、Dubbo
、Spring
框架、紀錄檔介面中都有用到,不同的是有的使用Java
原生的實現,有的框架則自己實現了一套SPI
機制
API
全稱Application Programming Interface
, 翻譯為「應用程式介面」,指的是應用程式為外部提供服務的介面,這個介面通常由服務提供者自行開發,定義好介面後很少改動。API
與SPI
示意圖如圖1,圖2所示
一般應用(模組)之間通過介面進行通訊,服務提供方提供介面並進行實現後,呼叫方就可以通過呼叫這個介面擁有服務提供發提供的能力,這個就是API
當介面是由服務呼叫方提供,並且由服務提供方進行實現時,服務呼叫方就可以根據自己的需要選擇特定實現,而不用更改業務程式碼以獲取相應的功能,這個就是SPI
這個功能是向註冊中心註冊服務的一個範例(過於簡單的範例),
首先定義一個介面Registry
, 這個介面只有一個功能,就是向註冊中心註冊一個服務,但是我現在並不確定我選的是什麼註冊中心,於是提供了一個統一的介面,由各個廠商進行實現
package cn.bmilk.chat.spi;
public interface Registry {
void registry(String host, int port);
}
廠商實現好後,需要在其META-INF/services
資料夾下新增一個檔案,檔名為該介面的全限定名即:cn.bmilk.chat.spi.Registry
, 內容為介面實現的全限定名,這裡我寫了兩個
cn.bmilk.chat.spi.EurekaRegistry
cn.bmilk.chat.spi.ZookeeperRegistry
兩個類的都是空實現,內容如下
@Override
public void registry(String host, int port) {
System.out.println(this + "registry , host = " + host +" port = " + port);
}
下面編寫測試主類,通過 ServiceLoader
載入 Registry
實現
public class MainTest {
public static void main(String[] args) {
ServiceLoader<Registry> load = ServiceLoader.load(Registry.class);
Iterator<Registry> iterator = load.iterator();
while (iterator.hasNext()){
Registry registry = iterator.next();
registry.registry("127.0.0.1", 10086);
}
}
}
執行結果
class cn.bmilk.chat.spi.EurekaRegistry
cn.bmilk.chat.spi.EurekaRegistry@12a3a380registry , host = 127.0.0.1 port = 10086
class cn.bmilk.chat.spi.ZookeeperRegistry
cn.bmilk.chat.spi.ZookeeperRegistry@29453f44registry , host = 127.0.0.1 port = 10086
從執行結果中可以看到EurekaRegistry
和ZookeeperRegistry
都被範例化並且生成相應的物件。但是我們全程並沒有顯示的載入和生成EurekaRegistry
和ZookeeperRegistry
類物件,那麼是怎麼來的呢?
SPI
機制的核心就是ServiceLoader
類。其主要的屬性如下:
// 指出介面組態檔的位置,也就是為什麼要在META-INF/services/下建立介面的全限定名檔案的原因
private static final String PREFIX = "META-INF/services/";
// 正在被載入的類(介面)的class物件
private final Class<S> service;
// 載入使用的類載入器
private final ClassLoader loader;
// 建立 ServiceLoader 時採用的存取控制上下文
private final AccessControlContext acc;
// 快取已經載入的實現, 按範例化順序快取
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
load()
方法的實現如下:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 使用當前執行緒的ClassLoader進行載入待載入的實現類
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// load 方法本質是建立一個ServiceLoader物件
return new ServiceLoader<>(service, loader);
}
// new ServiceLoader<>(service, loader)的實現
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
providers.clear();
// 根據介面型別(父類別)和類載入器初始化LazyIterator
lookupIterator = new LazyIterator(service, loader);
}
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
跟蹤load()
方法發現其本質是建立了一個ServiceLoader
物件,其共有兩個引數,分別是代載入的類父類別(介面)Class
類物件和類載入器。在構造方法中完成了兩件事,一個是變數賦值,一個是呼叫reload()
方法。reload()
方法則根據介面型別(父類別)和類載入器初始化LazyIterator
當執行ServiceLoader#iterator()
時,會建立java.util.Iterator
匿名內部類實現:
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
當執行hasNext()
方法時,會先去providers
查詢已經載入的快取實現,如果不存在,則會繼續呼叫LazyIterator#hasNext()
用於發現尚未載入的實現,最後的實現在LazyIterator#hasNextService()
中
LazyIterator
的關鍵屬性
// 快取所有需要查詢jar包(檔案)路徑,
Enumeration<URL> configs = null;
// 快取所有被查詢到的實現類全限定名
Iterator<String> pending = null;
// 迭代器使用,下一個需要被載入的類全限定名
String nextName = null;
hasNextService()
實現核心如下:
// 獲取所由需要掃描的包路徑
configs = loader.getResources(fullName);
// 迴圈掃描configs中所有的包路徑,解析META-INF/services中的指定檔案(上例中的cn.bmilk.chat.spi.Registry檔案)
//
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// pending快取了所有查詢到的類全限定名
pending = parse(service, configs.nextElement());
}
在知道是否存在介面的實現後,就是通過next()
方法獲取實現,核心功能由nextService()
貢獻,核心實現如下:
// 獲取一個實現類全限定名
String cn = nextName;
// 載入這個類
Class<?> c = Class.forName(cn, false, loader);
// 使用反射建立物件
c.newInstance()
hasNextService()
完成堆組態檔的讀取,nextService()
完成類的載入和物件的建立,這個一切都沒有在ServiceLoader
建立時完成,這也是體現了延遲Lazy
的一個含義
loadInstalled()
和load()
一樣,本質都是建立了一個ServiceLoader
d物件,不同點是使用的載入器不同,load()
使用的是Thread.currentThread().getContextClassLoader()
當前執行緒的上下文載入器, loadInstalled()
使用的是ExtClassLoader
載入器來載入
具體實現如下:
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
使用這個方法將只掃描JDK
安裝目錄jre/lib/ext
下的jar
包中指定的實現,我們應用程式類路徑下的實現將會被忽略掉
Java SPI
雖然使用了懶載入機制,但是其獲取一個實現時,需要使用迭代器迴圈載入所有的實現類這個兩個問題,在Dubbo
實現自己的SPI
機制時進行了增強,可以僅載入自己想要的擴充套件實現。
想不明白 說不清楚,想明白再補充