1、SPI簡介
SPI 全稱為 Service Provider Interface,是一種服務發現機制。SPI 的本質是將介面實現類的全限定名配置在檔案中,並由服務載入器讀取配置檔案,載入實現類。這樣可以在執行時,動態為介面替換實現類。正因此特性,我們可以很容易的通過 SPI 機制為我們的程式提供擴充功能。SPI 機制在第三方框架中也有所應用,比如 Dubbo 就是通過 SPI 機制載入所有的元件。不過,Dubbo 並未使用 Java 原生的 SPI 機制,而是對其進行了增強,使其能夠更好的滿足需求。在 Dubbo 中,SPI 是一個非常重要的模組。基於 SPI,我們可以很容易的對 Dubbo 進行擴充。如果大家想要學習 Dubbo 的原始碼,SPI 機制務必弄懂。接下來,我們先來了解一下 Java SPI 與 Dubbo SPI 的用法,然後再來分析 Dubbo SPI 的原始碼。
2、SPI示例
2.1 Java SPI示例
本節通過一個示例演示 Java SPI 的使用方法。首先,我們定義一個介面,名稱為 HelloService。
public interface HelloService {
void sayHello();
}
家下來定義兩個實現類:HelloAService、HelloBService;
public class HelloAService implements HelloService {
@Override
public void sayHello() {
System.out.println("Hello, I am A");
}
}
public class HelloBService implements HelloService {
@Override
public void sayHello() {
System.out.println("Hello, I am B");
}
}
接下來 META-INF/services 資料夾下建立一個檔案,名稱為 Robot 的全限定名 org.apache.spi.Robot。檔案內容為實現類的全限定的類名,如下:
org.apache.spi.HelloAService
org.apache.spi.HelloBService
做好所需的準備工作,接下來編寫程式碼進行測試
public class JavaSPITest {
@Test
public void sayHello() throws Exception {
ServiceLoader<HelloService> serviceLoader = ServiceLoader.load(HelloService.class);
System.out.println("Java SPI");
serviceLoader.forEach(HelloService::sayHello);
}
}
最後結果如下:
Java SPI
Hello, I am A
Hello, I am B
從測試結果可以看出,我們的兩個實現類被成功的載入,並輸出了相應的內容。關於 Java SPI 的演示先到這裡,接下來演示 Dubbo SPI。
2.2 Dubbo SPI
Dubbo 並未使用 Java SPI,而是重新實現了一套功能更強的 SPI 機制。Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以載入指定的實現類。Dubbo SPI 所需的配置檔案需放置在 META-INF/dubbo 路徑下,配置內容如下。
helloAService = org.apache.spi.HelloAService
helloBService = org.apache.spi.HelloBService
與 Java SPI 實現類配置不同,Dubbo SPI 是通過鍵值對的方式進行配置,這樣我們可以按需載入指定的實現類。另外,在測試 Dubbo SPI 時,需要在 Robot 介面上標註 @SPI 註解。下面來演示 Dubbo SPI 的用法:
public class DubboSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<HelloService> extensionLoader =
ExtensionLoader.getExtensionLoader(HelloService.class);
HelloService a = extensionLoader.getExtension("HelloAService");
a.sayHello();
HelloService b = extensionLoader.getExtension("HelloBService");
b.sayHello();
}
}
測試結果如下:
Java SPI
Hello, I am A
Hello, I am B
3、Dubbo SPI原始碼解析
Dubbo SPI相關邏輯都在ExtensionLoader 類中,首先通過getExtensionLoader獲取一個ExtensionLoader例項,然後在根據getExtension獲取type的擴充套件類。
getExtensionLoader方法比較簡單,先從快取中獲取,如果快取不存在,則建立ExtensionLoader物件,並存入快取中,在看getExtension方法:
public T getExtension(String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
if ("true".equals(name)) {
return getDefaultExtension();
}
// 根據副檔名從快取中獲取,快取中沒有,建立並存入快取
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
// 雙重檢查
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 根據副檔名建立例項物件
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
上面程式碼的邏輯比較簡單,首先檢查快取,快取未命中則建立擴充物件。下面我們來看一下建立擴充物件的過程是怎樣的。
private T createExtension(String name) {
// 從配置檔案中載入所有的擴充類,可得到“配置項名稱”到“配置類”的對映關係表
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
for (Class<?> wrapperClass : wrapperClasses) {
// 將當前 instance 作為引數傳給 Wrapper 的構造方法,並通過反射建立 Wrapper 例項。
// 然後向 Wrapper 例項中注入依賴,最後將 Wrapper 例項再次賦值給 instance 變數
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
type + ") could not be instantiated: " + t.getMessage(), t);
}
}
我們在通過名稱獲取擴充類之前,首先需要根據配置檔案解析出擴充項名稱到擴充類的對映關係表(Map<名稱, 擴充類>),之後再根據擴充項名稱從對映關係表中取出相應的擴充類即可。相關過程的程式碼分析如下
private Map<String, Class<?>> getExtensionClasses() {
// 從快取中獲取
Map<String, Class<?>> classes = cachedClasses.get();
// 雙重檢查
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 載入所有的擴充套件類
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
getExtensionClasses方法同樣是先從快取中讀取,快取不存在,在去載入:
private Map<String, Class<?>> loadExtensionClasses() {
// 獲取擴充套件類SPI註解
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
if (names.length > 1) {
throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
if (names.length == 1) {
cachedDefaultName = names[0];
}
}
}
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
// 載入指定資料夾下的配置檔案
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
return extensionClasses;
}
loadExtensionClasses 方法總共做了兩件事情,一是對 SPI 註解進行解析,二是呼叫 loadDirectory 方法載入指定資料夾配置檔案。SPI 註解解析過程比較簡單,無需多說。下面我們來看一下 loadDirectory 做了哪些事情。
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
// fileName = 資料夾路徑 + type 全限定名
String fileName = dir + type;
try {
Enumeration<java.net.URL> urls;
ClassLoader classLoader = findClassLoader();
if (classLoader != null) {
// 根據檔名載入所有的同名檔案
urls = classLoader.getResources(fileName);
} else {
urls = ClassLoader.getSystemResources(fileName);
}
if (urls != null) {
while (urls.hasMoreElements()) {
java.net.URL resourceURL = urls.nextElement();
// 載入資源
loadResource(extensionClasses, classLoader, resourceURL);
}
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", description file: " + fileName + ").", t);
}
}
loadDirectory 方法先通過 classLoader 獲取所有資源連結,然後再通過 loadResource 方法載入資源。我們繼續跟下去,看一下 loadResource 方法的實現。
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8"));
try {
String line;
// 讀取檔案中內容
while ((line = reader.readLine()) != null) {
final int ci = line.indexOf(`#`);
if (ci >= 0) {
line = line.substring(0, ci);
}
line = line.trim();
if (line.length() > 0) {
try {
String name = null;
int i = line.indexOf(`=`);
if (i > 0) {
// 以等於號 = 為界,擷取鍵與值
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
// 載入類,並通過 loadClass 方法對類進行快取
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
exceptions.put(line, e);
}
}
}
} finally {
reader.close();
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", class file: " + resourceURL + ") in " + resourceURL, t);
}
}
loadClass()方法主要是利用反射原理,根據類的許可權定名載入成類,並存入快取中