1.简介

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。接下来,我们先来了解一下 Java SPI 与 Dubbo SPI 的用法,然后再来分析 Dubbo SPI 的源码。

需要特别说明的是,本篇文章以及本系列其他文章所分析的源码版本均为 dubbo-2.6.4。因此大家在阅读文章的过程中,需注意将代码版本切换到 dubbo-2.6.4 tag 上。

2.SPI 示例

2.1 Java SPI 示例

前面简单介绍了 SPI 机制的原理,本节通过一个示例演示 Java SPI 的使用方法。首先,我们定义一个接口,名称为 Robot

  1. public interface Robot {
  2. void sayHello();
  3. }

接下来定义两个实现类,分别为 OptimusPrimeBumblebee

  1. public class OptimusPrime implements Robot {
  2. @Override
  3. public void sayHello() {
  4. System.out.println("Hello, I am Optimus Prime.");
  5. }
  6. }
  7. public class Bumblebee implements Robot {
  8. @Override
  9. public void sayHello() {
  10. System.out.println("Hello, I am Bumblebee.");
  11. }
  12. }

接下来 META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 org.apache.spi.Robot。文件内容为实现类的全限定的类名,如下:

  1. org.apache.spi.OptimusPrime
  2. org.apache.spi.Bumblebee

做好所需的准备工作,接下来编写代码进行测试。

  1. public class JavaSPITest {
  2. @Test
  3. public void sayHello() throws Exception {
  4. ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
  5. System.out.println("Java SPI");
  6. serviceLoader.forEach(Robot::sayHello);
  7. }
  8. }

最后来看一下测试结果,如下:

「1」Dubbo SPI👍 - 图1

从测试结果可以看出,我们的两个实现类被成功的加载,并输出了相应的内容。关于 Java SPI 的演示先到这里,接下来演示 Dubbo SPI。

2.2 Dubbo SPI 示例

Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容如下。

  1. optimusPrime = org.apache.spi.OptimusPrime
  2. bumblebee = org.apache.spi.Bumblebee

与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外,在测试 Dubbo SPI 时,需要在 Robot 接口上标注 @SPI 注解。下面来演示 Dubbo SPI 的用法:

  1. public class DubboSPITest {
  2. @Test
  3. public void sayHello() throws Exception {
  4. ExtensionLoader<Robot> extensionLoader =
  5. ExtensionLoader.getExtensionLoader(Robot.class);
  6. Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
  7. optimusPrime.sayHello();
  8. Robot bumblebee = extensionLoader.getExtension("bumblebee");
  9. bumblebee.sayHello();
  10. }
  11. }

测试结果如下:

「1」Dubbo SPI👍 - 图2

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

3. Dubbo SPI 源码分析

上面简单演示了 Dubbo SPI 的使用方法。我们首先通过 ExtensionLoadergetExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoadergetExtension 方法获取拓展类对象。这其中,getExtensionLoader 方法用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例。该方法的逻辑比较简单,本章就不进行分析了。下面我们从 ExtensionLoadergetExtension 方法作为入口,对拓展类对象的获取过程进行详细的分析。

  1. public T getExtension(String name) {
  2. if (name == null || name.length() == 0)
  3. throw new IllegalArgumentException("Extension name == null");
  4. if ("true".equals(name)) {
  5. // 获取默认的拓展实现类
  6. return getDefaultExtension();
  7. }
  8. // Holder,顾名思义,用于持有目标对象
  9. Holder<Object> holder = cachedInstances.get(name);
  10. if (holder == null) {
  11. cachedInstances.putIfAbsent(name, new Holder<Object>());
  12. holder = cachedInstances.get(name);
  13. }
  14. Object instance = holder.get();
  15. // 双重检查
  16. if (instance == null) {
  17. synchronized (holder) {
  18. instance = holder.get();
  19. if (instance == null) {
  20. // 创建拓展实例
  21. instance = createExtension(name);
  22. // 设置实例到 holder 中
  23. holder.set(instance);
  24. }
  25. }
  26. }
  27. return (T) instance;
  28. }

上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。下面我们来看一下创建拓展对象的过程是怎样的。

  1. private T createExtension(String name) {
  2. // 从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的映射关系表
  3. Class<?> clazz = getExtensionClasses().get(name);
  4. if (clazz == null) {
  5. throw findException(name);
  6. }
  7. try {
  8. T instance = (T) EXTENSION_INSTANCES.get(clazz);
  9. if (instance == null) {
  10. // 通过反射创建实例
  11. EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
  12. instance = (T) EXTENSION_INSTANCES.get(clazz);
  13. }
  14. // 向实例中注入依赖
  15. injectExtension(instance);
  16. Set<Class<?>> wrapperClasses = cachedWrapperClasses;
  17. if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
  18. // 循环创建 Wrapper 实例
  19. for (Class<?> wrapperClass : wrapperClasses) {
  20. // 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。
  21. // 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
  22. instance = injectExtension(
  23. (T) wrapperClass.getConstructor(type).newInstance(instance));
  24. }
  25. }
  26. return instance;
  27. } catch (Throwable t) {
  28. throw new IllegalStateException("...");
  29. }
  30. }

createExtension 方法的逻辑稍复杂一下,包含了如下的步骤:

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

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

3.1 获取所有的拓展类

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

  1. private Map<String, Class<?>> getExtensionClasses() {
  2. // 从缓存中获取已加载的拓展类
  3. Map<String, Class<?>> classes = cachedClasses.get();
  4. // 双重检查
  5. if (classes == null) {
  6. synchronized (cachedClasses) {
  7. classes = cachedClasses.get();
  8. if (classes == null) {
  9. // 加载拓展类
  10. classes = loadExtensionClasses();
  11. cachedClasses.set(classes);
  12. }
  13. }
  14. }
  15. return classes;
  16. }

这里也是先检查缓存,若缓存未命中,则通过 synchronized 加锁。加锁后再次检查缓存,并判空。此时如果 classes 仍为 null,则通过 loadExtensionClasses 加载拓展类。下面分析 loadExtensionClasses 方法的逻辑。

  1. private Map<String, Class<?>> loadExtensionClasses() {
  2. // 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的
  3. final SPI defaultAnnotation = type.getAnnotation(SPI.class);
  4. if (defaultAnnotation != null) {
  5. String value = defaultAnnotation.value();
  6. if ((value = value.trim()).length() > 0) {
  7. // 对 SPI 注解内容进行切分
  8. String[] names = NAME_SEPARATOR.split(value);
  9. // 检测 SPI 注解内容是否合法,不合法则抛出异常
  10. if (names.length > 1) {
  11. throw new IllegalStateException("more than 1 default extension name on extension...");
  12. }
  13. // 设置默认名称,参考 getDefaultExtension 方法
  14. if (names.length == 1) {
  15. cachedDefaultName = names[0];
  16. }
  17. }
  18. }
  19. Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
  20. // 加载指定文件夹下的配置文件
  21. loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
  22. loadDirectory(extensionClasses, DUBBO_DIRECTORY);
  23. loadDirectory(extensionClasses, SERVICES_DIRECTORY);
  24. return extensionClasses;
  25. }

loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情。

  1. private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
  2. // fileName = 文件夹路径 + type 全限定名
  3. String fileName = dir + type.getName();
  4. try {
  5. Enumeration<java.net.URL> urls;
  6. ClassLoader classLoader = findClassLoader();
  7. // 根据文件名加载所有的同名文件
  8. if (classLoader != null) {
  9. urls = classLoader.getResources(fileName);
  10. } else {
  11. urls = ClassLoader.getSystemResources(fileName);
  12. }
  13. if (urls != null) {
  14. while (urls.hasMoreElements()) {
  15. java.net.URL resourceURL = urls.nextElement();
  16. // 加载资源
  17. loadResource(extensionClasses, classLoader, resourceURL);
  18. }
  19. }
  20. } catch (Throwable t) {
  21. logger.error("...");
  22. }
  23. }

loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现。

  1. private void loadResource(Map<String, Class<?>> extensionClasses,
  2. ClassLoader classLoader, java.net.URL resourceURL) {
  3. try {
  4. BufferedReader reader = new BufferedReader(
  5. new InputStreamReader(resourceURL.openStream(), "utf-8"));
  6. try {
  7. String line;
  8. // 按行读取配置内容
  9. while ((line = reader.readLine()) != null) {
  10. // 定位 # 字符
  11. final int ci = line.indexOf('#');
  12. if (ci >= 0) {
  13. // 截取 # 之前的字符串,# 之后的内容为注释,需要忽略
  14. line = line.substring(0, ci);
  15. }
  16. line = line.trim();
  17. if (line.length() > 0) {
  18. try {
  19. String name = null;
  20. int i = line.indexOf('=');
  21. if (i > 0) {
  22. // 以等于号 = 为界,截取键与值
  23. name = line.substring(0, i).trim();
  24. line = line.substring(i + 1).trim();
  25. }
  26. if (line.length() > 0) {
  27. // 加载类,并通过 loadClass 方法对类进行缓存
  28. loadClass(extensionClasses, resourceURL,
  29. Class.forName(line, true, classLoader), name);
  30. }
  31. } catch (Throwable t) {
  32. IllegalStateException e = new IllegalStateException("Failed to load extension class...");
  33. }
  34. }
  35. }
  36. } finally {
  37. reader.close();
  38. }
  39. } catch (Throwable t) {
  40. logger.error("Exception when load extension class...");
  41. }
  42. }

loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于主要用于操作缓存,该方法的逻辑如下:

  1. private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL,
  2. Class<?> clazz, String name) throws NoSuchMethodException {
  3. if (!type.isAssignableFrom(clazz)) {
  4. throw new IllegalStateException("...");
  5. }
  6. // 检测目标类上是否有 Adaptive 注解
  7. if (clazz.isAnnotationPresent(Adaptive.class)) {
  8. if (cachedAdaptiveClass == null) {
  9. // 设置 cachedAdaptiveClass缓存
  10. cachedAdaptiveClass = clazz;
  11. } else if (!cachedAdaptiveClass.equals(clazz)) {
  12. throw new IllegalStateException("...");
  13. }
  14. // 检测 clazz 是否是 Wrapper 类型
  15. } else if (isWrapperClass(clazz)) {
  16. Set<Class<?>> wrappers = cachedWrapperClasses;
  17. if (wrappers == null) {
  18. cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
  19. wrappers = cachedWrapperClasses;
  20. }
  21. // 存储 clazz 到 cachedWrapperClasses 缓存中
  22. wrappers.add(clazz);
  23. // 程序进入此分支,表明 clazz 是一个普通的拓展类
  24. } else {
  25. // 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常
  26. clazz.getConstructor();
  27. if (name == null || name.length() == 0) {
  28. // 如果 name 为空,则尝试从 Extension 注解中获取 name,或使用小写的类名作为 name
  29. name = findAnnotationName(clazz);
  30. if (name.length() == 0) {
  31. throw new IllegalStateException("...");
  32. }
  33. }
  34. // 切分 name
  35. String[] names = NAME_SEPARATOR.split(name);
  36. if (names != null && names.length > 0) {
  37. Activate activate = clazz.getAnnotation(Activate.class);
  38. if (activate != null) {
  39. // 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键,
  40. // 存储 name 到 Activate 注解对象的映射关系
  41. cachedActivates.put(names[0], activate);
  42. }
  43. for (String n : names) {
  44. if (!cachedNames.containsKey(clazz)) {
  45. // 存储 Class 到名称的映射关系
  46. cachedNames.put(clazz, n);
  47. }
  48. Class<?> c = extensionClasses.get(n);
  49. if (c == null) {
  50. // 存储名称到 Class 的映射关系
  51. extensionClasses.put(n, clazz);
  52. } else if (c != clazz) {
  53. throw new IllegalStateException("...");
  54. }
  55. }
  56. }
  57. }
  58. }

如上,loadClass 方法操作了不同的缓存,比如 cachedAdaptiveClasscachedWrapperClassescachedNames 等等。除此之外,该方法没有其他什么逻辑了。

到此,关于缓存类加载的过程就分析完了。整个过程没什么特别复杂的地方,大家按部就班的分析即可,不懂的地方可以调试一下。接下来,我们来聊聊 Dubbo IOC 方面的内容。

3.2 Dubbo IOC

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

  1. private T injectExtension(T instance) {
  2. try {
  3. if (objectFactory != null) {
  4. // 遍历目标类的所有方法
  5. for (Method method : instance.getClass().getMethods()) {
  6. // 检测方法是否以 set 开头,且方法仅有一个参数,且方法访问级别为 public
  7. if (method.getName().startsWith("set")
  8. && method.getParameterTypes().length == 1
  9. && Modifier.isPublic(method.getModifiers())) {
  10. // 获取 setter 方法参数类型
  11. Class<?> pt = method.getParameterTypes()[0];
  12. try {
  13. // 获取属性名,比如 setName 方法对应属性名 name
  14. String property = method.getName().length() > 3 ?
  15. method.getName().substring(3, 4).toLowerCase() +
  16. method.getName().substring(4) : "";
  17. // 从 ObjectFactory 中获取依赖对象
  18. Object object = objectFactory.getExtension(pt, property);
  19. if (object != null) {
  20. // 通过反射调用 setter 方法设置依赖
  21. method.invoke(instance, object);
  22. }
  23. } catch (Exception e) {
  24. logger.error("fail to inject via method...");
  25. }
  26. }
  27. }
  28. }
  29. } catch (Exception e) {
  30. logger.error(e.getMessage(), e);
  31. }
  32. return instance;
  33. }

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

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

4.总结

本篇文章简单分别介绍了 Java SPI 与 Dubbo SPI 用法,并对 Dubbo SPI 的加载拓展类的过程进行了分析。另外,在 Dubbo SPI 中还有一块重要的逻辑这里没有进行分析,即 Dubbo SPI 的扩展点自适应机制。该机制的逻辑较为复杂,我们将会在下一篇文章中进行详细的分析。

好了,本篇文章就先到这里了。如果文章中有错误不妥之处,欢迎大家提 issue 进行反馈,或者提 pull request 进行修正。让我们携手共建 Dubbo 社区。