相信對dubbo有過了解的小伙伴應該知道,dubbo之所以被廣泛的使用,其中最重要的一個原因是因為其優(yōu)秀的可擴展性。而如此良好的擴展性有兩個密不可分的原因,一個是設計模式,另一個就是dubbo自身獨特的擴展點機制-dubbo SPI,本文將主要從以下幾個方面來詳細解讀dubbo SPI的實現(xiàn)機制。
- java spi
- dubbo 中spi優(yōu)化與特性
- 源碼解讀擴展點注解
- 總結(jié)
一、java spi
在講解Dubbo SPI之前,先了解一下Java SPI是怎么使用的。SPI的全稱是Service Provider Interface,起初是提供給廠商做插件開發(fā)的。通俗點解釋其實就是策略模式,定義一個接口,有多個實現(xiàn),只不過接口對應的實現(xiàn)不在代碼中直接聲明,而是通過配置文件來配置這個對應實現(xiàn)的關(guān)系。具體步驟如下:
(1) 定義一個接口及對應的方法。 (2) 編寫該接口的一個實現(xiàn)類。
(3) 在META-INF/services/目錄下,創(chuàng)建一個以接口全路徑命名的文件,如com.test.spi.PrintService
(4) 文件內(nèi)容為具體實現(xiàn)類的全路徑名,如果有多個,則用分行符分隔。
(5) 在代碼中通過java.util.ServiceLoader來加載具體的實現(xiàn)類。
Java SPI示例代碼
public interface Printservice ( <——① SPI 接口定義
void printlnfo();
}
public class PrintServicelmpl implements Printservice { _②SPI接口實現(xiàn)類
Override
public void printlnfo() {
System.out.println("hello world");
}
}
public static void main(String[] args) ( //調(diào)用SPI具體的實現(xiàn)
ServiceLoader<PrintService> serviceServiceLoader =
ServiceLoader.load(PrintService.class);
for (Printservice printservice : serviceServiceLoader) ( <-------------
//此處會輸出:hello world 獲取所有的SPI實現(xiàn),循環(huán)調(diào)用
printService.printInfo();
}
}
dubbo官方文檔中對于java spi的缺點給出了一下兩點
- JDK 標準的 SPI 會一次性實例化擴展點所有實現(xiàn),如果有擴展實現(xiàn)初始化很耗時,但如果沒用上也加載,會很浪費資源。
- 如果擴展點加載失敗,連擴展點的名稱都拿不到了。比如:JDK 標準的 ScriptEngine,通過 getName() 獲取腳本類型的名稱,但如果 RubyScriptEngine 因為所依賴的 jruby.jar 不存在,導致 RubyScriptEngine 類加載失敗,這個失敗原因被吃掉了,和 ruby 對應不起來,當用戶執(zhí)行 ruby 腳本時,會報不支持 ruby,而不是真正失敗的原因。
二、dubbo spi
針對以上兩點,我們來看dubbo做了哪些優(yōu)化:
2.1 按需獲取擴展點實現(xiàn)
- 在上文java spi的演示代碼中我們看到,java.util.ServiceLoader會一次把Printservice接口下的所有實現(xiàn)類全部初始化。用戶直接調(diào)用即可。Dubbo SPI只是加載配置文件中的類, 并分成不同的種類緩存在內(nèi)存中,而不會立即全部初始化,在性能上有更好的表現(xiàn)。具體的實現(xiàn)原理會在后面講解,此處演示一個使用示例。
PrintService接口的Dubbo SPI改造
PrintService接口的Dubbo SPI改造
① 在目錄META-INF/dubbo/internal下建立配置文com.test.spi.Printservice,文件內(nèi)容如下
impl=com.test.spi.PrintServiceImpl
② 為接口類添加SPI注解,設置默認實現(xiàn)為impl
@SPI("impl")
public interface Printservice {
void printlnfo();
)
③實現(xiàn)類不變
public class PrintServicelmpl implements Printservice (
Override
public void printlnfo() (
System.out println("hello world");
}
}
④調(diào)用Dubbo SPI
public static void main(String[] args) (
//通過 ExtensionLoader 獲取接口PrintService.class 的默認實現(xiàn)
PrintService printservice = ExtensionLoader
.getExtensionLoader(PrintService.class).getDefaultExtension();
//此處會輸出 PrintServicelmpl 打印的 hello world
printService.printInfo();
}
我們發(fā)現(xiàn),在dubbo中,如果一個擴展點有多個實現(xiàn),我們可以不畢直接加載所有實現(xiàn),而是根據(jù)自己的需要獲取擴展點實現(xiàn),具體實現(xiàn)原理下文我們通過源碼分析。
2.2、異常處理
Java SPI加載失敗,可能會因為各種原因?qū)е庐惓P畔⒈弧巴痰簟?,導致開發(fā)人員問題追蹤比較困難。Dubbo SPI在擴展加載失敗的時候會先拋出真實異常并打印日志。擴展點在被動加載的時候,即使有部分擴展加載失敗也不會影響其他擴展點和整個框架的使用
2.3、IOC和AOP機制
- 在dubbo中,很多功能都是通過擴展點來實現(xiàn)的,既然如此,一旦擴展點很多的話,擴展點之間的依賴關(guān)系怎么處理也是一個問題,而在dubbo中則是通過自動裝配,即如果實例化一個擴展點實現(xiàn),會繼續(xù)看有沒有依賴此擴展點的擴展點。判斷方法也很簡單,就是通過set方法,即一個擴展點可以通過setter方法直接注入其他擴展點。這個和spring的IOC原理類似,所以我們稱它為dubbo spi中的IOC,也稱為擴展點的自動擴展特性,具體實現(xiàn)邏輯方法為
injectExtension(T instance),后面會詳細解釋。這么說有點抽象,我們來看官網(wǎng)中給出的具體示例:
有兩個為擴展點 CarMaker(造車者)、WheelMaker (造輪者)
public interface CarMaker {
Car makeCar();
}
public interface WheelMaker {
Wheel makeWheel();
}
CarMaker 的一個實現(xiàn)類
public class RaceCarMaker implements CarMaker {
WheelMaker wheelMaker;
public void setWheelMaker(WheelMaker wheelMaker) {
this.wheelMaker = wheelMaker;
}
public Car makeCar() {
// ...
Wheel wheel = wheelMaker.makeWheel();
// ...
return new RaceCar(wheel, ...);
}
}
ExtensionLoader 加載 CarMaker 的擴展點實現(xiàn) RaceCarMaker 時,setWheelMaker 方法的 WheelMaker 也是擴展點則會注入 WheelMaker 的實現(xiàn)。
這里帶來另一個問題,ExtensionLoader 要注入依賴擴展點時,如何決定要注入依賴擴展點的哪個實現(xiàn)。在這個示例中,即是在多個WheelMaker 的實現(xiàn)中要注入哪個。
這個問題在下面一點 [擴展點自適應]特性的時候講解
- 熟悉設計模式的應該知道,裝飾者模式是很常用的一種設計模式。它通常用來在不改變原有對象的行為方法的同時,用來對原有對象進行方法增強。而在dubbo中,實例化一個擴展點實現(xiàn)的同時,也會判斷此對象有沒有作為一個包裝類對象中構(gòu)造方法的對象,如果是,也會實例化該
wrapper包裝類。舉例說明:
private T createExtension(String name){
//這里省略了部分代碼
.......
/**
* 向擴展類注入其依賴的擴展點屬性,這里是體現(xiàn)了擴展點自動裝配的特性
*/
injectExtension(instance);
/**
* 這里的cachedWrapperClasses對象 在執(zhí)行g(shù)etExtensionClasses方法時已經(jīng)賦值
* 擴展點自動包裝特性,ExtensionLoader在加載擴展時,如果發(fā)現(xiàn)這個擴展類包含其他擴展點作為構(gòu)造函數(shù)的參數(shù),
* 則這個擴展類會被認為是wrapper類,比如 ProtocolFilterWrapper,就是在構(gòu)造函數(shù)中注入了 Protocol類型的擴展點
* 那么這個wrapper類也會被實例化并且注入擴展點屬性
*/
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
for (Class<?> wrapperClass : wrapperClasses) {
//wrapper對象的實例化(injectExtension():向擴展類注入其依賴的屬性,如擴展類A又依賴了擴展類B,那么就向A中注入擴展類B)
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
.......
}
在上面這個創(chuàng)建擴展點的方法中,擴展點自動裝配以后,會繼續(xù)對包裝類對象進行注入。這個和aop原理一樣,稱為dubbo spi中的aop,也叫擴展點的自動包裝。
三、源碼解讀擴展點注解
上文大致說明了dubbo spi對于java spi的優(yōu)化點,下面我們針對優(yōu)化點進行一波源碼分析。
3.1、@SPI 注解源碼解讀
首先看下按需獲取獲取擴展點實現(xiàn)。這里涉及到一個注解 @SPI,在上文Dubbo SPI改造示例代碼中也有體現(xiàn),@SPI注解可以使用在類、接口和枚舉類上,Dubbo框架中都是使用在接口上。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {
/**
* default extension name
*/
String value() default "";
}
我們可以看到SPI注解有一個value屬性,通過這個屬性,我們可以傳入不同的參數(shù)來設置這個接口的默認實現(xiàn)類。例如,我們可以看到Transporter接口使用Netty作為默認實現(xiàn)
@SPI(”netty”)
public interface Transporter!
}
下面具體看一下是怎么根據(jù)名稱獲取到具體的實現(xiàn)類的,代碼入口:
org.apache.dubbo.common.extension.ExtensionLoader#getExtension
public T getExtension(String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
if ("true".equals(name)) {
//獲取默認的擴展點實現(xiàn)
return getDefaultExtension();
}
final Holder<Object> holder = getOrCreateHolder(name);
Object instance = holder.get();
//從緩存中獲取,緩存中沒有,則創(chuàng)建,這里使用了雙重檢查鎖
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
//創(chuàng)建擴展點實現(xiàn)類
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
/**
* 1、加載定義文件中的各個子類,然后將目標name對應的子類返回后進行實例化。
* 2、通過目標子類的set方法為其注入其所依賴的bean,這里既可以通過SPI,也可以通過Spring的BeanFactory獲取所依賴的bean,injectExtension(instance)。
* 3、獲取定義文件中定義的wrapper對象,然后使用該wrapper對象封裝目標對象,并且還會調(diào)用其set方法為wrapper對象注入其所依賴的屬性
* @param name
* @return
*/
@SuppressWarnings("unchecked")
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);
}
/**
* 向擴展類注入其依賴的擴展點屬性,這里是體現(xiàn)了擴展點自動裝配的特性
*/
injectExtension(instance);
/**
* 這里的cachedWrapperClasses對象 在執(zhí)行g(shù)etExtensionClasses方法時已經(jīng)賦值
* 擴展點自動包裝特性,ExtensionLoader在加載擴展時,如果發(fā)現(xiàn)這個擴展類包含其他擴展點作為構(gòu)造函數(shù)的參數(shù),
* 則這個擴展類會被認為是wrapper類,比如 ProtocolFilterWrapper,就是在構(gòu)造函數(shù)中注入了 Protocol類型的擴展點
* 那么這個wrapper類也會被實例化并且注入擴展點屬性
*/
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
for (Class<?> wrapperClass : wrapperClasses) {
//wrapper對象的實例化(injectExtension():向擴展類注入其依賴的屬性,如擴展類A又依賴了擴展類B,那么就向A中注入擴展類B)
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}
createExtension是個關(guān)鍵的方法:此方法基本分為三個步驟:
1、加載定義文件中的各個子類,然后將目標name對應的子類返回后進行實例化。
2、通過目標子類的set方法為其注入其所依賴的bean,這里既可以通過SPI,也可以通過Spring的BeanFactory獲取所依賴的bean,injectExtension(instance)。
3、獲取定義文件中定義的wrapper對象,然后使用該wrapper對象封裝目標對象,并且還會調(diào)用其set方法為wrapper對象注入其所依賴的屬性
1、getExtensionClasses 方法比較長,在此不一一列舉,主邏輯就是獲取到擴展點的所有實現(xiàn)類,中間會加入各種緩存提高性能,需要注意的是這里獲取的只是擴展點的實現(xiàn)類,并沒有實例化,這也印證了我們上面所說的按照需要獲取擴展實現(xiàn)類,并且只是加載配置文件中的類, 并分成不同的種類緩存在內(nèi)存中,而不會立即全部初始化,在性能上有更好的表現(xiàn)。
2、injectExtension 是擴展點自動擴展的特性具體實現(xiàn),基本原理:
方法總體實現(xiàn)了類似Spring的IoC機制,其實現(xiàn)原理比較簡單:首先通
過反射獲取類的所有方法,然后遍歷以字符串set開頭的方法,得到set方法的參數(shù)類型,再通過ExtensionFactory尋找參數(shù)類型相同的擴展類實例,如果找到,就設值進去
/**
* 注入擴展類
* 像擴展類中注入其依賴的屬性,如擴展類A又依賴了擴展類B,那么就向A中注入擴展類B
*
*injectExtension方法總體實現(xiàn)了類似Spring的IoC機制,其實現(xiàn)原理比較簡單:首先通
* 過反射獲取類的所有方法,然后遍歷以字符串set開頭的方法,得到set方法的參數(shù)類型,再通
* 過ExtensionFactory尋找參數(shù)類型相同的擴展類實例,如果找到,就設值進去
* @param instance
* @return
*/
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
if (isSetter(method)) {
/**
* Check {@link DisableInject} to see if we need auto injection for this property
*/
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
Class<?> pt = method.getParameterTypes()[0];
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
String property = getSetterProperty(method);
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
//執(zhí)行set方法
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("Failed to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
3.2、擴展點自適應注解:?Adaptive
上文中我們遺留了一個問題,ExtensionLoader 要注入依賴擴展點時,如何決定要注入依賴擴展點的哪個實現(xiàn)。這就需要提到擴展點的自適應注解,?Adaptive。
@Adaptive注解可以標記在類、接口、枚舉類和方法上,但是在整個Dubbo框架中,只有幾個地方使用在類級別上,如AdaptiveExtensionFactory和AdaptiveCompiler,其余都標注在方法上。如果標注在接口的方法上,即方法級別注解,則可以通過參數(shù)動態(tài)獲得實現(xiàn)類,方法級別注解在第一次getExtension時,會自動生成和編譯一個動態(tài)的Adaptive類,從而達到動態(tài)實現(xiàn)類的效果。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
//數(shù)組,可以設置多個key,會按順序依次匹配
String[] value() default {};
}
該注解也可以傳入value參數(shù),是一個數(shù)組。我們在代碼清單4.9中可以看到,Adaptive可以傳入多個key值,在初始化Adaptive注解的接口時,會先對傳入的URL進行key值匹配,第一個key沒匹配上則匹配第二個,以此類推。直到所有的key匹配完畢,如果還沒有匹配到, 則會使用“駝峰規(guī)則”匹配,如果也沒匹配到,則會拋出IllegalStateException異常。 什么是"駝峰規(guī)則”呢?如果包裝類(Wrapper 沒有用Adaptive指定key值,則Dubbo會自動把接口名稱根據(jù)駝峰大小寫分開,并用符號連接起來,以此來作為默認實現(xiàn)類的名稱,如下面示例中的 SimpleExt 會被轉(zhuǎn)化為simple.ext。
對于@Adaptive 注解的解析可以從這個單元測試著手:
org.apache.dubbo.common.extension.ExtensionLoader_Adaptive_Test#test_getAdaptiveExtension_defaultAdaptiveKey
@SPI("impl1")
public interface SimpleExt {
// @Adaptive example, do not specify a explicit key.
@Adaptive
String echo(URL url, String s);
@Adaptive({"key1", "key2"})
String yell(URL url, String s);
// no @Adaptive
String bang(URL url, int i);
}
public class SimpleExtImpl1 implements SimpleExt {
public String echo(URL url, String s) {
return "Ext1Impl1-echo";
}
public String yell(URL url, String s) {
return "Ext1Impl1-yell";
}
public String bang(URL url, int i) {
return "bang1";
}
}
public class SimpleExtImpl2 implements SimpleExt {
public String echo(URL url, String s) {
return "Ext1Impl2-echo";
}
public String yell(URL url, String s) {
return "Ext1Impl2-yell";
}
public String bang(URL url, int i) {
return "bang2";
}
}
public class SimpleExtImpl3 implements SimpleExt {
public String echo(URL url, String s) {
return "Ext1Impl3-echo";
}
public String yell(URL url, String s) {
return "Ext1Impl3-yell";
}
public String bang(URL url, int i) {
return "bang3";
}
}
@Test
public void test_getAdaptiveExtension_defaultAdaptiveKey() throws Exception {
{
SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension();
Map<String, String> map = new HashMap<String, String>();
URL url = new URL("p1", "1.2.3.4", 1010, "path1", map);
String echo = ext.echo(url, "haha");
assertEquals("Ext1Impl1-echo", echo);
}
{
SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension();
Map<String, String> map = new HashMap<String, String>();
map.put("simple.ext", "impl2");
URL url = new URL("p1", "1.2.3.4", 1010, "path1", map);
String echo = ext.echo(url, "haha");
assertEquals("Ext1Impl2-echo", echo);
}
}
public T getAdaptiveExtension() {
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
if (createAdaptiveInstanceError == null) {
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
//創(chuàng)建自適應擴展點實例
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
}
}
}
} else {
throw new IllegalStateException("Failed to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
}
}
return (T) instance;
}
private Class<?> getAdaptiveExtensionClass() {
//加載所有擴展點實現(xiàn)類
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
//此處是關(guān)鍵的一步,生成自適應擴展類
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
createAdaptiveExtensionClass是關(guān)鍵的一步:
為擴展點接口自動生成實現(xiàn)類字符串,實現(xiàn)類主要包含以下邏輯:為接口中每個有Adaptive注解的方法生成默認實現(xiàn)(沒有注解的方法則生成空實現(xiàn)),每個默認實現(xiàn)都會從URL中提取Adaptive參數(shù)值,并以此為依據(jù)動態(tài)加載擴展點。然后,框架會使用不同的編譯器,把實現(xiàn)類字符串編譯為自適應類并返回
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);
}
}
通過debug我們可以看到生成的類字符串到底長什么樣,這里我們貼出來看一下:
* package org.apache.dubbo.common.extension.support;
* package org.apache.dubbo.common.extension.ext1;
* import org.apache.dubbo.common.extension.ExtensionLoader;
* public class SimpleExt$Adaptive implements org.apache.dubbo.common.extension.ext1.SimpleExt {
* public java.lang.String bang(org.apache.dubbo.common.URL arg0, int arg1) {
* throw new UnsupportedOperationException("The method public abstract java.lang.String org.apache.dubbo.common.extension.ext1.SimpleExt.bang(org.apache.dubbo.common.URL,int) of interface org.apache.dubbo.common.extension.ext1.SimpleExt is not adaptive method!");
* }
* public java.lang.String yell(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
* if (arg0 == null) throw new IllegalArgumentException("url == null");
* org.apache.dubbo.common.URL url = arg0;
* String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));
* if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([key1, key2])");
* org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt)ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);
* return extension.yell(arg0, arg1);
* }
* public java.lang.String echo(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
* if (arg0 == null) throw new IllegalArgumentException("url == null");
* org.apache.dubbo.common.URL url = arg0;
* String extName = url.getParameter("simple.ext", "impl1");
* if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([simple.ext])");
* org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt)ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);
* return extension.echo(arg0, arg1);
* }
* }
由上面動態(tài)生成的$Adaptive類可以得知,每個默認實現(xiàn)都會從URL中提取Adaptive參數(shù)值,并以此為依據(jù)動態(tài)加載擴展點,如示例所示,就是根據(jù)url中獲取到extName參數(shù),然后調(diào)用 getExtension(extName)。
如果一個接口上既有@SPI(”impl2”)注解,方法上又有@Adaptive(”impl1”)注解,那么會以哪個key作為默認實現(xiàn)呢?由上面動態(tài)生成的Adaptive類可以得知,最終動態(tài)生成的實現(xiàn)方法會是url.getParameter(simple.ext, "impl”),即優(yōu)先通過?Adaptive注解傳入的key去查找擴展實現(xiàn)類;如果沒找到,則通過@SPI注解中的key去查找;如果@SPI注解中沒有默認值,則把類名轉(zhuǎn)化為key,再去查找。
除了上面的這個示例,還有一個示例可以參考,我們看dubbo中的傳輸協(xié)議,接口默認為dubbo的傳輸協(xié)議,在服務暴露和引用的方法中,加了@Adaptive注解。
@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;
}
private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
以上獲取 protocol的代碼,會生成一個中間代理類如下:
package org.apache.dubbo.rpc;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
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 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.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);
}
}
和上述例子相同,在動態(tài)獲取協(xié)議的過程中,也是通過url中的參數(shù)來動態(tài)獲取,如果url中protocol參數(shù)為空,則默認使用dubbo協(xié)議,如果不為空,比如服務暴露的過程中,先本地暴露,那么url中protocol = injvm,那么獲取到的協(xié)議就是InjvmProtocol。
3.3、擴展點自動激活注解:?Active
上面我們提到自適應的擴展實現(xiàn)機制動態(tài)尋找實現(xiàn)類的方式比較靈活,但只能激活一個具體的實現(xiàn)類,如果需要多個實現(xiàn)類同時被激活,如Filter可以同時有多個過濾器;或者根據(jù)不同的條件,同時激活多個實現(xiàn)類, 如何實現(xiàn)?這就涉及最后一個特性一一自動激活
使用@Activate注解,可以標記對應的擴展點默認被激活啟用。該注解還可以通過傳入不同的參數(shù),設置擴展點在不同的條件下被自動激活。主要的使用場景是某個擴展點的多個實現(xiàn)類需要同時啟用(比如Filter擴展點)
@Active 可以傳入的參數(shù)很多

org.apache.dubbo.common.extension.ExtensionLoaderTest#testLoadActivateExtension 測試用來來進行解析。
public List<T> getActivateExtension(URL url, String[] values, String group) {
List<T> exts = new ArrayList<>();
List<String> names = values == null ? new ArrayList<>(0) : Arrays.asList(values);
if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
//獲取擴展點所有實現(xiàn)類
getExtensionClasses();
//遍歷整個@Acitve注解集合
for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Object activate = entry.getValue();
String[] activateGroup, activateValue;
if (activate instanceof Activate) {
activateGroup = ((Activate) activate).group();
activateValue = ((Activate) activate).value();
} else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
} else {
continue;
}
//group條件匹配
if (isMatchGroup(group, activateGroup)) {
T ext = getExtension(name);
if (!names.contains(name)
&& !names.contains(REMOVE_VALUE_PREFIX + name)
&& isActive(activateValue, url)) {
exts.add(ext);
}
}
}
//排序
exts.sort(ActivateComparator.COMPARATOR);
}
List<T> usrs = new ArrayList<>();
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
if (!name.startsWith(REMOVE_VALUE_PREFIX)
&& !names.contains(REMOVE_VALUE_PREFIX + name)) {
if (DEFAULT_KEY.equals(name)) {
if (!usrs.isEmpty()) {
exts.addAll(0, usrs);
usrs.clear();
}
} else {
T ext = getExtension(name);
usrs.add(ext);
}
}
}
if (!usrs.isEmpty()) {
exts.addAll(usrs);
}
return exts;
}
該方法主線流程分為4步:
(1) 依舊是獲取擴展點的所有實現(xiàn)
(2) 遍歷整個?Activate注解集合,根據(jù)傳入URL匹配條件(匹配group> name等),得
到所有符合激活條件的擴展類實現(xiàn)。然后根據(jù)@Active中配置的before、after、order等參數(shù)進行排序
(3) 遍歷所有用戶自定義擴展類名稱,根據(jù)用戶URL配置的順序,調(diào)整擴展點激活順序
(遵循用戶在 URL 中配置的順序,例如 URL 為 test ://localhost/test?ext=orderlJdefault,則擴展點ext的激活順序會遵循先order再default,其中default代表所有有@Activate注解的擴展點)。
(4) 返回所有自動激活類集合。
四、總結(jié)
好了,擼了這么久源碼,總結(jié)一波:
dubbo Spi為了優(yōu)化java spi的缺點,引入了可以按照需要獲取擴展點的實現(xiàn)類。其中
@SPI注解通常用在接口上,指定擴展點的實現(xiàn)類。并且在加載擴展點的過程中,引入了自動擴展和自動封裝擴展點的特性,在自動擴展的過程過程中,因為需要確定自動擴展的是具體哪一個實現(xiàn)類,
又引入了@Adaptive注解,該注解大部分用在方法級別,通過生成動態(tài)的$Adaptive類來解析參數(shù),并通過參數(shù)動態(tài)獲取具體的實現(xiàn)類。但是其自適應擴展點實現(xiàn)只能一個,
又引入了@Active注解,此注解可以通過傳入不同的參數(shù),設置擴展點在不同的條件下多個實現(xiàn)類被自動激活,主要的使用場景是某個擴展點的多個實現(xiàn)類需要同時啟用(比如Filter擴展點)。
備注:文中示例代碼版本:2.7.3
參考文獻:
1、dubbo官網(wǎng)
2、書籍:深入理解Apache Dubbo與實戰(zhàn)