Dubbo里除了Service和Config層為API,其它各層均為SPI。相比于JAVA中的SPI僅僅通過接口類名獲取所有實現,Dubbo的實現可以通過接口類名和key值來獲取一個具體的實現。通過SPI機制,Dubbo實現了面向插件編程,只定義了模塊的接口,實現由各插件來完成。
1. 使用方式
1.1 Java SPI
在擴展類的jar包內,放置擴展點配置文件META-INF/service/接口全限定名,內容為:擴展實現類全限定名,多個實現類用換行符分隔。
如下為MySQL中Driver接口的實現:

package com.mysql.jdbc;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
... }
調用時使用ServiceLoader加載所有的實現并通過循環來找到目標實現類
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();try{
while(driversIterator.hasNext()) {
driversIterator.next(); }} catch(Throwable t) {
// Do nothing
}
1.2 Dubbo SPI
拿Dubbo中Protocol接口來說,Protocol的定義如下:
package org.Apache.dubbo.rpc;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;
@SPI("dubbo")
public interface Protocol { int getDefaultPort();
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException; @Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; void destroy();
}
需要指出的是Invoker繼承了Node接口,而Node接口提供了getUrl方法,既每個方法都能從入參獲得URL對象。
public interface Node {
URL getUrl();
boolean isAvailable();
void destroy();
}
要求
1.接口上有org.apache.dubbo.common.extension.SPI注解,提供默認的實現
2.對于支持自適應擴展的方法要求方法入參能獲得org.apache.dubbo.common.URL對象,同時方法上有org.apache.dubbo.common.extension.Adaptive注解,Adaptive注解可以提供多個key名,以便從URL中獲取對應key的值,從而匹配到對應的實現(這里Protocol比較特別,沒有提供key名也能根據URL來動態獲取實現,后面會說明)
3.在擴展類的jar包內,放置擴展點配置文件META-INF/dubbo/接口全限定名,內容為:配置名=擴展實現類全限定名,多個實現類用換行符分隔。如Protocol的默認實現:

注意:META-INF/dubbo/internal為Dubbo內部實現的配置文件路徑
調用時通過ExtensionLoader根據需要來選擇具體的實現類,
ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
可選方式包括
1.選擇默認的實現
Protocol protocol = loader.getDefaultExtension();
2.根據指定key選擇實現
Protocol protocol = loader.getExtension("dubbo");
3.根據URL參數動態獲取實現
Protocol protocol = loader.getAdaptiveExtension();
2. Dubbo SPI 特性
Dubbo對Java中的標準SPI進行了擴展增強,官方文檔中提到其提供了如下特性:
- 擴展點自動包裝
- 擴展點自動裝配
- 擴展點自適應
- 擴展點自動激活
在介紹各個特性前先介紹下大概的內部實現。從上面的使用中可以看到Dubbo對SPI擴展的主要實現在ExtensionLoader類中,關于這個類源碼的講解可以看官方文檔,講解的很詳細,這邊主要說下大概過程:
- 根據傳入的類型從classpath中查找META-INF/dubbo/internal和META-INF/dubbo路徑下所有對應的擴展點配置文件
- 讀取擴展點配置文件中所有的鍵值對
- 根據鍵值對緩存Class對象,如果類有同類型參數的構造函數,則為包裝類,會緩存在另一個容器中
- 實例化對象,緩存后并返回
2.1 擴展點自動包裝
在返回真正實例前,會用該類型的包裝類進行包裝,既采用裝飾器模式進行功能增強。
if (CollectionUtils.isNotEmpty(wrApperClasses)) {
// 循環創建 Wrapper 實例
for (Class<?> wrapperClass : wrapperClasses) {
// 將當前 instance 作為參數傳給 Wrapper 的構造方法,并通過反射創建 Wrapper 實例。
// 然后向 Wrapper 實例中注入依賴,最后將 Wrapper 實例再次賦值給 instance 變量
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
其實就是使用靜態代理來實現AOP

2.2 擴展點自動裝配
返回實例前會遍歷所有的setXXX方法,判斷set方法參數是否存在自適應對象,如果存在則通過ExtensionLoader加載自適應對象然后進行賦值,可以通過方法上加org.apache.dubbo.common.extension.DisableInject注解來屏蔽該功能。該功能其實就是實現了IOC,具體可以看ExtensionLoader的injectExtension方法。
2.3 擴展點自適應
同上面介紹的一樣,Dubbo的SPI可以根據傳入的URL參數中攜帶的數據來動態選擇具體的實現。
2.4 擴展點自動激活
上面介紹過,Adaptive注解是加在方法上的,類似的有個注解org.apache.dubbo.common.extension.Activate是加在實現類上。當加在Class上時,ExtensionLoader會將對應的key和Class信息緩存到另一個容器中,后續可以通過ExtensionLoader獲取某一類的實現列表,既如下方法
public List<T> getActivateExtension(URL url, String[] values){
...}
3. ExtensionLoader自適應擴展機制
ExtensionLoader自適應擴展機制的大概實現邏輯是這樣的:Dubbo會為拓展接口生成具有代理功能的代碼,然后通過 javassist 或 jdk 編譯這段代碼,得到 Class 類。最后再通過反射創建代理類,在代理類中,就可以通過URL對象的參數來確定到底調用哪個實現類。主要實現在createAdaptiveExtensionClass方法中。
private Class<?> createAdaptiveExtensionClass() {
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
ClassLoader classLoader = findClassLoader(); org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}
上面的Protocol接口經過處理后的內容如下:
public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
public void destroy() {
throw new UnsupportedOperationException(
"The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
} public int getDefaultPort() {
throw new UnsupportedOperationException(
"The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
} public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {
if (arg0 == null) {
throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
} if (arg0.getUrl() == null) {
throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
} org.apache.dubbo.common.URL url = arg0.getUrl(); String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
if (extName == null) {
throw new IllegalStateException(
"Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
} org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.export(arg0);
} public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
if (arg1 == null) {
throw new IllegalArgumentException("url == null");
} org.apache.dubbo.common.URL url = arg1; String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
if (extName == null) {
throw new IllegalStateException(
"Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
} org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.refer(arg0, arg1);
}}
前面提到過,Protocol是一個比較特殊的接口,Adaptive注解里沒有指定Key,因而沒法通過URL攜帶的參數獲取具體的實現類。從上面處理過后的內容來看,可以看到,對于Protocol接口直接使用的URL的protocol屬性字段的值來進行判斷的。
如下例子是在Adaptive里有提供key的實現處理過后的內容:
public java.lang.String sayHello(org.apache.dubbo.common.URL arg0) {
if (arg0 == null) {
throw new IllegalArgumentException("url == null");
} org.apache.dubbo.common.URL url = arg0; String extName = url.getParameter("keyA", "dobbo");
if (extName == null) {
throw new IllegalStateException("Failed to get extension (cn.demo.spi.Factory) name from url (" + url.toString() + ") use keys([keyA])");
} cn.demo.spi.Factory extension = (cn.demo.spi.Factory) ExtensionLoader.getExtensionLoader(cn.demo.spi.Factory.class).getExtension(extName);
return extension.sayHello(arg0);
}
可以發現這種情況直接使用的URL的getParameter方法從攜帶的參數中獲取對應的值。
通過查看AdaptiveClassCodeGenerator的實現可以發現該類的generateExtNameAssignment里對protocol做了特殊的判斷。AdaptiveClassCodeGenerator完成了代理類內容的創建,大概過程為:根據原接口定義(type),包裝出根據URL參數動態調用getExtension方法的實現類,然后將動作實際委托給了ExtensionLoader.getExtensionLoader(type).getExtension(extName);方法。其中extName為方法上的Adaptive注解指定的key的對應值,獲取過程為:
- 如果Adaptive注解配置的key為空,則使用類名作為擴展名
- 如果擴展名為protocol,則從URL的protocol里獲取對應的值
- 如果擴展名不為protocol,且方法參數里有org.apache.dubbo.rpc.Invocation,則從URL.getMethodParameter里獲取
- 以上都沒有則直接從URL.getParameter中獲取。
Protocol接口由于沒有在Adaptive注解里指定key,則會使用類名protocol作為默認的擴展名,從而命中第2條規則。
通過自適應擴展機制,Dubbo框架的各層的核心實現都是基于接口的,而將具體的實現下放到插件中。根據加載的插件不同,各層能夠選擇適合的實現而不會影響核心的邏輯流程。如Protocol接口,表示通信協議,可選的實現包括dubbo、rmi、http等。