上一節提到過,Dubbo裡除了Service和Config層為API,其它各層均為SPI。相比於Java中的SPI僅僅通過接口類名獲取所有實現,Dubbo的實現可以通過接口類名和key值來獲取一個具體的實現。通過SPI機制,Dubbo實現了面向插件編程,只定義了模塊的接口,實現由各插件來完成。
1. 使用方式
1.1 Java SPI
在擴展類的jar包內,放置擴展點配置文件META-INF/service/接口全限定名,內容為:擴展實現類全限定名,多個實現類用換行符分隔。
如下為Mysql中Driver接口的實現:
<code>package
com.mysql.jdbc;import
java.sql.SQLException;public
class
Driver
extends
NonRegisteringDriver
implements
java
.sql
.Driver
{ ... }/<code>
調用時使用ServiceLoader加載所有的實現並通過循環來找到目標實現類
<code>ServiceLoader loadedDrivers = ServiceLoader.load(Driver.
class
); Iterator driversIterator = loadedDrivers.iterator();try
{while
(driversIterator.hasNext()) { driversIterator.next(); } }catch
(Throwable t) { }/<code>
1.2 Dubbo SPI
拿Dubbo中Protocol接口來說,Protocol的定義如下:
<code>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
Exporter export(Invoker invoker) throws RpcException;@Adaptive
Invoker refer(Class type, URL url) throws RpcException;void
destroy
(); }/<code>
需要指出的是Invoker繼承了Node接口,而Node接口提供了getUrl方法,既每個方法都能從入參獲得URL對象。
<code>public
interface
Node
{URL
getUrl
()
;boolean
isAvailable
()
;void
destroy
()
; }/<code>
要求
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根據需要來選擇具體的實現類,
<code>ExtensionLoader loader = ExtensionLoader.getExtensionLoader(Protocol.
class
);/<code>
可選方式包括
1.選擇默認的實現
<code>Protocol
protocol = loader.getDefaultExtension();/<code>
2.根據指定key選擇實現
<code>Protocol
protocol = loader.getExtension("dubbo"
);/<code>
3.根據URL參數動態獲取實現
<code>Protocol
protocol = loader.getAdaptiveExtension();/<code>
2. Dubbo SPI 特性
Dubbo對Java中的標準SPI進行了擴展增強,官方文檔中提到其提供瞭如下特性:
- 擴展點自動包裝
- 擴展點自動裝配
- 擴展點自適應
- 擴展點自動激活
在介紹各個特性前先介紹下大概的內部實現。從上面的使用中可以看到Dubbo對SPI擴展的主要實現在ExtensionLoader類中,關於這個類源碼的講解可以看官方文檔,講解的很詳細,這邊主要說下大概過程:
- 根據傳入的類型從classpath中查找META-INF/dubbo/internal和META-INF/dubbo路徑下所有對應的擴展點配置文件
- 讀取擴展點配置文件中所有的鍵值對
- 根據鍵值對緩存Class對象,如果類有同類型參數的構造函數,則為包裝類,會緩存在另一個容器中
- 實例化對象,緩存後並返回
2.1 擴展點自動包裝
在返回真正實例前,會用該類型的包裝類進行包裝,既採用裝飾器模式進行功能增強。
<code>if
(CollectionUtils.isNotEmpty(wrapperClasses)) {for
(Class> wrapperClass : wrapperClasses) { instance = injectExtension((T) wrapperClass.getConstructor(type
).newInstance(instance)); } }/<code>
其實就是使用靜態代理來實現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獲取某一類的實現列表,既如下方法
<code>public
ListgetActivateExtension
(URL url, String[] values
){ ... }/<code>
3. ExtensionLoader自適應擴展機制
ExtensionLoader自適應擴展機制的大概實現邏輯是這樣的:Dubbo會為拓展接口生成具有代理功能的代碼,然後通過 javassist 或 jdk 編譯這段代碼,得到 Class 類。最後再通過反射創建代理類,在代理類中,就可以通過URL對象的參數來確定到底調用哪個實現類。主要實現在
createAdaptiveExtensionClass方法中。
<code>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); }/<code>
上面的Protocol接口經過處理後的內容如下:
<code>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); } }/<code>
前面提到過,Protocol是一個比較特殊的接口,Adaptive註解裡沒有指定Key,因而沒法通過URL攜帶的參數獲取具體的實現類。從上面處理過後的內容來看,可以看到,對於Protocol接口直接使用的URL的protocol屬性字段的值來進行判斷的。
如下例子是在Adaptive裡有提供key的實現處理過後的內容:
<code>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); }/<code>
可以發現這種情況直接使用的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等。