dubbo系列-擴展點機制-Dubbo SPI

相信對dubbo有過了解的小伙伴應該知道,dubbo之所以被廣泛的使用义图,其中最重要的一個原因是因為其優(yōu)秀的可擴展性。而如此良好的擴展性有兩個密不可分的原因电抚,一個是設計模式,另一個就是dubbo自身獨特的擴展點機制-dubbo SPI街望,本文將主要從以下幾個方面來詳細解讀dubbo SPI的實現機制。

  • java spi
  • dubbo 中spi優(yōu)化與特性
  • 源碼解讀擴展點注解
  • 總結

一、java spi

在講解Dubbo SPI之前赫舒,先了解一下Java SPI是怎么使用的有咨。SPI的全稱是Service Provider Interface,起初是提供給廠商做插件開發(fā)的滤祖。通俗點解釋其實就是策略模式挤牛,定義一個接口,有多個實現,只不過接口對應的實現不在代碼中直接聲明喜德,而是通過配置文件來配置這個對應實現的關系观蓄。具體步驟如下:

(1) 定義一個接口及對應的方法。 (2) 編寫該接口的一個實現類辑舷。
(3) 在META-INF/services/目錄下,創(chuàng)建一個以接口全路徑命名的文件玖绿,如com.test.spi.PrintService
(4) 文件內容為具體實現類的全路徑名寂嘉,如果有多個父叙,則用分行符分隔躬贡。
(5) 在代碼中通過java.util.ServiceLoader來加載具體的實現類。

Java SPI示例代碼

public interface Printservice ( <——① SPI 接口定義
       void printlnfo();
}

public class PrintServicelmpl implements Printservice { _②SPI接口實現類
       Override
       public void printlnfo() {
           System.out.println("hello world");
       }
 }

public static void main(String[] args) (  //調用SPI具體的實現
   ServiceLoader<PrintService> serviceServiceLoader =
   ServiceLoader.load(PrintService.class);
   for (Printservice printservice : serviceServiceLoader) ( <-------------
       //此處會輸出:hello world 獲取所有的SPI實現,循環(huán)調用
       printService.printInfo(); 
   } 
}

dubbo官方文檔中對于java spi的缺點給出了一下兩點

  • JDK 標準的 SPI 會一次性實例化擴展點所有實現,如果有擴展實現初始化很耗時潦匈,但如果沒用上也加載,會很浪費資源卧斟。
  • 如果擴展點加載失敗留特,連擴展點的名稱都拿不到了菱鸥。比如:JDK 標準的 ScriptEngine艾凯,通過 getName() 獲取腳本類型的名稱,但如果 RubyScriptEngine 因為所依賴的 jruby.jar 不存在贝乎,導致 RubyScriptEngine 類加載失敗情连,這個失敗原因被吃掉了,和 ruby 對應不起來览效,當用戶執(zhí)行 ruby 腳本時却舀,會報不支持 ruby,而不是真正失敗的原因朽肥。

二禁筏、dubbo spi

針對以上兩點,我們來看dubbo做了哪些優(yōu)化:

2.1 按需獲取擴展點實現

  • 在上文java spi的演示代碼中我們看到衡招,java.util.ServiceLoader會一次把Printservice接口下的所有實現類全部初始化。用戶直接調用即可每强。Dubbo SPI只是加載配置文件中的類始腾, 并分成不同的種類緩存在內存中,而不會立即全部初始化空执,在性能上有更好的表現浪箭。具體的實現原理會在后面講解,此處演示一個使用示例辨绊。

PrintService接口的Dubbo SPI改造

PrintService接口的Dubbo SPI改造
① 在目錄META-INF/dubbo/internal下建立配置文com.test.spi.Printservice,文件內容如下
impl=com.test.spi.PrintServiceImpl 

② 為接口類添加SPI注解奶栖,設置默認實現為impl
@SPI("impl")   
public interface Printservice {
    void printlnfo();
)

③實現類不變
public class PrintServicelmpl implements Printservice ( 
Override
public void printlnfo() (
      System.out println("hello world");
} 
}

④調用Dubbo SPI
public static void main(String[] args) ( 
    //通過 ExtensionLoader 獲取接口PrintService.class 的默認實現
    PrintService printservice = ExtensionLoader
    .getExtensionLoader(PrintService.class).getDefaultExtension();
    //此處會輸出 PrintServicelmpl 打印的 hello world
    printService.printInfo();
}

我們發(fā)現,在dubbo中门坷,如果一個擴展點有多個實現宣鄙,我們可以不畢直接加載所有實現,而是根據自己的需要獲取擴展點實現默蚌,具體實現原理下文我們通過源碼分析冻晤。

2.2、異常處理

Java SPI加載失敗绸吸,可能會因為各種原因導致異常信息被“吞掉”鼻弧,導致開發(fā)人員問題追蹤比較困難设江。Dubbo SPI在擴展加載失敗的時候會先拋出真實異常并打印日志。擴展點在被動加載的時候攘轩,即使有部分擴展加載失敗也不會影響其他擴展點和整個框架的使用

2.3叉存、IOC和AOP機制

  • 在dubbo中,很多功能都是通過擴展點來實現的度帮,既然如此歼捏,一旦擴展點很多的話,擴展點之間的依賴關系怎么處理也是一個問題够傍,而在dubbo中則是通過自動裝配甫菠,即如果實例化一個擴展點實現,會繼續(xù)看有沒有依賴此擴展點的擴展點冕屯。判斷方法也很簡單寂诱,就是通過set方法,即一個擴展點可以通過setter方法直接注入其他擴展點安聘。這個和spring的IOC原理類似痰洒,所以我們稱它為dubbo spi中的IOC,也稱為擴展點的自動擴展特性浴韭,具體實現邏輯方法為 injectExtension(T instance)丘喻,后面會詳細解釋。這么說有點抽象念颈,我們來看官網中給出的具體示例:
    有兩個為擴展點 CarMaker(造車者)泉粉、WheelMaker (造輪者)
public interface CarMaker {
    Car makeCar();
}
 
public interface WheelMaker {
    Wheel makeWheel();
}

CarMaker 的一個實現類

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 的擴展點實現 RaceCarMaker 時,setWheelMaker 方法的 WheelMaker 也是擴展點則會注入 WheelMaker 的實現榴芳。
這里帶來另一個問題嗡靡,ExtensionLoader 要注入依賴擴展點時,如何決定要注入依賴擴展點的哪個實現窟感。在這個示例中讨彼,即是在多個WheelMaker 的實現中要注入哪個。
這個問題在下面一點 [擴展點自適應]特性的時候講解

  • 熟悉設計模式的應該知道柿祈,裝飾者模式是很常用的一種設計模式哈误。它通常用來在不改變原有對象的行為方法的同時,用來對原有對象進行方法增強躏嚎。而在dubbo中蜜自,實例化一個擴展點實現的同時,也會判斷此對象有沒有作為一個包裝類對象中構造方法的對象紧索,如果是袁辈,也會實例化該 wrapper包裝類。舉例說明:
private T createExtension(String name){
             //這里省略了部分代碼
             .......
   /**
             * 向擴展類注入其依賴的擴展點屬性珠漂,這里是體現了擴展點自動裝配的特性
             */
            injectExtension(instance);
            /**
             * 這里的cachedWrapperClasses對象 在執(zhí)行getExtensionClasses方法時已經賦值
             * 擴展點自動包裝特性晚缩,ExtensionLoader在加載擴展時尾膊,如果發(fā)現這個擴展類包含其他擴展點作為構造函數的參數,
             * 則這個擴展類會被認為是wrapper類荞彼,比如 ProtocolFilterWrapper,就是在構造函數中注入了 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 注解源碼解讀

首先看下按需獲取獲取擴展點實現被啼。這里涉及到一個注解 @SPI,在上文Dubbo SPI改造示例代碼中也有體現帜消,@SPI注解可以使用在類、接口和枚舉類上浓体,Dubbo框架中都是使用在接口上泡挺。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {

    /**
     * default extension name
     */
    String value() default "";

}

我們可以看到SPI注解有一個value屬性,通過這個屬性命浴,我們可以傳入不同的參數來設置這個接口的默認實現類娄猫。例如,我們可以看到Transporter接口使用Netty作為默認實現

@SPI(”netty”)
public interface Transporter!
}

下面具體看一下是怎么根據名稱獲取到具體的實現類的生闲,代碼入口:
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)) {
            //獲取默認的擴展點實現
            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)建擴展點實現類
                    instance = createExtension(name);
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }
/**
 * 1闸婴、加載定義文件中的各個子類,然后將目標name對應的子類返回后進行實例化冲茸。
 * 2、通過目標子類的set方法為其注入其所依賴的bean缅帘,這里既可以通過SPI轴术,也可以通過Spring的BeanFactory獲取所依賴的bean,injectExtension(instance)钦无。
 * 3逗栽、獲取定義文件中定義的wrapper對象,然后使用該wrapper對象封裝目標對象失暂,并且還會調用其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);
        }
        /**
         * 向擴展類注入其依賴的擴展點屬性彼宠,這里是體現了擴展點自動裝配的特性
         */
        injectExtension(instance);
        /**
         * 這里的cachedWrapperClasses對象 在執(zhí)行getExtensionClasses方法時已經賦值
         * 擴展點自動包裝特性鳄虱,ExtensionLoader在加載擴展時,如果發(fā)現這個擴展類包含其他擴展點作為構造函數的參數凭峡,
         * 則這個擴展類會被認為是wrapper類拙已,比如 ProtocolFilterWrapper,就是在構造函數中注入了 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是個關鍵的方法:此方法基本分為三個步驟:

1摧冀、加載定義文件中的各個子類倍踪,然后將目標name對應的子類返回后進行實例化。
2索昂、通過目標子類的set方法為其注入其所依賴的bean建车,這里既可以通過SPI,也可以通過Spring的BeanFactory獲取所依賴的bean椒惨,injectExtension(instance)缤至。
3、獲取定義文件中定義的wrapper對象框产,然后使用該wrapper對象封裝目標對象凄杯,并且還會調用其set方法為wrapper對象注入其所依賴的屬性

1、getExtensionClasses 方法比較長秉宿,在此不一一列舉戒突,主邏輯就是獲取到擴展點的所有實現類,中間會加入各種緩存提高性能描睦,需要注意的是這里獲取的只是擴展點的實現類膊存,并沒有實例化,這也印證了我們上面所說的按照需要獲取擴展實現類忱叭,并且只是加載配置文件中的類隔崎, 并分成不同的種類緩存在內存中,而不會立即全部初始化韵丑,在性能上有更好的表現爵卒。

2、injectExtension 是擴展點自動擴展的特性具體實現撵彻,基本原理:

方法總體實現了類似Spring的IoC機制钓株,其實現原理比較簡單:首先通
過反射獲取類的所有方法,然后遍歷以字符串set開頭的方法陌僵,得到set方法的參數類型轴合,再通過ExtensionFactory尋找參數類型相同的擴展類實例,如果找到碗短,就設值進去

/**
 * 注入擴展類
 * 像擴展類中注入其依賴的屬性受葛,如擴展類A又依賴了擴展類B,那么就向A中注入擴展類B
 *
 *injectExtension方法總體實現了類似Spring的IoC機制,其實現原理比較簡單:首先通
 * 過反射獲取類的所有方法总滩,然后遍歷以字符串set開頭的方法纲堵,得到set方法的參數類型,再通
 * 過ExtensionFactory尋找參數類型相同的擴展類實例咳秉,如果找到婉支,就設值進去
 * @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 要注入依賴擴展點時向挖,如何決定要注入依賴擴展點的哪個實現。這就需要提到擴展點的自適應注解炕舵,?Adaptive何之。

@Adaptive注解可以標記在類、接口咽筋、枚舉類和方法上溶推,但是在整個Dubbo框架中,只有幾個地方使用在類級別上奸攻,如AdaptiveExtensionFactory和AdaptiveCompiler,其余都標注在方法上蒜危。如果標注在接口的方法上,即方法級別注解睹耐,則可以通過參數動態(tài)獲得實現類辐赞,方法級別注解在第一次getExtension時,會自動生成和編譯一個動態(tài)的Adaptive類硝训,從而達到動態(tài)實現類的效果响委。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
    //數組,可以設置多個key窖梁,會按順序依次匹配
    String[] value() default {};
}

該注解也可以傳入value參數赘风,是一個數組。我們在代碼清單4.9中可以看到纵刘,Adaptive可以傳入多個key值邀窃,在初始化Adaptive注解的接口時,會先對傳入的URL進行key值匹配假哎,第一個key沒匹配上則匹配第二個蛔翅,以此類推。直到所有的key匹配完畢位谋,如果還沒有匹配到, 則會使用“駝峰規(guī)則”匹配堰燎,如果也沒匹配到掏父,則會拋出IllegalStateException異常。 什么是"駝峰規(guī)則”呢秆剪?如果包裝類(Wrapper 沒有用Adaptive指定key值赊淑,則Dubbo會自動把接口名稱根據駝峰大小寫分開爵政,并用符號連接起來,以此來作為默認實現類的名稱陶缺,如下面示例中的 SimpleExt 會被轉化為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() {
        //加載所有擴展點實現類
        getExtensionClasses();
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        //此處是關鍵的一步,生成自適應擴展類
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

createAdaptiveExtensionClass是關鍵的一步:

為擴展點接口自動生成實現類字符串饱岸,實現類主要包含以下邏輯:為接口中每個有Adaptive注解的方法生成默認實現(沒有注解的方法則生成空實現)掺出,每個默認實現都會從URL中提取Adaptive參數值,并以此為依據動態(tài)加載擴展點苫费。然后汤锨,框架會使用不同的編譯器,把實現類字符串編譯為自適應類并返回

    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類可以得知闲礼,每個默認實現都會從URL中提取Adaptive參數值,并以此為依據動態(tài)加載擴展點铐维,如示例所示柬泽,就是根據url中獲取到extName參數,然后調用 getExtension(extName)嫁蛇。
如果一個接口上既有@SPI(”impl2”)注解锨并,方法上又有@Adaptive(”impl1”)注解,那么會以哪個key作為默認實現呢棠众?由上面動態(tài)生成的Adaptive類可以得知琳疏,最終動態(tài)生成的實現方法會是url.getParameter(simple.ext, "impl”),即優(yōu)先通過?Adaptive注解傳入的key去查找擴展實現類;如果沒找到闸拿,則通過@SPI注解中的key去查找空盼;如果@SPI注解中沒有默認值,則把類名轉化為key,再去查找新荤。

除了上面的這個示例揽趾,還有一個示例可以參考,我們看dubbo中的傳輸協議苛骨,接口默認為dubbo的傳輸協議篱瞎,在服務暴露和引用的方法中,加了@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)獲取協議的過程中严衬,也是通過url中的參數來動態(tài)獲取澄者,如果url中protocol參數為空,則默認使用dubbo協議,如果不為空粱挡,比如服務暴露的過程中赠幕,先本地暴露,那么url中protocol = injvm询筏,那么獲取到的協議就是InjvmProtocol榕堰。

3.3、擴展點自動激活注解:?Active

上面我們提到自適應的擴展實現機制動態(tài)尋找實現類的方式比較靈活嫌套,但只能激活一個具體的實現類逆屡,如果需要多個實現類同時被激活,如Filter可以同時有多個過濾器;或者根據不同的條件灌危,同時激活多個實現類康二, 如何實現?這就涉及最后一個特性一一自動激活

使用@Activate注解勇蝙,可以標記對應的擴展點默認被激活啟用沫勿。該注解還可以通過傳入不同的參數,設置擴展點在不同的條件下被自動激活味混。主要的使用場景是某個擴展點的多個實現類需要同時啟用(比如Filter擴展點)

@Active 可以傳入的參數很多

active參數
對于@Active注解的解析可以從這個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)) {
        //獲取擴展點所有實現類
        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) 依舊是獲取擴展點的所有實現
(2) 遍歷整個?Activate注解集合,根據傳入URL匹配條件(匹配group> name等)翁锡,得
到所有符合激活條件的擴展類實現蔓挖。然后根據@Active中配置的before、after馆衔、order等參數進行排序
(3) 遍歷所有用戶自定義擴展類名稱瘟判,根據用戶URL配置的順序,調整擴展點激活順序
(遵循用戶在 URL 中配置的順序角溃,例如 URL 為 test ://localhost/test?ext=orderlJdefault,則擴展點ext的激活順序會遵循先order再default,其中default代表所有有@Activate注解的擴展點)拷获。
(4) 返回所有自動激活類集合。

四减细、總結

好了匆瓜,擼了這么久源碼,總結一波:

dubbo Spi為了優(yōu)化java spi的缺點未蝌,引入了可以按照需要獲取擴展點的實現類驮吱。其中@SPI注解通常用在接口上,指定擴展點的實現類萧吠。并且在加載擴展點的過程中左冬,引入了自動擴展和自動封裝擴展點的特性,在自動擴展的過程過程中纸型,因為需要確定自動擴展的是具體哪一個實現類又碌,
又引入了@Adaptive注解九昧,該注解大部分用在方法級別,通過生成動態(tài)的$Adaptive類來解析參數毕匀,并通過參數動態(tài)獲取具體的實現類。但是其自適應擴展點實現只能一個癌别,
又引入了@Active注解皂岔,此注解可以通過傳入不同的參數,設置擴展點在不同的條件下多個實現類被自動激活展姐,主要的使用場景是某個擴展點的多個實現類需要同時啟用(比如Filter擴展點)
躁垛。

備注:文中示例代碼版本:2.7.3
參考文獻:
1、dubbo官網
2圾笨、書籍:深入理解Apache Dubbo與實戰(zhàn)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末教馆,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子擂达,更是在濱河造成了極大的恐慌土铺,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件板鬓,死亡現場離奇詭異悲敷,居然都是意外死亡,警方通過查閱死者的電腦和手機俭令,發(fā)現死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進店門后德,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人抄腔,你說我怎么就攤上這事瓢湃。” “怎么了赫蛇?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵绵患,是天一觀的道長。 經常有香客問我棍掐,道長藏雏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任作煌,我火速辦了婚禮掘殴,結果婚禮上,老公的妹妹穿的比我還像新娘粟誓。我一直安慰自己奏寨,他們只是感情好,可當我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布鹰服。 她就那樣靜靜地躺著病瞳,像睡著了一般揽咕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上套菜,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天亲善,我揣著相機與錄音,去河邊找鬼逗柴。 笑死蛹头,一個胖子當著我的面吹牛,可吹牛的內容都是我干的戏溺。 我是一名探鬼主播渣蜗,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼旷祸!你這毒婦竟也來了耕拷?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤托享,失蹤者是張志新(化名)和其女友劉穎骚烧,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體嫌吠,經...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡止潘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了辫诅。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凭戴。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖炕矮,靈堂內的尸體忽然破棺而出么夫,到底是詐尸還是另有隱情,我是刑警寧澤肤视,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布档痪,位于F島的核電站,受9級特大地震影響邢滑,放射性物質發(fā)生泄漏腐螟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一困后、第九天 我趴在偏房一處隱蔽的房頂上張望乐纸。 院中可真熱鬧,春花似錦摇予、人聲如沸汽绢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宁昭。三九已至跌宛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間积仗,已是汗流浹背疆拘。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留寂曹,地道東北人入问。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像稀颁,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子楣黍,可洞房花燭夜當晚...
    茶點故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內容