Dubbo自適應擴展機制

上一節提到過,Dubbo裡除了Service和Config層為API,其它各層均為SPI。相比於Java中的SPI僅僅通過接口類名獲取所有實現,Dubbo的實現可以通過接口類名和key值來獲取一個具體的實現。通過SPI機制,Dubbo實現了面向插件編程,只定義了模塊的接口,實現由各插件來完成。

1. 使用方式

1.1 Java SPI

在擴展類的jar包內,放置擴展點配置文件META-INF/service/接口全限定名,內容為:擴展實現類全限定名,多個實現類用換行符分隔。

如下為Mysql中Driver接口的實現:

Dubbo自適應擴展機制

<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的默認實現:

Dubbo自適應擴展機制

注意: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類中,關於這個類源碼的講解可以看官方文檔,講解的很詳細,這邊主要說下大概過程:

  1. 根據傳入的類型從classpath中查找META-INF/dubbo/internal和META-INF/dubbo路徑下所有對應的擴展點配置文件
  2. 讀取擴展點配置文件中所有的鍵值對
  3. 根據鍵值對緩存Class對象,如果類有同類型參數的構造函數,則為包裝類,會緩存在另一個容器中
  4. 實例化對象,緩存後並返回

2.1 擴展點自動包裝

在返回真正實例前,會用該類型的包裝類進行包裝,既採用裝飾器模式進行功能增強。

<code>

if

(CollectionUtils.isNotEmpty(wrapperClasses)) {

for

(Class> wrapperClass : wrapperClasses) { instance = injectExtension((T) wrapperClass.getConstructor(

type

).newInstance(instance)); } }/<code>

其實就是使用靜態代理來實現AOP

Dubbo自適應擴展機制

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

List

getActivateExtension

(

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的對應值,獲取過程為:

  1. 如果Adaptive註解配置的key為空,則使用類名作為擴展名
  2. 如果擴展名為protocol,則從URL的protocol裡獲取對應的值
  3. 如果擴展名不為protocol,且方法參數裡有org.apache.dubbo.rpc.Invocation,則從URL.getMethodParameter裡獲取
  4. 以上都沒有則直接從URL.getParameter中獲取。

Protocol接口由於沒有在Adaptive註解裡指定key,則會使用類名protocol作為默認的擴展名,從而命中第2條規則。

通過自適應擴展機制,Dubbo框架的各層的核心實現都是基於接口的,而將具體的實現下放到插件中。根據加載的插件不同,各層能夠選擇適合的實現而不會影響核心的邏輯流程。如Protocol接口,表示通信協議,可選的實現包括dubbo、rmi、http等。

Dubbo自適應擴展機制


分享到:


相關文章: