Dubbo SPI源碼分析

在Dubbo的官網上,能夠看到這樣一句話:java

Dubbo具備高度的可擴展能力,遵循微內核+插件的設計原則,全部核心能力如Protocol、Transport、Serialization被設計爲擴展點,平等對待內置實現和第三方實現。apache

對於一個開源的RPC框架,可擴展性是十分重要的,那麼Dubbo是怎麼來實現這一點的呢,咱們能夠在Dubbo的源碼中隨處能夠看到下面這樣的代碼:緩存

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
複製代碼

這個ExtensionLoader就是Dubbo擴展能力的基礎,也是理解Dubbo運行機制的基石,那麼下面咱們先來了解了解SPI是什麼。安全

SPI機制

SPI(Service Provider Interface),是一種服務發現機制,Dubbo的SPI是從Java SPI加強而來,Dubbo的文檔中給了三個加強的理由:ruby

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

其中第一點懶加載和第三點IOC是咱們平時所熟知的,也是我我的認爲比較重要的,至於第二點筆者也從未碰見過這樣的場景。bash

首先來看一個小小的Dubbo SPI的使用案例:app

  1. 定義一個Interface Transporter負載均衡

    @SPI("udp") 
    public interface Transporter {
    
        void send(String msg);
    
        @Adaptive("transporter")
        void send(String msg, URL url);
    }
    複製代碼
  2. 定義Transporter的實現類框架

    UDPTransportertcp

    public class UDPTransporter implements Transporter{
        public void send(String msg) {
            System.out.println("Transfer " + msg + " thorough UDP");
        }
        public void send(String msg, URL url) {
            send(msg);
        }
    }
    複製代碼

    TCPTransporter

    @Activate(value = "reliability")
    public class TCPTransporter implements Transporter{
        public void send(String msg) {
            System.out.println("Transfer " + msg + " thorough TCP");
        }
        public void send(String msg, URL url) {
            send(msg);
        }
    }
    複製代碼
  3. 關聯SPI機制

    咱們須要在/resources/META-INF/dubbo目錄下建立以下文件

image-20191217231726993.png

文件內容爲:

tcp=com.wanglaomo.playground.dubbo.provider.spi.TCPTransporter
udp=com.wanglaomo.playground.dubbo.provider.spi.UDPTransporter
複製代碼

相比於Java的SPI機制,咱們能夠看到Dubbo的SPI多了一層映射關係{tcp -> TCPTransporter, udp -> UDPTransporter}。

  1. 最後調用Transporter實現

    ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Transporter.class);
    
    System.out.println(">>>>>>>>> 基礎用法");
    Transporter transporter = (Transporter) extensionLoader.getExtension("tcp");
            transporter.send("msg");
    
    System.out.println(">>>>>>>>> 默認實現");
    Transporter defaultTransporter = (Transporter) extensionLoader.getDefaultExtension();
    defaultTransporter.send("msg");
    
    System.out.println(">>>>>>>>> 自適應實現");
    Transporter adaptiveTransporter = (Transporter) extensionLoader.getAdaptiveExtension();
            adaptiveTransporter.send("msg", URL.valueOf("test://localhost/test?transporter=udp"));
    
    System.out.println(">>>>>>>>> 自動激活實現");
    List<Transporter> activeTransporters = extensionLoader.getActivateExtension(URL.valueOf("test://localhost/test?reliability=true"), (String) null);
    for(Transporter activateTransporter : activeTransporters) {
        activateTransporter.send("msg");
    }
    複製代碼

結果爲:

>>>>>>>>> 基礎用法
Transfer msg thorough TCP
>>>>>>>>> 默認實現
Transfer msg thorough UDP
>>>>>>>>> 自適應實現
Transfer msg thorough TCP
>>>>>>>>> 自動激活實現
Transfer msg thorough TCP
複製代碼

在這個小小的Demo裏面咱們一次性地把Dubbo SPI的全部用法都過了個遍,在Dubbo源碼中也無外乎就這幾種。

如今看起來可能還有些不知因此然,看完下面的幾個小節就會明白了。

Dubbo SPI 基本概念

Dubbo中全部可擴展的接口都統稱爲擴展點,如:註冊中心的擴展(Registry)、序列化方式的擴展(Serialization)等。

當在接口上標註**@SPI**註解時,則代表該接口是一個Dubbo的擴展點(如Demo中的Transporter接口)。在Dubbo啓動時,會將這些接口及相應的實現類掃描並進行相應類的加載。所以當咱們能夠能夠經過實現擴展點接口或@SPI註解讓Dubbo自動幫咱們管理擴展點。

META-INF/dubbo下的文件中,等號左邊爲擴展點的別名,等號右邊爲擴展點的實現類。至於爲何要放在這個目錄下,則是Dubbo約定俗成的一種寫法。

Dubbo URL至關於一次Dubbo調用的配置信息,是可變化的。

@Activate代表該擴展點是可被激活的擴展點,當value爲空表示默認激活,當value不爲空時,則表示Dubbo URL中包含相關參數時纔會激活。同時還有一個group的屬性,通常填寫爲CONSUMER或PROVIDER,代表只有在消費者或生產者中才會激活。

@Adaptive能夠被標註在類和方法上,當標註在方法上時,代表Dubbo會對生成的代理類的該方法進行自適應拓展,會根據URL中的參數調用相應的擴展點的方法。從而實如今運行時動態的決定加載某一個擴展點。至關於代理模式和策略模式的一個結合。基本上,每個擴展點都會被Dubbo生成一個相應的自適應擴展類。當@Adaptive被標註在類上時,則代表Dubbo直接使用該類做爲已實現自適應擴展的類,而不用Dubbo再自行生成。目前Dubbo有這麼兩個類被加上了@Adaptive註解:AdaptiveCompilerAdaptiveExtensionFactory。能夠看看這兩個類,加深理解。

擴展點的IOC工廠

在上面Demo中高頻出現的ExtensionLoader類則實現了對這些擴展點的管理。能夠看到ExtensionLoader中維護了這個靜態的ConcurrentHashMap,鍵爲擴展點接口對應的Class,值爲相對應的ExtensionLoader。

static ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();
複製代碼

而在每一個ExtensionLoader的每個對象中維護着下面的四種成員變量:

// 可激活的ExtensionLoader
Map<String, Object> cachedActivates = new ConcurrentHashMap<>();
// 自適應的實現
Class<?> cachedAdaptiveClass;
// 默認實現的名稱 
String cachedDefaultName;
// 全部的擴展點別名和擴展點實體的集合
ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
複製代碼

經過這四種成員變量咱們能夠發現ExtensionLoader是一個很重的工廠類對象,再結合下面會講到的擴展點自動注入,ExtensionLoader基本上實現了一個功能完備的擴展點IOC工廠。

那麼咱們來經過源碼看看這個IOC工廠是怎麼運行的,咱們先以這一句代碼爲出發點來理解ExtensionLoader

// ExtensionLoader#getExtensionLoader
// ExtensionLoader的集合
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();

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 an interface!");
    }
    // 判斷是否爲@SPI標註的擴展點
    if (!withExtensionAnnotation(type)) {
        throw new IllegalArgumentException("Extension type (" + type +
                                           ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
    }

    // 建立ExtensionLoader
    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;
}

複製代碼

首先作一些擴展點接口的參數判斷,而後經過ConcurrentHashMap#putIfAbsent方法來實現ExtensionLoader的延遲加載。而後讓咱們看一下ExtensionLoader的建立過程。

// ExtensionLoader#construction
private final ExtensionFactory objectFactory;

private ExtensionLoader(Class<?> type) {
        this.type = type;
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }
複製代碼

咱們能夠看到基本上這裏只是個工廠類的殼子,相應的擴展類並無被加載,由於擴展點實現類的加載也是延遲的。至於這個ExtensionFactory,由於它自己也被標註爲@SPI擴展點,在獲取它的ExtensionLoader時也會來這裏走一遭,因此在這裏進行了特殊的判斷,打斷了循環。

顧名思義,ExtensionFactory也是個工廠類,包含了一個接口,該接口用於獲取一個擴展點的實現類

<T> T getExtension(Class<T> type, String name);
複製代碼

Dubbo會在自動注入擴展點時使用到該方法,它一共有三個實現類:

ExtensionFactory.png

在Duubo中通常會直接使用AdaptiveExtensionFactory來獲取擴展點實現類。

// AdapttiveExtensionFactory 
public AdaptiveExtensionFactory() {
        ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
        List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
        // 獲取全部的擴展點實現類 
        for (String name : loader.getSupportedExtensions()) {
            list.add(loader.getExtension(name));
        }
        factories = Collections.unmodifiableList(list);
    }

    @Override
    public <T> T getExtension(Class<T> type, String name) {
        // 逐個去獲取實現類
        for (ExtensionFactory factory : factories) {
            T extension = factory.getExtension(type, name);
            if (extension != null) {
                return extension;
            }
        }
        return null;
    }
複製代碼

在建立AdaptiveExtensionFactory時,會利用ExtensionLoader加載全部的ExtensionFactory擴展點的實現類,並在getExtension方法中逐個嘗試獲取擴展點實現類。

SPIExtensionFactory比較簡單,就是直接經過ExtensionLoader得到實現類的自適應擴展點實現類。

// SPIExtensionFactory#getExtension
public <T> T getExtension(Class<T> type, String name) {
    if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
        ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
        if (!loader.getSupportedExtensions().isEmpty()) {
            return loader.getAdaptiveExtension();
        }
    }
    return null;
}
複製代碼

而SpringExtensionFactory則是依託於Spring的IOC容器加載實現類。

// SpringExtensionFactory#getExtension
public <T> T getExtension(Class<T> type, String name) {
    if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
        return null;
    }

    for (ApplicationContext context : CONTEXTS) {
        if (context.containsBean(name)) {
            Object bean = context.getBean(name);
            if (type.isInstance(bean)) {
                return (T) bean;
            }
        }
    }
    // ...省略
}
複製代碼

我的理解,SpringExtensionFactory是對SPIExtensionFactory的一個補充,能夠繞過SPI配置而直接使用開發者熟悉的Spring Bean的這一套東西。

講到這裏,可能很多讀者以爲ExtensionLoader和ExtensionFactory有些重複的感受。我我的理解是,ExtensionFactory是一個SPI加載機制的擴展點,能夠將別的加載類的體系歸入到Dubbo SPI的這個模型, 從而實現和Dubbo框架的融合,方便Dubbo統一的進行擴展點的管理,而不用在代碼中四處「打補丁」.

而鑑於如今有且只有Spring的一種,若是在不使用Spring Bean的狀況下,並且徹底能夠去除掉ExtensionFactory這個類。

Dubbo SPI的擴展點分類

上面講清楚Dubbo的擴展點工廠後,咱們就來接入本文的核心,一個擴展點在被ExtensionLoader加載後,它的實現類會造成下面這幾種擴展點:

  1. 普通擴展點實現
  2. 自適應擴展點實現(有且僅有一個)
  3. 可激活擴展點實現
  4. 默認擴展點實現(零個或一個)
  5. wrapper擴展點實現

一個擴展點的實現可能同時隸屬於上面的一種或多種。下面來看一個普通的擴展點實現是怎麼加載的。

普通擴展點

// ExtensionLoader#getExtension
public T getExtension(String name) {
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    // 若是name爲true,則返回默認實現
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    final Holder<Object> holder = getOrCreateHolder(name);
    // 雙重檢查鎖加載擴展點實現實例
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

private Holder<Object> getOrCreateHolder(String name) {
    
    // 由於Holder對象很輕量,因此這裏沒有使用雙重檢查鎖
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<>());
        holder = cachedInstances.get(name);
    }
    return holder;
}

// Holder
public class Holder<T> {
    private volatile T value;
    public void set(T value) {
        this.value = value;
    }
    public T get() {
        return value;
    }
}
複製代碼

由於createExtension是一個很重的方法,會加載不少類,爲了不重複加載,包裝了一層Holder對象,並對每一次加載執行雙重檢查鎖確保線程安全。

// ExtensionLoader#createExtension
private T createExtension(String name) {
    // 獲取和擴展點別名配對的實現類的Class文件
    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);
        // 處理wrapperClass,在下面自動包裝擴展點講解
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (CollectionUtils.isNotEmpty(wrapperClasses)) {
            for (Class<?> wrapperClass : wrapperClasses) {
                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);
    }
}
複製代碼

上面這段代碼主要包含三個主要邏輯,咱們下面一個一個看.

1. 獲取擴展點實現類的Class文件

private Map<String, Class<?>> getExtensionClasses() {
    Map<String, Class<?>> classes = cachedClasses.get();
    // 雙重檢查鎖
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 掃描並收集全部擴展點的Class文件
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}
複製代碼

Dubbo會將全部的擴展點的Class文件收集起來,同時對不一樣的實現類的Class做區分,使用雙重檢查鎖避免重複加載。

// ExtensionLoader#loadExtensionClasses
private static final String SERVICES_DIRECTORY = "META-INF/services/";
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";

// synchronized in getExtensionClasses
private Map<String, Class<?>> loadExtensionClasses() {
    // 在下面默認擴展點講解
    cacheDefaultExtensionName();

    Map<String, Class<?>> extensionClasses = new HashMap<>();
    // [套娃一]從給定目錄中加載SPI配置文件及收集擴展點Class文件
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
    loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
    loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
    return extensionClasses;
}
複製代碼

如上面所述Dubbo會從約定的目錄下(/META-INF/services/META-INF/dubbo/META-INF/dubbo/internal)中掃描SPI配置文件。後面的replace應該是爲了適配到Apache包的緣故

// ExtensionLoader#loadDirectory
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
    // 在上面demo中
    // fileName = "METAINF/dubbo/com.wanglaomo.playground.dubbo.provider.spi.Transporter"
    String fileName = dir + type;
    try {
        Enumeration<java.net.URL> urls;
        // 依次從這幾個地方找ClassLoader 
        // Thread ClassLoader -> class.getClassLoader() -> SystemClassLoader
        ClassLoader classLoader = findClassLoader();
        if (classLoader != null) {
            urls = classLoader.getResources(fileName);
        } else {
            urls = ClassLoader.getSystemResources(fileName);
        }
        if (urls != null) {
            while (urls.hasMoreElements()) {
                java.net.URL resourceURL = urls.nextElement();
                // [套娃二]繼續追進去吧
                loadResource(extensionClasses, classLoader, resourceURL);
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                     type + ", description file: " + fileName + ").", t);
    }
}
複製代碼

這一層套娃主要是獲取當前環境中的ClassLoader,確保可以正確的加載擴展點實現類Class。同時使用該ClassLoader加載SPI配置文件

// ExtensionLoader#loadResource
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
    try {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
            String line;
            // 以咱們的demo中的例子來講 
            // line = "tcp=com.wanglaomo.playground.dubbo.provider.spi.TCPTransporter"
            while ((line = reader.readLine()) != null) {
                final int ci = line.indexOf('#');
                if (ci >= 0) {
                    line = line.substring(0, ci);
                }
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        String name = null;
                        int i = line.indexOf('=');
                        // 以等號分割line得到擴展點別名和擴展點實現類Class的全限定名
                        if (i > 0) {
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            // [套娃三] 真正得到Class的地方
                            loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                        }
                    } catch (Throwable t) {
                        IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
                        exceptions.put(line, e);
                    }
                }
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                     type + ", class file: " + resourceURL + ") in " + resourceURL, t);
    }
}
複製代碼

這一層套娃是解析SPI配置文件,獲得擴展點別名和擴展點實現類Class的全限定名

// ExtensionLoader#loadClass
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
    // 判斷實現類是否implements擴展點接口
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("Error occurred when loading extension class (interface: " +
                                        type + ", class line: " + clazz.getName() + "), class "
                                        + clazz.getName() + " is not subtype of interface.");
    }
    // 在下面自適應擴展點中講解 講解若是當前擴展點實現類上被標註了@Adaptive則代表該類爲可適應擴展點實現類
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        cacheAdaptiveClass(clazz);
    // 在下面自動包裝擴展點中講解
    } else if (isWrapperClass(clazz)) {
        cacheWrapperClass(clazz);
    } else {
        // 檢查擴展點實現類的無參構造方法
        clazz.getConstructor();
        // 若是擴展點別名爲空則從別的地方獲取別名
        if (StringUtils.isEmpty(name)) {
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
            }
        }

        String[] names = NAME_SEPARATOR.split(name);
        if (ArrayUtils.isNotEmpty(names)) {
            // 在下面自動激活擴展點中講解
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                // 將擴展點實現類Class和擴展點別名的對應關係保存在Map中,方便查詢
                cacheName(clazz, n);
                // 將擴展點別面和擴展點實現類Class的對應關係保存在Map中
                saveInExtensionClass(extensionClasses, clazz, n);
            }
        }
    }
}

// ExtensionLoader#findAnnotationName
private String findAnnotationName(Class<?> clazz) {
    org.apache.dubbo.common.Extension extension = clazz.getAnnotation(org.apache.dubbo.common.Extension.class);
    if (extension == null) {
        // 沒有被標註,則以駝峯命名法獲取擴展點別名
        String name = clazz.getSimpleName();
        if (name.endsWith(type.getSimpleName())) {
            name = name.substring(0, name.length() - type.getSimpleName().length());
        }
        return name.toLowerCase();
    }
    // 若是擴展點實現類上被標註了@Extension註解,則以@Extension註解的value值爲擴展點別名
    return extension.value();
}

private void saveInExtensionClass(Map<String, Class<?>> extensionClasses, Class<?> clazz, String name) {
    Class<?> c = extensionClasses.get(name);
    if (c == null) {
        extensionClasses.put(name, clazz);
    } else if (c != clazz) {
        // 不容許有兩個擴展點別名相同
        throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + name + " on " + c.getName() + " and " + clazz.getName());
    }
}
複製代碼

這最後一層套娃主要是區分不一樣的擴展點實現類,咱們能夠看到,自適應擴展點和自動包裝擴展點是和其餘類型擴展點實現類是各自分隔的。而自動激活擴展點、默認擴展點和普通擴展點是相容的。

至此,全部的獲取擴展點實現類的Class文件的相關邏輯已經追蹤完畢。結合ExtensionLoader#createExtension中的代碼,咱們能夠發現,Dubbo一次性會將全部的擴展點的Class文件收集起來,可是並不會將這些Class文件實例化,而是在使用到該擴展點實現類時纔會實例化,從而達到了延遲加載的效果。

2.擴展點實現類Class實例化

T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
    // 經過反射建立實例
    EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
    instance = (T) EXTENSION_INSTANCES.get(clazz);
}
複製代碼

擴展點實例化的代碼就比較簡單,由於以前已經檢查過擴展點實現類的無參構造方法,因此這裏能夠直接使用clazz.newInstance()來實例化。同時也使用了ConcurrentHashMap的putIfAbsent來確保線程安全性。

3.擴展點自動注入

// ExtensionLoader#injectExtension
private T injectExtension(T instance) {
    try {
        // 前面所述的ExtensionFactory的實例不能爲空
        if (objectFactory != null) {
            // 循環當前實例的方法
            for (Method method : instance.getClass().getMethods()) {
                // 全部對的set方法
                if (isSetter(method)) {
                    // 當方法上被標註@DisableInject時忽略自動注入
                    if (method.getAnnotation(DisableInject.class) != null) {
                        continue;
                    }
                    // 只根據set方法的第一個參數作匹配並獲得該參數的類文件
                    Class<?> pt = method.getParameterTypes()[0];
                    // 若是爲基本數據類型則跳過
                    if (ReflectUtils.isPrimitives(pt)) {
                        continue;
                    }
                    try {
                        // 根據駝峯命名法得到參數的擴展點別名
                        String property = getSetterProperty(method);
                        // 經過ExtensionFactory得到擴展點實例
                        Object object = objectFactory.getExtension(pt, property);
                        if (object != null) {
                            // 執行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;
}
複製代碼

IOC注入的方式在上面的註釋中已經寫得很清楚了,並無想象中那麼智能,甚至能夠說有一點笨拙,須要對全部的擴展點的set方法有了一些約束,可是勝在簡單好用。

4. 普通擴展點總結

實現原理主要是依賴了ClassLoader的一些機制,同時使用了反射的技術來操做擴展點實現類的Class文件和其上對應的註解。我的感受是一個比較經典的根據配置文件來加載工廠類的實現。其中的延遲加載、根據註解自動配置以及自動注入值得好好的吸取一下

普通擴展點的加載是邏輯最長的一段,其餘類型的擴展點的加載都是依託於普通擴展點加載完成。若是有什麼疑惑的地方,能夠反覆理解一下。

自適應擴展點

自適應擴展點的加載咱們從加載普通擴展點時的那個邏輯分支繼續追蹤

private volatile Class<?> cachedAdaptiveClass = null; 

// ExtensionLoader#loadClass
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
    // ... 省略
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        cacheAdaptiveClass(clazz);
    // ... 省略
}

private void cacheAdaptiveClass(Class<?> clazz) {
    if (cachedAdaptiveClass == null) {
        // 緩存自適應擴展點爲
        cachedAdaptiveClass = clazz;
        // 被標註爲@Adaptive的擴展點實現類只能有一個
    } else if (!cachedAdaptiveClass.equals(clazz)) {
        throw new IllegalStateException("More than 1 adaptive class found: "
                                        + cachedAdaptiveClass.getClass().getName()
                                        + ", " + clazz.getClass().getName());
    }
}
複製代碼

這一段代碼就是以前所說的這句

當@Adaptive被標註在類上時,則代表Dubbo直接使用該類做爲已實現自適應擴展的類,而不用Dubbo再自行生成。

的具體實現。那麼Dubbo是怎麼自動生成自適應擴展點呢?讓咱們從加載自適應擴展點的邏輯開始分析

// ExtensionLoader#getAdaptiveExtension
private final Holder<Object> cachedAdaptiveInstance = new Holder<>();

public T getAdaptiveExtension() {
    
    // 仍是同樣的,雙重檢查鎖確保只會加載一次且保證線程安全
    Object instance = cachedAdaptiveInstance.get();
    if (instance == null) {
        // 若是以前加載自適應擴展點時報錯則直接拋出異常避免重複
        if (createAdaptiveInstanceError == null) {
            synchronized (cachedAdaptiveInstance) {
                instance = cachedAdaptiveInstance.get();
                if (instance == null) {
                    try {
                        // 建立自適應擴展點實例
                        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 T createAdaptiveExtension() {
    try {
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {
        throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
    }
}
複製代碼

建立自適應擴展點實例有三個步驟,其中injectExtension自動注入和以前的加載普通擴展點時同樣的。並且也是經過無參構造方法新建一個新的實例。重點在於getAdaptiveExtensionclass獲取自適應擴展點實現類的Class。

// ExtensionLoader#getAdaptiveExtensionClass
private Class<?> getAdaptiveExtensionClass() {
    // 和以前相似加載全部的擴展點Class文件,有可能已經加載過了。
    getExtensionClasses();
    // 若是該type全部的擴展點中有標註爲@Adaptive的擴展點實現類,則直接使用
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    // 不然建立一個
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
複製代碼

這一段的邏輯仍是和以前所述一致。

// ExtensionLoader#createAdaptiveExtensionClass
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();
    // 使用Compiler編譯該源代碼並生成Class文件
    return compiler.compile(code, classLoader);
}
複製代碼

經過給定的擴展點接口的Class文件及默認擴展點的名稱生成自適應擴展點的源代碼,由於該方法比較繁瑣,主要就是字符串的組裝,因此這裏直接貼出咱們上面的demo生成的自適應擴展點實現的源代碼,方便理解。

package com.wanglaomo.playground.dubbo.provider.spi;

import org.apache.dubbo.common.extension.ExtensionLoader;

public class Transporter$Adaptive implements com.wanglaomo.playground.dubbo.provider.spi.Transporter {
    // 由於adptive方法必需要傳入URL對象,因此這裏直接拋出一異常
    public void send(java.lang.String arg0) {
        throw new UnsupportedOperationException("The method public abstract void com.wanglaomo.playground.dubbo.provider.spi.Transporter.send(java.lang.String) of interface com.wanglaomo.playground.dubbo.provider.spi.Transporter is not adaptive method!");
    }

    public void send(java.lang.String arg0, org.apache.dubbo.common.URL arg1) {
        // 檢查URL參數
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg1;
        // 得到URL對象中有關咱們事先配置好的@Adaptive參數對應的值,沒有的話使用默認擴展點名稱
        String extName = url.getParameter("transporter", "udp");
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (com.wanglaomo.playground.dubbo.provider.spi.Transporter) name from url (" + url.toString() + ") use keys([transporter])");
        // 根據從參數得到的擴展點別名得到相應的擴展點實現類
        com.wanglaomo.playground.dubbo.provider.spi.Transporter extension = (com.wanglaomo.playground.dubbo.provider.spi.Transporter) ExtensionLoader.getExtensionLoader(com.wanglaomo.playground.dubbo.provider.spi.Transporter.class).getExtension(extName);
        // 調用具體擴展點實現類的具體方法
        extension.send(arg0, arg1);
    }
}
複製代碼

從生成出來的代碼來看,主要是配合工廠類和URL對象來實現自動適配。

有了源代碼以後,就會調用Compiler對象進行編譯,目前支持兩種編譯器,JavaassistCompiler和JDKCompiler。Javassist是一個操做字節碼文件的庫,能夠動態的更改字節碼文件,固然也能夠實時編譯,建立新的字節碼文件。具體Compiler是怎麼工做的和SPI機制關係不大,因此這裏不作講解。

自適應擴展點很好的實現了了在運行時動態決定調用某個擴展點實現的功能,而且默認會給每個擴展點生成一個,方便調用,主要的難點就是生成類的源代碼和動態編譯。

自動激活擴展點

自動激活擴展點的加載咱們從加載普通擴展點時的那個邏輯分支繼續追蹤

// ExtensionLoader#cacheActivateClass
private void cacheActivateClass(Class<?> clazz, String name) {
    // 當擴展點實現類被標註上@Activate註解時,則將擴展點別名和Activate對象的關聯關係保存在Map中
    Activate activate = clazz.getAnnotation(Activate.class);
    if (activate != null) {
        cachedActivates.put(name, activate);
    } else {
        // support com.alibaba.dubbo.common.extension.Activate
        com.alibaba.dubbo.common.extension.Activate oldActivate = clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class);
        if (oldActivate != null) {
            cachedActivates.put(name, oldActivate);
        }
    }
}
複製代碼

這段代碼沒什麼特別的,主要是將關聯關係提早收集並保存。主要的邏輯在加載自動激活擴展點的代碼中

// ExtensionLoader#getActivateExtension
public List<T> getActivateExtension(URL url, String key, String group) {
    
    // 從URL對象中經過給定鍵獲取對應擴展點別名集合
    String value = url.getParameter(key);
    // 若是擴展點別名集合不爲空則以逗號分隔
    return getActivateExtension(url, StringUtils.isEmpty(value) ? null : COMMA_SPLIT_PATTERN.split(value), group);
}

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);
    // REMOVE_VALUE_PREFIX + DEFAULT_KEY = -default
    // 即若是URL配置對象中沒有-default的配置
    if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
        getExtensionClasses();
        // 迭代收集起來的擴展點別名和Activate對象的關聯關係
        for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
            String name = entry.getKey();
            Object activate = entry.getValue();

            String[] activateGroup, activateValue;
            // 得到@Active註解上標註的激活Value集合和激活Group集合
            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是否在激活group中
            if (isMatchGroup(group, activateGroup)) {
                // 根據擴展點別名得到擴展點實例
                T ext = getExtension(name);
                
                // 若是指定擴展點別名不包含該擴展點,若是包含在下面會處理
                if (!names.contains(name)
                    // 若是URL配置對象中沒有指定去除該擴展點
                    && !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);
            }
        }
    }
    // 合併兩種擴展點集合usrs和exts
    if (!usrs.isEmpty()) {
        exts.addAll(usrs);
    }
    return exts;
}

private boolean isMatchGroup(String group, String[] groups) {
    if (StringUtils.isEmpty(group)) {
        return true;
    }
    if (groups != null && groups.length > 0) {
        for (String g : groups) {
            if (group.equals(g)) {
                return true;
            }
        }
    }
    return false;
}

// 經過URL配置對象中是否存在參數名爲 `value`或`.value` ,而且參數值非空。
private boolean isActive(String[] keys, URL url) {
    if (keys.length == 0) {
        return true;
    }
    for (String key : keys) {
        for (Map.Entry<String, String> entry : url.getParameters().entrySet()) {
            String k = entry.getKey();
            String v = entry.getValue();
            if ((k.equals(key) || k.endsWith("." + key))
                && ConfigUtils.isNotEmpty(v)) {
                return true;
            }
        }
    }
    return false;
}
複製代碼

-default配置的意思是不包含@Activate註解事先配置的匹配規則,只在這次給定的鍵從URL配置對象得到的擴展點別名集合中進行匹配。

自動激活擴展點比較繞的就是會有兩種匹配邏輯。把他們割裂看起來會好理解一點。我我的感受這種寫法雖然會讓配置時少配置一些參數,可是難以一眼就判斷出到底是哪些擴展點是激活的。

默認擴展點

默認擴展點的加載咱們從加載普通擴展點時的那個邏輯分支繼續追蹤

//ExtensionLoader#cacheDefaultExtensionName
private String cachedDefaultName;

private void cacheDefaultExtensionName() {
    // type就是擴展點接口的Class文件
    // 以上面demo爲例,則type = com.wanglaomo.playground.dubbo.provider.spi.Transporter
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    // SPI註解標註不能爲空
    if (defaultAnnotation != null) {
        // @SPI註解的value熟悉
        String value = defaultAnnotation.value();
        if ((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));
            }
            // 若是value不爲空且只有一個,則將默認擴展點別名保存下來
            if (names.length == 1) {
                cachedDefaultName = names[0];
            }
        }
    }
}
複製代碼

從上面所知,獲取默認擴展點別名是在加載全部擴展點的Class文件時,順帶收集的,並且只能有一個。由於loadExtensionClasses幾乎是全部方法的前置方法,因此在獲取默認默認擴展點時,cachedDefaultName確定已經被賦值。下面來看看獲取默認擴展點實例的邏輯。

public T getDefaultExtension() {
     // 確保cachedDefaultName必定已被收集過
     getExtensionClasses();
     // cachedDefaultName爲空和爲"true"時返回空
     if (StringUtils.isBlank(cachedDefaultName) || "true".equals(cachedDefaultName)) {
         return null;
     }
     // 繼續得到普通擴展點實例的流程
     return getExtension(cachedDefaultName);
 }
複製代碼

默認擴展點的邏輯比較簡單,就是事先配置在註解中,而後獲取。

自動包裝擴展點

自動包裝擴展點在外面上面的Demo中並無包含,由於我沒有找到比較適合的例子來代表它的做用。可是自動包裝也是很重要的,是一個很好玩的特性,Dubbo官方對於Wrapper類的介紹爲:

  • 自動包裝擴展點的 Wrapper 類。ExtensionLoader 在加載擴展點時,若是加載到的擴展點有拷貝構造函數,則斷定爲擴展點 Wrapper 類。

  • Wrapper 類一樣實現了擴展點接口,可是 Wrapper 不是擴展點的真正實現。它的用途主要是用於從 ExtensionLoader 返回擴展點時,包裝在真正的擴展點實現外。即從 ExtensionLoader 中返回的其實是 Wrapper 類的實例,Wrapper 持有了實際的擴展點實現類。

    擴展點的 Wrapper 類能夠有多個,也能夠根據須要新增。

    經過 Wrapper 類能夠把全部擴展點公共邏輯移至 Wrapper 中。新加的 Wrapper 在全部的擴展點上添加了邏輯,有些相似 AOP,即 Wrapper 代理了擴展點。

  • AOP 類都命名爲 XxxWrapper

  • 儘可能採用 AOP 實現擴展點的通用行爲,而不要用基類,好比負載均衡以前的 isAvailable 檢查,它是獨立於負載均衡以外的,不須要檢查的是URL參數關閉。

因此說,咱們能夠把Wrapper類看做是Dubbo對於擴展點AOP的簡單實現,在Dubbo內部大量的使用了該特性,好比ProtocolFilterWrapper

public class ProtocolFilterWrapper implements Protocol {

    private final Protocol protocol;
    
    private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
        Invoker<T> last = invoker;
        List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);

        if (!filters.isEmpty()) {
            for (int i = filters.size() - 1; i >= 0; i--) {
                final Filter filter = filters.get(i);
                // 組成責任鏈模式的調用
                // ... 省略
            }
        }

        return new CallbackRegistrationInvoker<>(last, filters);
    }
}
複製代碼

經過這個例子咱們能夠看到,利用Wrapper類的特性,咱們能夠將某個擴展點的方法的調用包裝起來,在方法調用的先後加上咱們本身的邏輯,如上面就造成了一種責任鏈模式的攔截器調用。

理解了Wrapper的做用後,就很好理解自動包裝相關的代碼了。對於SPI來講,須要作的是給Wrapper類自動注入真正擴展點類的實現。讓咱們從以前的那個邏輯分支繼續開始。

// ExtensionLoader#loadClass
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        // ... 省略
    } else if (isWrapperClass(clazz)) {
        cacheWrapperClass(clazz);
    } else {
        // ... 省略
    }
}

private boolean isWrapperClass(Class<?> clazz) {
    try {
        // 若是Class包含一個有且只有擴展點接口參數的方法,則爲自動擴展實現類
        clazz.getConstructor(type);
        return true;
    } catch (NoSuchMethodException e) {
        return false;
    }
}

private void cacheWrapperClass(Class<?> clazz) {
    if (cachedWrapperClasses == null) {
        cachedWrapperClasses = new ConcurrentHashSet<>();
    }
    // 收集全部該擴展點的自動包裝擴展點Class
    cachedWrapperClasses.add(clazz);
}
複製代碼

比較簡單,仍是收集。下面繼續看組裝自動包裝擴展點的邏輯:

// ExtensionLoader#createExtension
private T createExtension(String name) {
    // ... 省略
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
	    // ... 省略
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (CollectionUtils.isNotEmpty(wrapperClasses)) {
            for (Class<?> wrapperClass : wrapperClasses) {
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
    }
    // ... 省略
}
複製代碼

當咱們真正的擴展點實現類被建立出來後,會進行自動包裝類的實體注入。經過調用相應的構造方法實現。

我我的感受自動包裝是一個自動注入的特殊狀況,屬於擴展點實現之間的注入。算是個AOP的一種簡易實現吧。

結語

這篇文章沒想到會寫的這麼長,徹底梳理完後,Dubbo的SPI機制仍是比較清晰的。Dubbo依託於微內核+插件的設計機制的確擴展性很好,並且實現並不複雜,四種特殊的擴展點也都頗有特色並且頗有用,其中有不少有意思的設計(延遲加載、類aop的簡易模型、默認生成的自適應擴展點...)值得好好再品一下。若是文章中有什麼問題,歡迎指正。

相關文章
相關標籤/搜索