Motan_SPI 插件扩展机制

本贴最后更新于 1677 天前,其中的信息可能已经渤澥桑田

开源推荐

推荐一款一站式性能监控工具(开源项目)

Pepper-Metrics 是跟一位同事一起开发的开源组件,主要功能是通过比较轻量的方式与常用开源组件(jedis/mybatis/motan/dubbo/servlet)集成,收集并计算 metrics,并支持输出到日志及转换成多种时序数据库兼容数据格式,配套的 grafana dashboard 友好的进行展示。项目当中原理文档齐全,且全部基于 SPI 设计的可扩展式架构,方便的开发新插件。另有一个基于 docker-compose 的独立 demo 项目可以快速启动一套 demo 示例查看效果 https://github.com/zrbcool/pepper-metrics-demo。如果大家觉得有用的话,麻烦给个 star,也欢迎大家参与开发,谢谢:)


进入正题...

Motan 系列文章


0 Motan 中的 SPI

Motan 的 SPI 与 Dubbo 的 SPI 类似,它在 Java 原生 SPI 思想的基础上做了优化,并且与 Java 原生 SPI 的使用方式很相似。

介绍 Java 原生 SPI 的相关文章有很多,这里不再赘述。下面主要介绍一下 Motan 中的 SPI 机制,先从使用说起。

0.1 SPI 的使用

下面以实现一个 Filter 的扩展为例,说说他咋用。

Filter 的作用是在 Provider 端接收请求或 Consumer 端发送请求时进行拦截,做一些特别的事情。下面从 Consumer 端的视角,做一个计算单次请求响应时间的统计需求。

首先需要写一个类,实现 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 配置 filter

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 机制。

  • motan
    5 引用 • 2 回帖
  • SPI

    Service Provider Interface

    12 引用 • 2 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3167 引用 • 8207 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • TonniZhao

    会优先尝试从单例集合 singletonInstances 中获取,如果获取不到

  • 其他回帖
  • TonniZhao

    获取单例时