Dubbo 擴展機制——SPI

1. 簡介

Dubbo 良好的擴展性與兩個方面密不可分,一是整個框架中針對不同的場景,恰到好處的使用了各種設計模式,二是接下來要講的加載機制。基于 Dubbo SPI 加載機制,讓整個框架的接口和具體實現類解耦,從而奠定了整個框架良好可擴展性的基礎。

2. Java SPI

在講解 Dubbo SPI 之前,我們先了解下 Java SPI 是怎么使用的。SPI 全稱 Service Provider Interface,起初是提供給廠商做插件開發的。Java SPI 使用了策略模式,一個接口多種實現。我們只聲明接口,具體的實現并不在程序中直接確定,而是由程序之外的配置掌控,用于具體實現的裝配。具體步驟如下:

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

3. Dubbo SPI

與 Java SPI 相比,Dubbo SPI 做了一定的改進和優化:

  1. JDK 標準的 SPI 會一次性實例化擴展點的所有實現,如果沒用上也加載,則浪費資源。而 Dubbo SPI 只是加載配置文件中的類,而不會立即全部初始化。
  2. 增加了對擴展 IOC 和 AOP 的支持,一個擴展可以直接 setter 注入其他擴展。

3.1 代碼示例

聲明擴展點:

package com.alibaba.dubbo.examples.spi.api;

import com.alibaba.dubbo.common.extension.SPI;

@SPI("print")
public interface PrintService {

    void printInfo();
}

聲明擴展點的實現類:

public class PrintServiceImpl implements PrintService {

    @Override
    public void printInfo() {
        System.out.println("hello,world");
    }
}

在 META-INF/dubbo/ 目錄下,新建配置文件 com.alibaba.dubbo.examples.spi.api.PrintService,內容如下:

print=com.alibaba.dubbo.examples.spi.impl.PrintServiceImpl

3.2 擴展點的配置規范

Dubbo SPI 和 Java SPI 類似,需要在 META-INF/dubbo/ 下放置對應的 SPI 配置文件,文件名稱需要命名為接口的全路徑名。配置的內容為 key=擴展點實現類的全路徑名,如果有多個實現類則使用換行符分隔。其中 key 為 Dubbo SPI 注解中傳入的參數。另外,Dubbo SPI 還兼容了 Java SPI 的配置路徑,在 Dubbo 啟動時,會默認掃描這三個目錄下的配置文件:META-INF/services/、META-INF/dubbo/、META-INF/dubbo/internal。

3.3 擴展點的特性

從 Dubbo 官方文檔中可以知道,擴展類一共包含四種特性:自動包裝、自動加載、自適應和自動激活。

1. 自動包裝
自動包裝是一種擴展類,ExtensionLoader在加載擴展時,如果發現這個擴展類包含其他擴展點實例作為構造函數的參數,則這個擴展類就會被認定為是Wrapper類,例如:

public class ProtocolFilterWrapper implements Protocol {

    private final Protocol protocol;

    public ProtocolFilterWrapper(Protocol protocol) {
        if (protocol == null) {
            throw new IllegalArgumentException("protocol == null");
        }
        this.protocol = protocol;
    }
......
}

ProtocolFilterWrapper繼承了Protocol接口,同時其構造函數中又注入了一個Protocol類型的參數。因此ProtocolFilterWrapper會被認定為Wrapper類。這是一種裝飾模式,把通用的抽象邏輯封裝或對子類進行增強,讓子類可以更加專注具體的實現。

2. 自動加載
除了在構造函數中傳入其他擴展實例,我們還經常使用setter方法設置屬性值,如果某個擴展類是另一個擴展點類的成員屬性,并且擁有setter方法,那么框架也會自動注入對應的擴展點實例。ExtensionLoader在執行擴展點初始化的時候,會自動通過setter方法注入對應的實現類。這里有個問題,如果擴展類屬性是一個接口,他有多種實現,那么具體注入哪個呢?這就涉及第三個特性——自適應。

3. 自適應
在 Dubbo SPI 中,我們使用@Adaptive注解,可以動態地通過 URL 中的參數來確定要使用哪個具體的實現類。從而解決自動加載中的實例注入問題。@Adaptive注解使用示例如下:

@SPI("netty")
public interface Transporter {

    @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
    Server bind(URL url, ChannelHandler handler) throws RemotingException;

    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    Client connect(URL url, ChannelHandler handler) throws RemotingException;

}

@Adaptive傳入兩個 Constants 中的參數,他們的值分別是"server"、“transporter”。當外部調用Transporter#bind方法時,會動態從傳入的參數"URL"中提取 key 參數“server”的 value 值,如果能匹配上某個擴展實現類則直接使用對應的實現類;如果未匹配上,則繼續通過第二個 key 參數“transporter”提取 value 的值。如果都未匹配上,則拋出異常。也就是說,如果@Adaptive中傳入了多個參數,則依次進行實現類的匹配,直到最后拋出異常。

這種動態尋找實現類的方式比較靈活,但只能激活一個具體的實現類,如果需要激活多個實現類,如 Filter 可以同時有多個過濾器;根據不同的條件,同時激活多個實現類,如何實現?這就涉及最后一個特性——自動激活。

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

3.4 擴展點注解

@SPI注解一般使用在接口上,作用是標記這個接口是一個 Dubbo SPI 接口,既是一個擴展點。注解中有一個 value 屬性,表示這個接口的默認實現類。例如上面的@SPI("netty"),我們可以看到Transporter接口使用Netty作為默認實現。

@Adaptive注解可以標注在類、接口、方法上。如果標注在接口的方法上,則可以通過參數動態的獲取實現類。方法級別注解在第一次getExtension時,會自動生成和編譯一個動態的Adaptive類,從而達到動態實現類的效果。例如:Transporter接口在 bind 和 connect 兩個方法上添加了@Adaptive注解。Dubbo 在初始化擴展點時,會自動生成一個Transporter$Adaptive類,里面會實現這兩個方法,方法里面會有一些通用的抽象邏輯,通過@Adaptive傳入的參數,找到并調用真正的實現類。當注解放在實現類上時,則整個實現類會直接作為默認實現,不再自動生成的Adaptive類。在擴展點接口的多個實現里,只能有一個實現類可以加@Adaptive注解,否則拋出異常。

3.5 ExtensionLoader 工作原理

ExtensionLoader是整個擴展機制的主要邏輯類,邏輯入口可以分為getExtensiongetAdaptiveExtensiongetActivateExtension三個,分別是獲取普通擴展類、獲取自適應擴展類、獲取自動激活擴展類。

3.5.1 getExtension 的實現原理

當調用getExtension(String name)方法時,會先檢查緩存中是否有現成的數據,沒有則調用createExtension開始創建。如果 name 為 “true”,則加載并返回默認擴展類。

在調用createExtension開始創建的過程中,也會檢查緩存中是否有配置信息,如果不存在擴展類,則會到 META-INF/services/、META-INF/dubbo/、META-INF/dubbo/internal/ 這幾路徑中讀取所有配置文件,得到對應擴展點實現類的全稱。擴展點配置信息加載過程源碼如下:

private Map<String, Class<?>> getExtensionClasses() {
        // 嘗試先從緩存中獲取配置信息
        Map<String, Class<?>> classes = cachedClasses.get();
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    // 緩存中沒有,則去配置文件加載
                    classes = loadExtensionClasses();
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }

private Map<String, Class<?>> loadExtensionClasses() {
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if (defaultAnnotation != null) {
            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));
                }
                if (names.length == 1) cachedDefaultName = names[0];
            }
        }

        Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
        loadDirectory(extensionClasses, DUBBO_DIRECTORY);
        loadDirectory(extensionClasses, SERVICES_DIRECTORY);
        return extensionClasses;
    }

檢查是否有@SPI注解。如果有,則獲取注解中的 value 值,作為擴展點默認實現。然后加載路徑下的配置文件loadDirectory

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
        String fileName = dir + type.getName();
        try {
            Enumeration<java.net.URL> urls;
            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 when load extension class(interface: " +
                    type + ", description file: " + fileName + ").", t);
        }
    }

通過getResources或者getSystemResources得到配置文件,然后遍歷并解析配置文件,得到擴展實現類,并加入緩存。加載完擴展點配置后,再通過反射獲得所有擴展實現類并緩存起來。注意,此處僅僅是把 Class 加載到 JVM 中,但并沒有做 Class 初始化。在加載 Class 文件時,會根據 Class 上的注解來判斷擴展點類型,再根據類型分類做緩存,此處邏輯在ExtensionLoader#loadClass

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
         ...
        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 if (isWrapperClass(clazz)) {
            // 如果是包裝類(Wrapper),則直接直接加入包裝擴展類的 Set 集合
            Set<Class<?>> wrappers = cachedWrapperClasses;
            if (wrappers == null) {
                cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
                wrappers = cachedWrapperClasses;
            }
            wrappers.add(clazz);
        } else {
            // 不是自適應類,也不是包裝類,只剩下普通擴展類了
            clazz.getConstructor();
            if (name == null || name.length() == 0) {
                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 (names != null && names.length > 0) {
                Activate activate = clazz.getAnnotation(Activate.class);
                if (activate != null) {
                    // 如果擴展類有 @Activate 注解,則加入自動激活類緩存
                    cachedActivates.put(names[0], activate);
                }
                for (String n : names) {
                    if (!cachedNames.containsKey(clazz)) {
                        cachedNames.put(clazz, n);
                    }
                    Class<?> c = extensionClasses.get(n);
                    if (c == null) {
                        // 加入普通擴展類緩存
                        extensionClasses.put(n, clazz);
                    } else if (c != clazz) {
                        throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
                    }
                }
            }
        }
    }

加載完畢擴展信息之后,完成了createExtension的第一步:

 private T createExtension(String name) {
        // 根據 key 獲取對應的擴展點實現類
        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);
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
                // 處理包裝擴展類
                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 + ")  could not be instantiated: " + t.getMessage(), t);
        }
    }

下一步就是根據傳入的 name 找到對應的類并通過Class.forName方法進行初始化,并為其注入依賴的其他擴展類(自動加載特性),我們看injectExtension

private T injectExtension(T instance) {
        try {
            if (objectFactory != null) {
                for (Method method : instance.getClass().getMethods()) {
                    if (method.getName().startsWith("set")
                            && method.getParameterTypes().length == 1
                            && Modifier.isPublic(method.getModifiers())) {
                       // 找到 setter 方法
                        if (method.getAnnotation(DisableInject.class) != null) {
                            continue;
                        }
                        Class<?> pt = method.getParameterTypes()[0];
                        try {
                            // 通過字符串截取,獲得小寫開頭的類名。如 setTestService,截取之后是 testService
                            String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                            // 根據 key 獲取對應的擴展點實例
                            Object object = objectFactory.getExtension(pt, property);
                            if (object != null) {
                                method.invoke(instance, object);
                            }
                        } catch (Exception e) {
                            logger.error("fail to inject via method " + method.getName()
                                    + " of interface " + type.getName() + ": " + e.getMessage(), e);
                        }
                    }
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return instance;
    }

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

當擴展類初始化之后,會檢查一次包裝擴展類Set<Class<?>> cachedWrapperClasses,查找包含與擴展點類型相同的構造函數,為其注入剛初始化的擴展類。從源碼可知,包裝類的構造函數注入也是通過injectExtension方法實現的。

3.5.2 getAdaptiveExtension 的實現原理

由之前的流程我們可以知道,getAdaptiveExtension()方法中,會為擴展點接口自動生成實現類字符串,實現類主要包含以下邏輯:為接口中每個有@Adaptive注解的方法生成默認實現(沒有注解的方法則生成空實現),每個默認實現都會從 URL 中提取@Adaptive參數值,并且以此為依據動態加載擴展點。然后框架會使用不同的編譯器,把實現類字符串編譯為自適應類并返回。

如果一個接口上既有@SPI("impl")注解,方法上又有@Adaptive("impl2")注解,那么會使用哪個 key 作為默認實現呢?其優先通過@Adaptive注解傳入的 key 去查找擴展實現類;如果沒有找到,則通過@SPI注解中 key 去查找;如果@SPI注解中沒有默認值,則會采用駝峰規則,把類名轉化為 key,再去查找。

駝峰規則:比如類名為 SimpleExt,則轉化后的 key 為 simple.ext。

3.5.3 getActivateExtension 的實現原理

由于@Activate使用場景較少,并且實現原理較為簡單,感興趣的同學自行去了解。

3.6 ExtensionFactory 的實現原理

ExtensionFactory類似于Spring中的BeanFactory,它是擴展類的 bean 工廠。我們看下它的實現類:


既然工廠接口有多個實現,那么是怎么確定使用哪個工廠實現的呢?我們可以看到AdaptiveExtensionFactory這個實現類工廠上有@Adaptive注解。因此,AdaptiveExtensionFactory會作為默認實現類。

除了AdaptiveExtensionFactory,還有SpiExtensionFactorySpringExtensionFactory兩個工廠。也就是說,我們除了可以從 Dubbo SPI 管理的容器中獲取擴展點實例,還可以從 Spring 容器獲取。

我們先看SpringExtensionFactory

public class SpringExtensionFactory implements ExtensionFactory {

    private static final Set<ApplicationContext> contexts = new ConcurrentHashSet<ApplicationContext>();

    public static void addApplicationContext(ApplicationContext context) {
        contexts.add(context);
        BeanFactoryUtils.addApplicationListener(context, shutdownHookListener);
    }
    ...
    @Override
    @SuppressWarnings("unchecked")
    public <T> T getExtension(Class<T> type, String name) {
        // 先根據名稱去獲取
        for (ApplicationContext context : contexts) {
            if (context.containsBean(name)) {
                Object bean = context.getBean(name);
                if (type.isInstance(bean)) {
                    return (T) bean;
                }
            }
        }
        // 再根據類型去獲取
        for (ApplicationContext context : contexts) {
            try {
                return context.getBean(type);
            } catch (NoUniqueBeanDefinitionException multiBeanExe) {
                logger.warn("Find more than 1 spring extensions (beans) of type " + type.getName() + ", will stop auto injection. Please make sure you have specified the concrete parameter type and there's only one extension of that type.");
            } catch (NoSuchBeanDefinitionException noBeanExe) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Error when get spring extension(bean) for type:" + type.getName(), noBeanExe);
                }
            }
        }

        logger.warn("No spring extension (bean) named:" + name + ", type:" + type.getName() + " found, stop get bean.");

        return null;
    }

}

該工廠提供了保存 Spring 上下文的靜態方法,可以把 Spring 上下文保存在 Set 集合中,當調用getExtension獲取擴展類時,會遍歷 Set 集合中的所有 Spring 上下,先根據名字依次去獲取,如果沒有獲取到,再根據類型去獲取。

那么 Spring 的上下文又是什么時候被保存的呢?我們可以通過代碼搜索得知,在ReferenceBeanServiceBean中會調用靜態方法保存 Spring 上下文,即一個服務被發布或者被引用的時候,對應的 Spring 上下文會被保存下來。

我們再看SpiExtensionFactory:

public class SpiExtensionFactory implements ExtensionFactory {

    @Override
    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;
    }

}

主要是獲取擴展點接口對應的 Adaptive 實現類。例如:某個擴展點實現類 ClassA 上有@Adaptive注解,則使用SpiExtensionFactory#getExtension會直接返回 ClassA 實例。

我們再看AdaptiveExtensionFactory:

/**
 * AdaptiveExtensionFactory
 * 該類的作用是管理其他的 ExtensionFactory
 */
@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {

    private final List<ExtensionFactory> factories;

    /**
     * 構造方法會加載其他擴展工廠
     */
    public AdaptiveExtensionFactory() {
        ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
        List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
        /*
         * 獲取 getExtensionClasses() 是不會加載被 @Adaptive 注解的實現類的
         * 即這里只是把其他兩個 ExtensionFactory 放入 factories 中
         */
        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;
    }

}

這個默認工廠在構造方法中就獲取了其他所有擴展類工廠并緩存起來,包括SpringExtensionFactorySpiExtensionFactory。被AdaptiveExtensionFactory緩存的工廠會通過TreeSet進行自然排序,SPI 排在前面,Spring 排在后面。當調用getExtension方法時,會遍歷所有的工廠,先從 SPI 容器獲取擴展類;如果沒有找到,再從 Spring 容器中查找。我們可以理解為,AdaptiveExtensionFactory持有了所有的工廠實現,它的getExtension方法只是遍歷它持有的所有工廠,最終還是調用 SPI 或者 Spring 工廠實現的getExtension方法。

4. 小結

我們沒有講擴展點的動態編譯,其實現手法跟ExtensionFactory類似,其采用含有@Adaptive注解的AdaptiveCompiler作為Compiler的默認實現,主要作用是為了管理其他Compiler(JavassistCompilerJdkCompiler)。由于Compiler接口上采用@SPI("javassist"),說明 Javassist 編譯器作為默認編譯器。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容