1. 程式人生 > >Dubbo擴充套件機制:ExtensionLoader

Dubbo擴充套件機制:ExtensionLoader

一、前言

     Dubbo的ExtensionLoader是實現“微核心+外掛式”的重要元件,它基於java spi機制卻又提供瞭如下擴充套件:

  • jdk spi僅僅通過介面類名獲取所有實現,而ExtensionLoader則通過介面類名和key值獲取一個實現
  • Adaptive實現,就是生成一個代理類,這樣就可以根據實際呼叫時的一些引數動態決定要呼叫的類了
  • 自動包裝實現,這種實現的類一般是自動啟用的,常用於包裝類,比如Protocol的兩個實現類:ProtocolFilterWrapper、ProtocolListenerWrapper

     jdk spi具有以下缺點:

  • 雖然ServiceLoader也算是使用的延遲載入,但是基本只能通過遍歷全部獲取,也就是介面的實現類全部載入並例項化一遍。如果你並不想用某些實現類,它也被載入並例項化了,這就造成了浪費。
  • 獲取某個實現類的方式不夠靈活,只能通過Iterator形式獲取,不能根據某個引數來獲取對應的實現類

      Dubbo框架是以URL為匯流排的模式,即執行過程中所有的狀態資料資訊都可以通過URL來獲取,比如當前系統採用什麼序列化,採用什麼通訊,採用什麼負載均衡等資訊,都是通過URL的引數來呈現的,所以在框架執行過程中,執行到某個階段需要相應的資料,都可以通過對應的Key從URL的引數列表中獲取,比如在cluster模組,到服務呼叫觸發到該模組,則會從URL中獲取當前呼叫服務的負載均衡策略,以及mock資訊等。

     ExtensionLoader本身是一個單例工廠類,它對外暴露getExtensionLoader靜態方法返回一個ExtensionLoader實體,這個方法的入參是一個Class型別,這個方法的意思是返回某個介面的ExtensionLoader。那麼對於某一個介面,只會有一個ExtensionLoader實體。ExtensionLoader中主要提供了以下方法實現擴充套件點的操作:

static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type);
String getExtensionName(T extensionInstance);
String getExtensionName(Class<?> extensionClass);
List<T> getActivateExtension(URL url, String key);
List<T> getActivateExtension(URL url, String[] values)
; List<T> getActivateExtension(URL url, String key, String group); List<T> getActivateExtension(URL url, String[] values, String group); boolean isMatchGroup(String group, String[] groups); boolean isActive(Activate activate, URL url) ; T getLoadedExtension(String name); Set<String> getLoadedExtensions(); T getExtension(String name) ; T getDefaultExtension(); boolean hasExtension(String name); Set<String> getSupportedExtensions(); String getDefaultExtensionName(); void addExtension(String name, Class<?> clazz) ; T getAdaptiveExtension() ;

二、擴充套件點註解

1、SPI

     Dubbo的SPI規範除了制定了在指定資料夾下面描述服務的實現資訊之外,作為擴充套件點的介面必須標註SPI註解,用來告訴Dubbo這個介面是通過SPI來進行擴充套件實現的,否則ExtensionLoader則不會對這個介面建立ExtensionLoader實體,並且呼叫ExtensionLoader.getExtensionLoader方法會出現IllegalArgumentException異常。在介面上標註SPI註解的時候可以配置一個value屬性用來描述這個介面的預設實現別名,例如Transporter的@SPI(“netty”)就是指定Transporter預設實現是NettyTransporter,因為NettyTransporter的別名是netty。這裡再對服務別名補充有點,別名是站在某一個介面的維度來區分不同實現的,所以一個介面的實現不能有相同的別名,否則Dubbo框架將啟動失敗,當然不同介面的各自實現別名可以相同。到此ExtensionLoader的實現原則和基本原理介紹完了,接下來我們來看看怎麼基於Dubbo的ExtensionLoader來實施我們自己的外掛化。同樣還是dubbo-demo專案中進行演示,在其中建立了一個demo-extension模組。     

      Dubbo中有很多的擴充套件點,這些擴充套件點介面有以下這些:

CacheFactory
Compiler
ExtensionFactory
LoggerAdapter
Serialization
StatusChecker
DataStore
ThreadPool
Container
PageHandler
MonitorFactory
RegistryFactory
ChannelHandler
Codec
Codec2
Dispatcher
Transporter
Exchanger
HttpBinder
Networker
TelnetHandler
ZookeeperTransporter
ExporterListener
Filter
InvokerListener
Protocol
ProxyFactory
Cluster
ConfiguratorFactory
LoadBalance
Merger
RouterFactory
RuleConverter
ClassNameGenerator
Validation

     看一下SPI介面的定義:

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

    /**
     * 預設擴充套件點名,預設為空
     */
	String value() default "";

}

      比如Transporter擴充套件點:

@SPI("netty")
public interface Transporter {

    /**
     * Bind a server.
     * 
     * @see com.alibaba.dubbo.remoting.Transporters#bind(URL, Receiver, ChannelHandler)
     * @param url server url
     * @param handler
     * @return server
     * @throws RemotingException
     *
     * 作為服務端,注意bind之後返回的是一個Server,而引數是要監聽的url和其對應的dubbo handler
     */
    @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
    Server bind(URL url, ChannelHandler handler) throws RemotingException;

    /**
     * Connect to a server.
     * 
     * @see com.alibaba.dubbo.remoting.Transporters#connect(URL, Receiver, ChannelListener)
     * @param url server url
     * @param handler
     * @return client
     * @throws RemotingException
     *
     * 作為客戶端,注意connect之後返回的是一個Client,其引數為要連線的url和對應的dubbo hanlder
     */
    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    Client connect(URL url, ChannelHandler handler) throws RemotingException;

}

2、Adaptive

    上面看到了一個新的註解:Adaptive,它表示適配的意思,ExtensionLoader通過分析介面配置的adaptive規則動態生成adaptive類並且載入到ClassLoader中,來實現動態適配。配置adaptive的規則也是通過Adaptive註解來設定,該註解有一個value屬性,通過設定這個屬性便可以設定該介面的Adaptive的規則,上面說過服務呼叫的所有資料均可以從URL獲取(Dubbo的URL匯流排模式),那麼需要Dubbo幫我們動態生成adaptive的擴充套件介面的方法入參必須包含URL,這樣才能根據執行狀態動態選擇具體實現。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
    
    /**
     * 作為URL的Key名,對應的Value作為要Adapt成的Extension名。
     * 如果URL中這些Key都沒有Value,就使用預設的擴充套件(在介面的SPI註解中設定的值)。
     * 比如,String[] {"key1", "key2"},表示
     * 先在URL上找key1的Value作為要Adapt成的Extension名;
     * key1沒有Value,則使用key2的Value作為要Adapt成的Extension名。
     * key2沒有Value,使用預設的擴充套件(SPI中指定)。
     * 如果沒有設定預設擴充套件,則方法呼叫會丟擲IllegalStateException。
     * 如果不設定則預設使用Extension介面類名的點分隔小寫字串。
     * 即對於Extension介面com.alibaba.dubbo.xxx.YyyInvokerWrapper的預設值為
     * String[] {"yyy.invoker.wrapper"}
     * 
     * @see SPI#value()
     */
    String[] value() default {};
    
}

     這裡仍以上面的Transporter介面中配置的adaptive規則為例,Transporter介面提供了兩個方法,一個是connect(用來建立客戶端連線),另一個是bind(用來繫結服務端埠提供服務),並且這兩個方法上面均通過Adaptive註解配置了value屬性,bind配置的是server和transporter,connect配置的是client和transporter。那麼配置這些值有什麼用呢?下面看看ExtensionLoader根據這些生成了什麼樣的adaptive程式碼。

public class Transporter$Adpative implements com.alibaba.dubbo.remoting.Transporter {
        public com.alibaba.dubbo.remoting.Client connect(com.alibaba.dubbo.common.URL arg0, com.alibaba.dubbo.remoting.ChannelHandler arg1) throws com.alibaba.dubbo.remoting.RemotingException {
            if (arg0 == null) throw new IllegalArgumentException("url == null");
            com.alibaba.dubbo.common.URL url = arg0;
            String extName = url.getParameter("client", url.getParameter("transporter", "netty"));
            if (extName == null)
                throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.remoting.Transporter) name from url(" + url.toString() + ") use keys([client, transporter])");
            com.alibaba.dubbo.remoting.Transporter extension = (com.alibaba.dubbo.remoting.Transporter) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.remoting.Transporter.class).getExtension(extName);
            return extension.connect(arg0, arg1);
        }

        public com.alibaba.dubbo.remoting.Server bind(com.alibaba.dubbo.common.URL arg0, com.alibaba.dubbo.remoting.ChannelHandler arg1) throws com.alibaba.dubbo.remoting.RemotingException {
            if (arg0 == null) throw new IllegalArgumentException("url == null");
            com.alibaba.dubbo.common.URL url = arg0;
            String extName = url.getParameter("server", url.getParameter("transporter", "netty"));
            if (extName == null)
                throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.remoting.Transporter) name from url(" + url.toString() + ") use keys([server, transporter])");
            com.alibaba.dubbo.remoting.Transporter extension = (com.alibaba.dubbo.remoting.Transporter) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.remoting.Transporter.class).getExtension(extName);
            return extension.bind(arg0, arg1);
        }
    }

      可以看到bind方法先對url引數(arg0)進行了非空判斷,然後便是呼叫url.getParameter方法,首先是獲取server引數,如果沒有獲取成功就獲取transporter引數,最後如果兩個引數均沒有,extName便是netty。獲取完引數之後,緊接著對extName進行非空判斷,接下來便是獲取Transporter的ExtensionLoader,並且獲取別名為extName的Transporter實現,並呼叫對應的bind,進行繫結服務埠操作。connect也是類似,只是它首先是從url中獲取client引數,在獲取transporter引數,同樣如果最後兩個引數都沒有,那麼extName也是netty,也依據extName獲取對已的介面擴充套件實現,呼叫connect方法。

   需要注意的是,沒有打上Adaptive註解的方法是不可以呼叫的,否則會丟擲UnsupportedOperationException異常。同時,Adaptive還可以打在一個介面的實現類上,這通常用於介面的方法引數中沒有URL(因此也就沒有辦法動態路由到具體實現類),因此這個時候就需要手動實現一個擴充套件點介面的Adaptive實現類,比如先頂一個擴充套件點介面:

@SPI("default")
public interface MyFirstExtension {
    public String sayHello(String name,ExtensionType type);
}

      接下來就是對這個外掛介面提供不同的實現,可以上面介面方法sayHello方法入參中並沒有URL型別,所以不能通過Dubbo動態生成adaptive類,需要自己來實現一個適配類。適配類如下:

@Adaptive
public class AdaptiveExtension implements MyFirstExtension {
    @Override
    public String sayHello(String name,ExtensionType type) {
        ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(MyFirst Extension.class);
        MyFirstExtension extension= (MyFirstExtension) extensionLoader.getDefaultExtension();
        switch (type){
            case DEFAULT:
                extension= (MyFirstExtension) extensionLoader.getExtension("default");
                break;
            case OTHER:
                extension= (MyFirstExtension) extensionLoader.getExtension("other");
                break;
        }
        return extension.sayHello(name,type);
    }
}

     可見在AdaptiveExtension中將會根據ExtensionType分發擴充套件的具體實現,並呼叫其sayHello方法。

     注:一個介面只可能存在一個Adaptive適配實現。

3、Activate 

    Activate註解主要用處是標註在外掛介面實現類上,用來配置該擴充套件實現類啟用條件。首先看一下Activate註解定義:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Activate {
    /**
     * Group過濾條件。
     * <br />
     * 包含{@link ExtensionLoader#getActivateExtension}的group引數給的值,則返回擴充套件。
     * <br />
     * 如沒有Group設定,則不過濾。
     */
    String[] group() default {};

    /**
     * Key過濾條件。包含{@link ExtensionLoader#getActivateExtension}的URL的引數Key中有,則返回擴充套件。
     * <p />
     * 示例:<br/>
     * 註解的值 <code>@Activate("cache,validatioin")</code>,
     * 則{@link ExtensionLoader#getActivateExtension}的URL的引數有<code>cache</code>Key,或是<code>validatioin</code>則返回擴充套件。
     * <br/>
     * 如沒有設定,則不過濾。
     */
    String[] value() default {};

    /**
     * 排序資訊,可以不提供。
     */
    String[] before() default {};

    /**
     * 排序資訊,可以不提供。
     */
    String[] after() default {};

    /**
     * 排序資訊,可以不提供。
     */
    int order() default 0;
}

     在Dubbo框架裡面的Filter的各種實現類都通過Activate標註,用來描述該Filter什麼時候生效。比如MonitorFilter通過Activate標註用來告訴Dubbo框架這個Filter是在服務提供端和消費端會生效的;而TimeoutFilter則是隻在服務提供端生效,消費端是不會呼叫該Filter。 ValidationFilter要啟用的條件除了在消費端和服務提供端啟用,它還配置了value,這個表述另一個啟用條件,上面介紹要獲取activate extension都需要傳入URL物件,那麼這個value配置的值則表述URL必須有指定的引數才可以啟用這個擴充套件。例如ValidationFilter則表示URL中必須包含引數validation(Constants.VALIDATION_KEY常量的值就是validation),否則即使是消費端和服務端都不會啟用這個擴充套件實現,仔細的同學還會發現在ValidationFilter中的Activate註解還有一個引數order,這是表示一種排序規則。因為一個介面的實現有多種,返回的結果是一個列表,如果不指定排序規則,那麼可能列表的排序不可控,為了實現這個所以添加了order屬性用來控制排序,其中order的值越大,那麼該擴充套件實現排序就越靠前。除了通過order來控制排序,還有before和after來配置當前擴充套件的位置,before和after配置的值是擴充套件的別名(擴充套件實現的別名是在圖23中等號左邊內容,下面出現的別名均是此內容)。

@SPI
public interface Filter {
	Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;
}
@Activate(group = {Constants.PROVIDER, Constants.CONSUMER})
public class MonitorFilter implements Filter {……}
@Activate(group = Constants.PROVIDER)
public class TimeoutFilter implements Filter {……}
@Activate(group = { Constants.CONSUMER, Constants.PROVIDER }, value = Constants.VALIDATION_KEY, order = 10000)
public class ValidationFilter implements Filter {……}

     上面基本對activate介紹的差不多了,在Dubbo框架中對這個用的最多的就是Filter的各種實現,因為Dubbo的呼叫會經過一個過濾器鏈,哪些Filter這個鏈中是通過各種Filter實現類的Activate註解來控制的。包括上面說的排序,也可以理解為過濾器鏈中各個Filter的前後順序。這裡的順序需要注意一個地方,這裡的排序均是框架本身實現擴充套件的進行排序使用者自定義的擴充套件預設是追加在列表後面。說到這裡具體例子:

<dubbo:reference id=”fooRef” interface=”com.foo.Foo”.. filter=”A,B,C”/>

     假設上面是一個有效的消費端服務引用,其中配置了一個filter屬性,並且通過逗號隔開配置了三個過濾器A,B,C(A,B,C均為Filter實現的別名),那麼對於介面Foo呼叫的過濾器鏈是怎麼樣的呢?首先Dubbo會載入預設的過濾器(一般消費端有三個ConsumerContextFilter,MonitorFilter,FutureFilter),並且對這些預設的過濾器實現進行排序(ActivateComparator實現排序邏輯),這寫預設過濾器實現會在過濾器鏈前面,後面緊接著的才是A,B,C三個自定義過濾器。

4、wrapper

      dubbo中還存在另一種擴充套件實現,那就是裝飾器,裝飾器擴充套件本身也只是一個擴充套件介面的實現而已,唯一的特殊在於,它具有一個引數型別為自身型別的建構函式,因此該擴充套件實現的作用主要用來增強、裝飾其他擴充套件。一下程式碼片段來自:loadFile方法。

 } else {// 不是@Adaptive型別
    try {
        // 如果有一個構造器的引數型別為本型別,那麼這就是一個裝飾器類
        // 判斷是否Wrapper型別
        clazz.getConstructor(type);
        Set<Class<?>> wrappers = cachedWrapperClasses;
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
            wrappers = cachedWrapperClasses;
        }
        // 放入到Wrapper實現類快取中
        wrappers.add(clazz);
        } catch (NoSuchMethodException e) {

      在一個擴充套件實現被載入時,該擴充套件介面對應的多有裝飾器都會被無序的裝飾到這個擴充套件實現上。這在createExtension中可以清晰的看到:

    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, (T) clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            }
            // 給這個例項注入各種屬性(set注入)
            injectExtension(instance);

            // 一個介面型別type的所有實現類中,如果構造器只有一個引數且這個引數型別為type,那麼會被作為一個裝飾器
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (wrapperClasses != null && wrapperClasses.size() > 0) {
                for (Class<?> wrapperClass : wrapperClasses) {
                    // 裝飾器(對每個裝飾後的例項都要執行set注入)
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }
            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                    type + ")  could not be instantiated: " + t.getMessage(), t);
        }
    }

三、使用方法總結

  1. 每個定義的SPI的介面都會構建一個ExtensionLoader例項,ExtensionLoader採用工廠模式,以靜態方法向外提供ExtensionLoader的例項。例項儲存在ExtensionLoader內部靜態不可變Map中。

  2. 外部使用時呼叫: 
    ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();getExtensionLoader方法建立ExtensionLoader例項,getAdaptiveExtension方法會載入擴充套件點中的實現類,並建立或者選擇介面卡。

    • 讀取SPI註解的value值,如果value不為空則作為預設的擴充套件點名
    • 依次讀取指定路徑下的擴充套件點 
      META-INF/dubbo/internal/ 
      META-INF/dubbo/ 
      META-INF/dubbo/services/
  3. getAdaptiveExtension方法最終呼叫loadFile方法逐行讀取SPI檔案內容並進行解析

    • 實現類上是否含有@Adaptive註解,如果有,則將其作為介面卡快取到cachedAdaptiveClass,並進入下一行配置的解析,一個SPI只能有一個介面卡,否則會報錯;
    • 如果實現類上沒有@Adaptive註解,那麼看其是否存在以當前獲取介面型別為入參的構造器,如果有,則將其作為包裝器(wrapper)存入cachedWrapperClasses變數;
    • 如果實現類既沒有@Adaptive註解,也不是包裝器,那它就是擴充套件點的具體實現
    • 判斷擴充套件實現上是否有@Activate註解,如果有,將其快取到cachedActivates(一個型別為Map<String, Activate>的變數)中,然後將其key作為擴充套件點的名字,放入cachedClasses(一個型別為Holder<Map<String, Class<?>>>的變數)中,dubbo支援在配置檔案中n:1的配置方式,即:不同名的協議使用同一個SPI實現,只要配置名字按照正則\s*[,]+\s*命名即可。
  4. 完成對檔案的解析後,getAdaptiveExtension方法開始建立介面卡例項。如果cachedAdaptiveClass已經在解析檔案中確定,則例項化該物件;如果沒有,則建立適配類位元組碼。

     Dubbo能夠為沒有介面卡的SPI生成介面卡位元組碼的必要條件:

  • 介面方法中必須至少有一個方法打上了@Adaptive註解
  • 打上了@Adaptive註解的方法引數必須有URL型別引數或者有引數中存在getURL()方法

     Dubbo生成程式碼後需要對程式碼進行編譯,大家注意,Dubbo中服務皆是SPI,編譯器的獲取依然需要ExtensionLoader來載入,Dubbo預設編譯器為javassist。Dubbo在載入Compiler時,Compiler的實現類之一AdaptiveCompiler中有@Adaptive的註解,即有已實現的介面卡,Dubbo不必為Compiler生成位元組碼,不然此時就死迴圈了。

      拿到介面卡Class後,Dubbo對介面卡進行例項化,並且實現了一個類似IOC功能的變數注入。IOC程式碼非常簡單,拿出類中public的set方法,從objectFactory中獲得對應的屬性值進行設定。

四、流程圖

                 

四、參考資料