Dubbo 源码解析 -- SPI 机制详解(一)

699 阅读9分钟

Dubbo 源码解析 -- SPI 机制详解(一)

什么是 SPI

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

  • 在 JDK 中有一个查找服务实现的工具类 java.util.ServiceLoader它实现了根据 classpath 下的 META-INF/services/ 目录中创建的以服务接口命名的配置文件,并根据这个配置文件里实现类的全限定名进行类的加载实例化。
  • Dubbo 中并没有使用原生的 Java SPI 机制,而是对其进行了增强1

Dubbo 中 SPI 实现方法

我们先看一个简单的 Dubbo 中 SPI 实现的实例

  • 先定义一个接口 SpiScanner
@SPI
public interface SpiScanner {
    void sayHello();
}
  • 接下来定义两个实现类,分别为 SpiScannerASpiScannerB
public class SpiScannerA implements SpiScanner {
    
    @Override
    public void sayHello() {
        System.out.println("Hello, I am SpiScannerA.");
    }
}

public class SpiScannerB implements SpiScanner {

    @Override
    public void sayHello() {
        System.out.println("Hello, I am SpiScannerB.");
    }
}
  • 在 META-INF/dubbo 中建立一个以 SpiScanner 全限定名命名的文件,并在其中加上两行配置
SpiScannerA = com.dubbo.test.spi.SpiScannerA
SpiScannerB = com.dubbo.test.spi.SpiScannerB

了解过 Java SPI 机制的朋友应该看出不同了,在 Dubbo 中关于实现类的配置是以键值对的方式进行配置的,这样可以按需加载制定的实现类。

public class DubboSPITest {

    @Test
    public void sayHello() throws Exception {
        ExtensionLoader<SpiScanner> extensionLoader = 
            ExtensionLoader.getExtensionLoader(SpiScanner.class);
        SpiScanner spiScannerA = extensionLoader.getExtension("SpiScannerA");
        spiScannerA.sayHello();
        SpiScanner spiScannerB = extensionLoader.getExtension("SpiScannerB");
        spiScannerB.sayHello();
    }
}

测试结果如下:

测试结果

Dubbo SPI 除了支持按需加载接口实现类,还增加了 IOC 和 AOP 等特性,这些特性将会在接下来的源码分析章节中一一进行介绍。


Dubbo SPI 源码分析

在 Dubbo 中 SPI 机制的主要实现逻辑封装在了类 org.apache.dubbo.common.extension.ExtensionLoader 中,在使用时我们先通过 ExtensionLoadergetExtensionLoader 方法获取一个制定的 ExtensionLoader 实例,然后再使用 ExtensionLoadergetExtension 方法获取实现类对象。

  • getExtensionLoader 方法源码:
@SuppressWarnings("unchecked")
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;
}
  • getExtension 方法源码
@SuppressWarnings("unchecked")
public T getExtension(String name) {
    return getExtension(name, true);
}

public T getExtension(String name, boolean wrap) {
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    final Holder<Object> holder = getOrCreateHolder(name);
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                instance = createExtension(name, wrap);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

可以看到在获取接口实现类的方式逻辑比较简单,同样是先检查缓,若缓存未命中则创建扩展对象。下面我们来看下创建扩展对象的过程

  • createExtension 方法源码
@SuppressWarnings("unchecked")
private T createExtension(String name, boolean wrap) {
  // 从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的映射关系表
    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);

      // 是否需要判断包装类
        if (wrap) {

            List<Class<?>> wrapperClassesList = new ArrayList<>();
            if (cachedWrapperClasses != null) {
                wrapperClassesList.addAll(cachedWrapperClasses);
                wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                Collections.reverse(wrapperClassesList);
            }

            if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
              // 循环创建 Wrapper 实例
                for (Class<?> wrapperClass : wrapperClassesList) {
                    Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
                    if (wrapper == null
                            || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
                      // 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。
                // 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
                        instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                    }
                }
            }
        }

      // 若接口继承了 Lifecycle,则调用 initialize() 方法
        initExtension(instance);
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                type + ") couldn't be instantiated: " + t.getMessage(), t);
    }
}

在这一步逻辑稍微复杂一些,简单来说包含了以下的几个步骤

  1. 通过 getExtensionClasses 方法获取到配置文件中所有实例化类
  2. 通过反射创建拓展对象
  3. 向拓展对象中注入依赖
  4. 若需要判断包装类则将拓展对象包裹在相应的 Wrapper 对象中
  5. 若接口继承了 Lifecycle,则调用 initialize() 方法

以上的几个步骤中第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。在接下来的章节中,将会重点分析 getExtensionClasses 方法的逻辑,以及简单介绍 Dubbo IOC 的具体实现。

获取接口的全部实现类

Dubbo SPI 在通过名称获取全部的实现类之前,首先会根据配置文件解析出实现类项的名称和实现类的关系映射表(Map<String, Class<?>>),之后再根据实现类的项名称取出对应的实现类。

  • getExtensionClasses 方法源码
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 方法源码
private Map<String, Class<?>> loadExtensionClasses() {
		// 缓存默认的实现类对象名(标注在 @SPI 注解上)
    cacheDefaultExtensionName();

    Map<String, Class<?>> extensionClasses = new HashMap<>();

		// 	遍历读取文件的路径策略,通过反射取出所有 LoadingStrategy 实现类,默认的有
  	// DubboExternalLoadingStrategy - META-INF/dubbo/external/
  	// DubboInternalLoadingStrategy - META-INF/dubbo/internal/
  	// DubboLoadingStrategy - META-INF/dubbo/
		// ServicesLoadingStrategy - META-INF/services/
    for (LoadingStrategy strategy : strategies) {
				// 从文件中读取,加载指定文件夹下的配置文件
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
    }

    return extensionClasses;
}

loadExtensionClasses 方法主要做了两件事情,一是对 @SPI 注解进行解析,缓存默认的实现类名,二是调用 loadDirectory 方法加载制定路径下的文件夹配置文件。

  • loadDirectory 方法源码
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,
                           boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {
		// fileName = 文件夹路径 + type 全限定名 
    String fileName = dir + type;
    try {
        Enumeration<java.net.URL> urls = null;
        ClassLoader classLoader = findClassLoader();

        // 首先尝试使用 ExtendionLoader 的 ClassLoader 加载类
        if (extensionLoaderClassLoaderFirst) {
            ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
            if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                urls = extensionLoaderClassLoader.getResources(fileName);
            }
        }
				// 根据文件名加载所有的同名文件
        if (urls == null || !urls.hasMoreElements()) {
            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, overridden, excludedPackages);
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                type + ", description file: " + fileName + ").", t);
    }
}

loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。

  • loadResource 方法源码
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
                          java.net.URL resourceURL, boolean overridden, String... excludedPackages) {
    try {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
            String line;
          // 按行读取配置内容
            while ((line = reader.readLine()) != null) {
              // 定位 # 字符
                final int ci = line.indexOf('#');
                if (ci >= 0) {
                  // 截取 # 之前的字符串,# 之后的内容为注释,需要忽略
                    line = line.substring(0, ci);
                }
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        String name = null;
                        int i = line.indexOf('=');
                        if (i > 0) {
                          // 以等于号 = 为界,截取键与值
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0 && !isExcluded(line, excludedPackages)) {
                          // 加载类,并通过 loadClass 方法对类进行缓存
                            loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name, overridden);
                        }
                    } catch (Throwable t) {
                        IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
                        exceptions.put(line, e);
                    }
                }
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                type + ", class file: " + resourceURL + ") in " + resourceURL, t);
    }
}

loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。

  • loadClass 方法源码
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                       boolean overridden) throws NoSuchMethodException {
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("Error occurred when loading extension class (interface: " +
                type + ", class line: " + clazz.getName() + "), class "
                + clazz.getName() + " is not subtype of interface.");
    }
  	// 检测目标类上是否有 Adaptive 注解
    if (clazz.isAnnotationPresent(Adaptive.class)) {
      // 设置 cachedAdaptiveClass 缓存
        cacheAdaptiveClass(clazz, overridden);
    } 
  // 检测 clazz 是否是 Wrapper 类型(检查是否是包装类)
  	else if (isWrapperClass(clazz)) {
      // 设置包装类缓存
        cacheWrapperClass(clazz);
    } else {
				// 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常
        clazz.getConstructor();
        if (StringUtils.isEmpty(name)) {
          	// 如果 name 为空,则尝试从 Extension 注解中获取 name,或使用小写的类名作为 name
            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 (ArrayUtils.isNotEmpty(names)) {
						// 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键,
            // 存储 name 到 Activate 注解对象的映射关系
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
              // 存储 Class 到名称的映射关系
                cacheName(clazz, n);
              // 存储名称到 Class 的映射关系
                saveInExtensionClass(extensionClasses, clazz, n, overridden);
            }
        }
    }
}

可以看到,loadClass 方法操作了不同的缓存,如 cacheAdaptiveClass、cacheWrapperClass、cacheActivateClass、cacheName、saveInExtensionClass。除此之外,该方法没有什么其他特殊的逻辑了。

到此,关于缓存类加载的过程就分析完了。整个过程没什么特别复杂的地方,大家按部就班的分析即可,同时这里基本也包括了 Dubbo SPI 的大部分逻辑。可以看出 Dubbo 这部分的逻辑还是比较容易理解的,Dubbo SPI 可以说是 Dubbo 可扩展性的核心。大家如果有不懂的地方可以调试一下。

Dubbo IOC

Dubbo IOC 是通过 setter 方法注入依赖。Dubbo 首先会通过反射获取到实例的所有方法,然后再遍历方法列表,检测方法名是否具有 setter 方法特征。若有,则通过 ObjectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中。

  • injectExtension 方法源码
private T injectExtension(T instance) {

    if (objectFactory == null) {
        return instance;
    }

    try {
        for (Method method : instance.getClass().getMethods()) {
          // 判断是否是 set 方法,通过参数唯一,修饰符 public,方法名是否是以 set 开头三个条件判断
            if (!isSetter(method)) {
                continue;
            }
            // 判断方法是否加注了 @DisableInject 注解
            if (method.getAnnotation(DisableInject.class) != null) {
                continue;
            }
          	// 获取方法的入参类型
            Class<?> pt = method.getParameterTypes()[0];
						// 如果入参为基本类型,则跳过
            if (ReflectUtils.isPrimitives(pt)) {
                continue;
            }

            try {
              	// 获取 set 方法的属性名,如 setAge 则返回 age
                String property = getSetterProperty(method);
              	// 通过 SPI 机制获取方法的入参对象
                Object object = objectFactory.getExtension(pt, property);
                if (object != null) {
                  // 通过反射调用 setter 方法设置依赖
                    method.invoke(instance, object);
                }
            } catch (Exception e) {
                logger.error("Failed to inject via method " + method.getName()
                        + " of interface " + type.getName() + ": " + e.getMessage(), e);
            }

        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}

在上面代码中,objectFactory 变量的类型为 AdaptiveExtensionFactory,AdaptiveExtensionFactory 内部维护了一个 ExtensionFactory 列表,用于存储其他类型的 ExtensionFactory。Dubbo 目前提供了两种 ExtensionFactory,分别是 SpiExtensionFactory 和 SpringExtensionFactory。前者用于创建自适应的拓展,后者是用于从 Spring 的 IOC 容器中获取所需的拓展。

Dubbo IOC 目前仅支持 setter 方式注入,总的来说,逻辑比较简单易懂。

总结

总的来说 Dubbo SPI 机制理解起来并不是非常复杂,这里给大家展示了一下示例并对 Dubbo SPI 加载实现类的过程做了简单的分析。有不懂的地方其实尝试下自己调试基本都可以找到答案。但 Dubbo SPI 作为 Dubbo 框架的重要组成部分还有一个非常重要的原因即 Dubbo SPI 的扩展自适应机制,该机制的逻辑比较负责,我想在下一篇文章中进行分析。

如果我在哪一部分写的不够到位或者写错了,还请在座的各位大佬提出意见。

Footnotes

  1. 本篇主要讲解 Dubbo 中的 SPI 机制,想要了解 Java 原生 SPI 的朋友请阅读其他文章。