Dubbo源码解析(2)SPI原理

2020-03-31 17:05:43 浏览数 (1)

本篇文章是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机制来加载扩展类的流程就梳理完毕了

0 人点赞