Motan系列-Motan的SPI插件擴展機制

開源推薦

推薦一款一站式性能監控工具(開源項目)java

Pepper-Metrics是跟一位同事一塊兒開發的開源組件,主要功能是經過比較輕量的方式與經常使用開源組件(jedis/mybatis/motan/dubbo/servlet)集成,收集並計算metrics,並支持輸出到日誌及轉換成多種時序數據庫兼容數據格式,配套的grafana dashboard友好的進行展現。項目當中原理文檔齊全,且所有基於SPI設計的可擴展式架構,方便的開發新插件。另有一個基於docker-compose的獨立demo項目能夠快速啓動一套demo示例查看效果github.com/zrbcool/pep…。若是你們以爲有用的話,麻煩給個star,也歡迎你們參與開發,謝謝:)git


進入正題...

0 Motan中的SPI

Motan的SPI與Dubbo的SPI相似,它在Java原生SPI思想的基礎上作了優化,而且與Java原生SPI的使用方式很類似。github

介紹Java原生SPI的相關文章有不少,這裏再也不贅述。下面主要介紹一下Motan中的SPI機制,先從使用提及。docker

0.1 SPI的使用

下面以實現一個Filter的擴展爲例,說說他咋用。數據庫

Filter的做用是在 Provider 端接收請求或 Consumer 端發送請求時進行攔截,作一些特別的事情。下面從 Consumer 端的視角,作一個計算單次請求響應時間的統計需求。api

首先須要寫一個類,實現 com.weibo.api.motan.filter.Filter 接口的 filter 方法。微信

@SpiMeta(name = "profiler")
public class ProfilerFilter implements Filter {
    
    @Override
    public Response filter(Caller<?> caller, Request request) {
        // 記錄開始時間
        long begin = System.nanoTime();
        try {
            final Response response = caller.call(request);
            return response;
        } finally {
            // 打印本次響應時間
            System.out.println("Time cost : " + (System.nanoTime() - begin));
        }
    }
}
複製代碼

其次,在 META-INF/services 目錄下建立名爲 com.weibo.api.motan.filter.Filter 的文件,內容以下:數據結構

# 例如:com.pepper.metrics.integration.motan.MotanProfilerFilter
#全限定名稱#.MotanProfilerFilter
複製代碼

而後給Protocol配置 filtermybatis

ProtocolConfigBean config = new ProtocolConfigBean();
config.setName("motan");
config.setMaxContentLength(1048576);
config.setFilter("profiler"); // 配置filter
return config;
複製代碼

最後在 RefererConfig 中使用這個 ProtocolConfig 便可。架構

BasicRefererConfigBean config = new BasicRefererConfigBean();
config.setProtocol("demoMotan");
// ... 省略其餘配置 ...

複製代碼

如此一來,在 Consumer 端就能夠攔截每次請求,並打印響應時間了。

接下來,繼續研究一下Motan是如何作到這件事的。

1 SPI的管理

Motan的SPI的實如今 motan-core/com/weibo/api/motan/core/extension 中。組織結構以下:

motan-core/com.weibo.api.motan.core.extension
    |-Activation:SPI的擴展功能,例如過濾、排序
    |-ActivationComparator:排序比較器
    |-ExtensionLoader:核心,主要負責SPI的掃描和加載
    |-Scope:模式枚舉,單例、多例
    |-Spi:註解,做用在接口上,代表這個接口的實現能夠經過SPI的形式加載
    |-SpiMeta:註解,做用在具體的SPI接口的實現類上,標註該擴展的名稱

複製代碼

1.1 內部管理的數據結構

private static ConcurrentMap<Class<?>, ExtensionLoader<?>> extensionLoaders = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
private ConcurrentMap<String, T> singletonInstances = null;
private ConcurrentMap<String, Class<T>> extensionClasses = null;
private Class<T> type;
private ClassLoader classLoader; // 類加載使用的ClassLoader
複製代碼

extensionLoaders 是類變量,他管理的是由 @Spi 註解標註的接口與其 ExtensionLoader 的映射,做爲全部SPI的全局管理器。

singletonInstances 維護了當前 ExtensionLoader 中的單例擴展。

extensionClasses 維護了當前 ExtensionLoader 全部擴展實例的Class對象,用於建立多例(經過class.newInstance建立)。

type 維護了當前 @Spi 註解標註的接口的 class 對象。

1.2 ExtensionLoader的初始化

Motan 中,能夠經過如下方式初始化ExtensionLoader(以上文中的Filter SPI爲例):

// 初始化 Filter 到全局管理器 `extensionLoaders` 中
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Filter.class);

複製代碼

而後咱們具體看一下 ExtensionLoader.getExtensionLoader(Filter.class) 都幹了啥。

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    // 檢查type是否爲空、type是不是接口類型、type是否被@Spi標註,檢查失敗會拋出異常
    checkInterfaceType(type);
    // 嘗試從上文提到的 `extensionLoaders` 管理器中獲取已有的ExtensionLoader
    ExtensionLoader<T> loader = (ExtensionLoader<T>) extensionLoaders.get(type);

    // 獲取失敗的話,嘗試掃描並加載指定type的擴展,並初始化之
    if (loader == null) {
        loader = initExtensionLoader(type);
    }
    return loader;
}
複製代碼

而後看看 initExtensionLoader 方法幹了啥。

// synchronized鎖控制,防止併發初始化
public static synchronized <T> ExtensionLoader<T> initExtensionLoader(Class<T> type) {
    ExtensionLoader<T> loader = (ExtensionLoader<T>) extensionLoaders.get(type);
    // 二層檢查,防止併發問題
    if (loader == null) {
        // type會賦值給實例變量 `type`,並初始化實例變量 `classLoader` 爲當前線程上線文的ClassLoader
        loader = new ExtensionLoader<T>(type);
        // 添加到全局管理器 `extensionLoaders` 中
        extensionLoaders.putIfAbsent(type, loader);

        loader = (ExtensionLoader<T>) extensionLoaders.get(type);
    }

    return loader;
}
複製代碼

至此,咱們就初始化了 Filter 接口的 ExtensionLoader,並將它託管到了 extensionLoaders 中。

1.3 SPI的掃描和加載以及獲取指定的擴展實例

Motan 是懶加載策略,當第一次獲取具體的某一擴展實例時,纔會掃描和加載全部的擴展實例。

例如,能夠經過如下方式獲取咱們上面建立的名爲 profilerFilter 接口的擴展實例。

// 初始化 Filter 到全局管理器 `extensionLoaders` 中
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Filter.class);
Filter profilterFilter = extensionLoader.getExtension("profiler");
複製代碼

Filter 接口的 ExtensionLoader 其實是在第一次 extensionLoader.getExtension("profiler") 時完成的。下面具體看一下 getExtension 方法幹了啥。

public T getExtension(String name) {
    // Notes:就是經過這個checkInit方法掃描和加載的
    checkInit();
    // .. 暫時省略 ..
}
複製代碼

繼續研究 checkInit 方法。

private volatile boolean init = false;
private void checkInit() {
    // 用init標記,只初始化一次
    if (!init) {
        loadExtensionClasses();
    }
}

private static final String PREFIX = "META-INF/services/";
private synchronized void loadExtensionClasses() {
    if (init) {
        return;
    }
    // 掃描和加載
    extensionClasses = loadExtensionClasses(PREFIX);
    singletonInstances = new ConcurrentHashMap<String, T>();

    init = true;
}
複製代碼

loadExtensionClasses 方法會掃描 META-INF/services/ 下的全部文件,並解析文件內容,它會調用 loadClass 方法,該方法實現以下:

// classNames 就是每一個文件中的具體擴展實現的全限定名稱。
private ConcurrentMap<String, Class<T>> loadClass(List<String> classNames) {
    ConcurrentMap<String, Class<T>> map = new ConcurrentHashMap<String, Class<T>>();

    for (String className : classNames) {
        try {
            Class<T> clz;
            if (classLoader == null) {
                clz = (Class<T>) Class.forName(className);
            } else {
                clz = (Class<T>) Class.forName(className, true, classLoader);
            }

            checkExtensionType(clz);
            // 獲取 @SpiMeta 註解聲明的名稱
            String spiName = getSpiName(clz);

            if (map.containsKey(spiName)) {
                failThrows(clz, ":Error spiName already exist " + spiName);
            } else {
                map.put(spiName, clz);
            }
        } catch (Exception e) {
            failLog(type, "Error load spi class", e);
        }
    }

    return map;
}
複製代碼

這個方法作的事情就是獲取全部合法的擴展的class。最終其返回值會賦值給實例變量 extensionClasses,至此完成了掃描和加載工做。

PS:由上可知,extensionClasses的K-V是具體擴展實現的 @SpiMeta 名稱和對應class的映射。以上文的 ProfilerFilter 爲例來講,KEY=profiler,VALUE=ProfilerFilter.class

1.4 獲取具體的SPI擴展實現

繼續看剛纔 getExtension 方法中省略的部分。

public T getExtension(String name) {
    checkInit();

    if (name == null) {
        return null;
    }

    try {
        Spi spi = type.getAnnotation(Spi.class);

        // 獲取單例
        if (spi.scope() == Scope.SINGLETON) {
            return getSingletonInstance(name);
        } else {
            // 獲取多例
            Class<T> clz = extensionClasses.get(name);

            if (clz == null) {
                return null;
            }

            return clz.newInstance();
        }
    } catch (Exception e) {
        failThrows(type, "Error when getExtension " + name, e);
    }

    return null;
}
複製代碼

獲取多例的狀況很容易,直接從前面加載好的 extensionClasses 中獲取,若是獲取到就 newInstance() 一個新的實例。

下面看下單例的狀況:getSingletonInstance 方法。

private T getSingletonInstance(String name) throws InstantiationException, IllegalAccessException {
    T obj = singletonInstances.get(name);

    if (obj != null) {
        return obj;
    }

    Class<T> clz = extensionClasses.get(name);

    if (clz == null) {
        return null;
    }

    synchronized (singletonInstances) {
        obj = singletonInstances.get(name);
        if (obj != null) {
            return obj;
        }

        obj = clz.newInstance();
        singletonInstances.put(name, obj);
    }

    return obj;
}
複製代碼

獲取單例時,會優先嚐試從單例集合 singletonInstances 中獲取,若是獲取不到,說明這個單例實例還沒添加到單例集合中(或沒有對應名稱的具體實例),而後會嘗試從 extensionClasses 中獲取,若是還獲取不到,就是真沒有了,若是獲取到,會new一個instance到單例集合中。

以上就是 Motan 獲取具體SPI擴展實現的方式。

以上即是 Motan 的 SPI 機制。


歡迎關注個人微信公衆號

公衆號
相關文章
相關標籤/搜索