【dubbo】dubbo SPI機制(ExtensionLoader)

    Dubbo強大的擴展能力,主要依賴於它本身實現的一套SPI機制,開發人員能夠根據dubbo的規範進行擴展示有的功能或者替換現有的實現,dubbo內部的功能的實現也都是經過的SPI來實現的,這是dubbo的功能高度可插拔的緣由。可是dubbo並無使用Java的SPI,之因此沒有使用Java自帶的SPI在官方文檔上有以下闡述。java

  •   JDK標準的SPI會一次性實例化擴展點全部實現,若是有擴展實現初始化很耗時,但若是沒用上也加載,會很浪費資源。
  •  若是擴展點加載失敗,連擴展點的名稱都拿不到了。好比:JDK標準的ScriptEngine,經過 getName()獲取腳本類型的名稱,但若是RubyScriptEngine由於所依賴的jruby.jar不存在,致使RubyScriptEngine類加載失敗,這個失敗緣由被吃掉了,和ruby對應不起來,當用戶執行ruby腳本時,會報不支持ruby,而不是真正失敗的緣由。
  •  增長了對擴展點IoC和AOP的支持,一個擴展點能夠直接setter注入其它擴展點。

    若是想要使用Java自帶的SPI,能夠參考java SPI (Service Provider Interface)緩存

使用約定

    在擴展類的jar包內,放置擴展點配置文件:META-INF/dubbo/接口全限定名,內容爲:配置名=擴展實現類全限定名,多個實現類用換行符分隔。
注意:這裏的配置文件是放在你本身的jar包內,不是dubbo自己的jar包內,Dubbo會全ClassPath掃描全部jar包內同名的這個文件,而後進行合併
dubbo SPI的主要是經過ExtensionLoader這個類來實現的,全部關鍵代碼也都在這個類中,使用方式有兩種,一種能夠手工指定,另一種是自動生成。ruby

基本用法和示例

    有一個DuSPI的接口,接口中有一個sayHi的方法,而後經過dubbo的SPI機制進行調用其相關實現。ide

接口類url

必需要有@SPI註解 ,SPI註解能夠有一個value參數,表明默認的實現。spa

//接口類,定義了一個SayHello的方法,須要被實現
@SPI
public interface DuSPI {
    public String SayHello(String hi);
}

//實現類,沒什麼好說的。
public class LocalDuSPI implements DuSPI {
    @Override
    public String SayHello(String hi) {
        return "FROM LocalDuSPI : "+hi;
    }
}


擴展點配置.net

調用方式設計

public class DuSPIMain {
    private static final DuSPI duSPI = ExtensionLoader.getExtensionLoader(DuSPI.class).getExtension("local");
    public static void main(String[] args) {
        String hi = duSPI.SayHello("擴展SPI");
        System.out.println(hi);
    }
}

輸出結果爲代理

    上述就是很是簡單的手工指定實現類用例。getExtension("local");接收一個key值,這個key就是spi文件中的key,獲得的實現就是key對應的實現類。code

    ExtensionLoader還有一種自動尋找擴展的方法,getAdaptiveExtension,不須要手工指定實現,而是在方法具體調用的時候自動發現其具體的實現類。例如Protocol擴展,它的export方法就是經過invoke的URL中的Protocol,自動的發現實現

private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();

Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));

Exporter<?> exporter = protocol.export(invoker);

Duboo SPI源碼解析ExtensionLoader類

經過對類註釋,能夠看到這個類主要實現如下幾個功能

自動注入關聯擴展點

自動Wrap上擴展點的Wrap類

缺省得到的擴展點的一個內部類Adaptive

這個類的實例需簡要有一個getExtensionLoader的靜態工廠方法,接收一個Class類型的對象,來獲取其實例。

@SuppressWarnings("unchecked")
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    if (type == null)
        throw new IllegalArgumentException("Extension type == null");
    if(!type.isInterface()) {
        throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
    }
    if(!withExtensionAnnotation(type)) {
        throw new IllegalArgumentException("Extension type(" + type +
                ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
    }

    ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    if (loader == null) {
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}

    經過以上代碼能夠看出,傳入的對象必須是一個接口,而且必須是帶有@SPI註解,獲取完了之後,針對相應的接口類型會緩存到map中去。

根據key返回相應的實現類方法
getExtensionClass(String name)

    首先會去在緩存裏面查找,看是否已經加載過這個類,若是加載過的話就直接返回,若是沒有,則配置文件中查找,而後進行加載。這樣就達到了它說的jdk的spi要所有加載全部擴展,而不是按需加載,若是有相應的擴展找不到就會報錯的或者一個類的加載很耗的問題。
建立擴展是使用的createExtension方法,類實例化之後,會查看有沒有對其餘擴展點的應用,若是有的話就調用injectExtension方法,去注入其餘的擴展類,這樣就達到了它所說的AOP的功能。

加載擴展類的方法是loadExtensionClasses

private Map<String, Class<?>> loadExtensionClasses() {
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if(defaultAnnotation != null) {
        String value = defaultAnnotation.value();
        if(value != null && (value = value.trim()).length() > 0) {
            String[] names = NAME_SEPARATOR.split(value);
            if(names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
                        + ": " + Arrays.toString(names));
            }
            if(names.length == 1) cachedDefaultName = names[0];
        }
    }
    
    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    loadFile(extensionClasses, DUBBO_DIRECTORY);
    loadFile(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}

經過以上能夠得出如下結論

  • 加載SPI註解,註解的value只能有一個,也就是默認擴展實現類。
  • Dubbo會依次掃描META-INF/dubbo/internal/、META-INF/dubbo/、
  • META-INF/services/ 這三個文件夾,優先級正好相反,看有沒有相應的SPI文件、經過loadFile方法進行掃描和加載。
     
private void loadFile(Map<String, Class<?>> extensionClasses, String dir) {
        String fileName = dir + type.getName();
        try {
            ...
            if (urls != null) {
                while (urls.hasMoreElements()) {
                    java.net.URL url = urls.nextElement();
                    try {
                        BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8"));
                        try {
                            String line = null;
                            while ((line = reader.readLine()) != null) {
                                final int ci = line.indexOf('#');
                                if (ci >= 0) line = line.substring(0, ci);
                                line = line.trim();
                                ...
                                        int i = line.indexOf('=');
                                        if (i > 0) {
                                            name = line.substring(0, i).trim();
                                            line = line.substring(i + 1).trim();
                                        }
                                ...
                                            Class<?> clazz = Class.forName(line, true, classLoader);
                                            if (! type.isAssignableFrom(clazz)) {
                                                throw new IllegalStateException("Error when load extension class(interface: " +
                                                        type + ", class line: " + clazz.getName() + "), class "
                                                        + clazz.getName() + "is not subtype of interface.");
                                            }
                                
                                            if (clazz.isAnnotationPresent(Adaptive.class)) {
                                                if(cachedAdaptiveClass == null) {
                                                    cachedAdaptiveClass = clazz;
                                                } else if (! cachedAdaptiveClass.equals(clazz)) {
                                                    throw new IllegalStateException("More than 1 adaptive class found: "
                                                            + cachedAdaptiveClass.getClass().getName()
                                                            + ", " + clazz.getClass().getName());
                                                }
                                            } else {
                                                try {
                                                    clazz.getConstructor(type);
                                 ...
                                                    String[] names = NAME_SEPARATOR.split(name);
                                                    if (names != null && names.length > 0) {
                                                        Activate activate = clazz.getAnnotation(Activate.class);
                                                        if (activate != null) {
                                                            cachedActivates.put(names[0], activate);
                                                        }
                                ...
                                                    }
                                                }
                                            }
                                        }
                                  
    }
}
  1. 經過加載類的文件能夠看出
  2. 首先會掃描目錄下的全部文件,
  3. 逐行讀取配置文件
  4. 忽略#號後內容做爲註釋
  5. 分割字符串,等號前面的做爲key,等號後面的做爲value
  6. 經過Class.forName(value)獲取具體的實現類對象
  7. 作一些類的類型校驗
  8. 緩存獲得的類

自適應Extention(Protocol示例)

在生成對象引用的時候,並不直接生成實現對象,而是先生成一個代理對象,直到調用的時候,在根據傳遞的參數決定調用的是哪一個具體的實現類。Dubbo的SPI實現了自動發現機制。經過調用getAdaptiveExtension方法生成代理對象。在Protocol接口的使用中就用到了這個方法。
ServiceConfig類用須要有引用對象Protocol,這是標明dubbo的服務註冊所使用的協議。Protocol的實現類不少

可是在聲明的時候並不指名要調用哪一種協議的實現。

private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

代碼上調用了getAdaptiveExtension()方法,生成代理對象,因爲是代碼生成的代理對象,在dubbo中並無定義,因此只能把類文件拼裝的代碼的代碼打印出來,生成類文件的代碼是ExtensionLoader的createAdaptiveExtensionClassCode方法。生成的代理對象的類的定義以下:

package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.extension.ExtensionLoader;

public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
    public void destroy() {
        throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }

    public int getDefaultPort() {
        throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }

    public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws java.lang.Class {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg1;
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }

    public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.Invoker {
        if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
        com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }
}

這是一個經過javassit自動生成的一個類,實現了Protocol接口,對於服務端內容導出發佈,實現了export方法,export 方法中,首先獲取Invoker的url,URL中獲取Protocol的protocol屬性的值,看是否進行了設置,默認爲dubbo,而後再調用dubboSPI(ExtensionLoader)機制中的getExtension方法,把Key傳入進去,以達到自動獲取Protocol的目的,而後找到真正的實現類,再調用其export方法。若是採用zk做爲註冊中心,經過配置<dubbo:registry address="zookeeper://localhost:2181" />
這樣取得的key是就是registry,真正的實現類則是

com.alibaba.dubbo.registry.integration.RegistryProtocol

固然,上述只是講述了dubbo spi設計的一個大概的思路,還有不少的其餘的細節問題在本文中並未提到,不過這對於瞭解dubbo自定義實現SPI的設計已經足夠。  

相關文章
相關標籤/搜索