前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Dubbo源码解析(2)SPI原理

Dubbo源码解析(2)SPI原理

作者头像
Java学习录
发布2020-03-31 17:05:43
3370
发布2020-03-31 17:05:43
举报
文章被收录于专栏:Java学习录Java学习录

本篇文章是Dubbo源码解析系列文章的第二篇,本系列文章分析基于Dubbo官方代码2.7.5-release版本,本篇文章主要分析Dubbo中是如何使用SPI机制来加载扩展类的

在阅读本文之前,你需要阅读如下文章:

  1. Dubbo源码解析(1)概览

SPI术语

什么是SPI

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能

Dubbo中的SPI约定

首先我们需要明确一点,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强

我们先来看一下其中涉及到一些名词

扩展点

Dubbo 中被 @SPI 注解的 Interface 为一个扩展点

扩展点约定

在 META-INF/services/* 、META-INF/dubbo/、 META-INF/dubbo/internal/, 这些路径下定义的文件名称为扩展点接口的全类名 , 文件中以键值对的方式配置扩展点的扩展实现。例如文件 META-INF/dubbo/internal/org.apache.dubbo.common.extension.ext1.SimpleExt 中定义的扩展 :

代码语言:javascript
复制
impl1=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl1
impl2=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl2
  impl3=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl3

这个文件代表了什么呢?org.apache.dubbo.common.extension.ext1.SimpleExt这个接口有三个可选的实现类:SimpleExtImpl1SimpleExtImpl2SimpleExtImpl3

默认适应扩展

如果我们把@SPI("impl1")注解放到SimpleExt这个接口上,就代表着这个接口的缺省实现就是文件中impl1属性所对应的那个实现类SimpleExtImpl1

除了这种方式,如果SimpleExtImpl1类上被标注了注解@Adaptive

它同样可以被代表接口的默认实现

条件适应扩展

@Activate 注解在扩展点的实现类上 ,表示了一个扩展类被获取到的的条件,符合条件就被获取,不符合条件就不获取 ,过滤条件包含group、 value

方法适应扩展

刚才说到@Adaptive注解在注解在类上,这个类就是缺省的适配扩展,除了这个用法,这个注解还可以标注在方法上。 当这个注解标注在方法上时, dubbo会根据在运行时通过传入的 URL 类型的参数或者内部含有获取 URL 方法的参数 ,从URL中获取到要使用的扩展类的名称 ,再去根据名称加载对应的扩展实例 ,用这个扩展实例对象调用相同的方法

源码解析

注意,本文不会分析@Activate@Adaptive相关的内容,大家看完本文可以自行研究

示例代码

源码分析使用的示例代码在dubbo-common项目中的ExtensionLoaderTest类的test_getExtension方法

代码语言:javascript
复制
   @Test
    public void test_getExtension() throws Exception {
        assertTrue(getExtensionLoader(SimpleExt.class).getExtension("impl1") instanceof SimpleExtImpl1);
        assertTrue(getExtensionLoader(SimpleExt.class).getExtension("impl2") instanceof SimpleExtImpl2);
    }

不去分析源码我们可以大概猜一下这两行代码,大致就是加载SimpleExt接口的两个实现类,这个接口上方提到过,我们先看下这个接口的内容

代码语言:javascript
复制
@SPI("impl1")
public interface SimpleExt {
    // @Adaptive example, do not specify a explicit key.
    @Adaptive
    String echo(URL url, String s);

    @Adaptive({"key1", "key2"})
    String yell(URL url, String s);

    // no @Adaptive
    String bang(URL url, int i);
}
ExtensionLoader初始化

现在我们开始分析源码

我们首先来看下getExtensionLoader方法

代码语言:javascript
复制
    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!");
        }
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type (" + type +
                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        }

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

逻辑比较简单

  1. 判断接口的合法性,传入的接口的类型是否为空,是否是个接口,是否包含SPI注解
  2. 从缓存中获取与拓展类对应的ExtensionLoader对象,若缓存未命中,则创建一个新的实例
SPI核心

ExtensionLoader对象初始化完毕后就可以执行getExtension方法了,这个方法的参数是这个接口的实现类在配置文件中的属性名

代码语言:javascript
复制
public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    if ("true".equals(name)) {
        // 获取默认的拓展实现类
        return getDefaultExtension();
    }
    // Holder,顾名思义,用于持有目标对象
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<Object>());
        holder = cachedInstances.get(name);
    }
    Object instance = holder.get();
    // 双重检查
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // 创建拓展实例
                instance = createExtension(name);
                // 设置实例到 holder 中
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

上面代码的逻辑比较简单,使用了一个双重检查锁的模式,主要关注创建拓展对象的过程

代码语言:javascript
复制
   private T createExtension(String name) {
        // 从配置文件中加载所有的拓展类,可得到“配置项属性”到“配置类”的映射关系表
        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 (CollectionUtils.isNotEmpty(wrapperClasses)) {
            // 循环创建 Wrapper 实例
                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);
        }
    }

createExtension方法的逻辑如下:

  1. 通过 getExtensionClasses 获取所有的拓展类
  2. 通过反射创建拓展对象
  3. 向拓展对象中注入依赖
  4. 将拓展对象包裹在相应的 Wrapper 对象中

以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现,我们主要关注下第一步

获取所有的拓展类

我们在通过名称获取拓展类之前,首先需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称, 拓展类>),之后再根据拓展项名称从映射关系表中取出相应的拓展类即可。相关过程的代码如下:

代码语言:javascript
复制
    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;
    }

又一个双重检查锁,重点关注 loadExtensionClasses方法吧:

代码语言:javascript
复制
    private Map<String, Class<?>> loadExtensionClasses() {
        cacheDefaultExtensionName();

        Map<String, Class<?>> extensionClasses = new HashMap<>();
        // internal extension load from ExtensionLoader's ClassLoader first
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName(), true);
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"), true);

        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;
    }
保存默认Extension

首先关注下第一行的cacheDefaultExtensionName

代码语言:javascript
复制
    // 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation != null) {
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
            // 对 SPI 注解内容进行切分
            String[] names = NAME_SEPARATOR.split(value);
            // 检测 SPI 注解内容是否合法,不合法则抛出异常
            if (names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension...");
            }

            // 设置默认名称,参考 getDefaultExtension 方法
            if (names.length == 1) {
                cachedDefaultName = names[0];
            }
        }
    }

回忆下SimpleExt接接口,这里校验的就是这个类的@SPI注解上的信息是否合法以及获取的@SPI注解的默认值impl1,这个值会赋值给cachedDefaultName属性,这个属性主要是获取接口默认实现的方法会用到

加载配置文件

接下来就是一堆loadDirectory方法,这6行代码里面一共加载了三个文件夹下的内容,而用了6行代码的原因是Dubbo贡献给apache之前使用的包名是以com.alibaba开头的,这里是为了兼容贡献之前的版本

现在我们来看下loadDirectory方法

代码语言:javascript
复制
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
    // fileName = 文件夹路径 + type 全限定名
    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("...");
    }
}

loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源,加载到具体的资源会通过反射加载类,具体逻辑我就不详细展示了,最终就是加载的下图的这个类

接着回到最初的createExtension方法,此时已经知道了需要加载的SimpleExt这个接口的实现类为SimpleExtImpl1,下面就是通过反射实例化这个类

这样下来通过整个SPI机制来加载扩展类的流程就梳理完毕了

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java学习录 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • SPI术语
    • 什么是SPI
      • Dubbo中的SPI约定
        • 扩展点
        • 扩展点约定
        • 默认适应扩展
        • 条件适应扩展
        • 方法适应扩展
    • 源码解析
      • 示例代码
        • ExtensionLoader初始化
          • SPI核心
            • 获取所有的拓展类
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档