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

相信對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ù)很多

active參數(shù)
對于@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)) {
        //獲取擴展點所有實現(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)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容