Java SPI 和 Dubbo SPI 的使用方式和实现细节

SPI 全称 Service Provider Interface,是一种服务发现机制。通过将接口的实现类全限定名配置在文件中,在运行时加载,以获取该接口所有的实现类,从而达到动态扩展的目的。因此,SPI 机制在开源框架中有大量的应用。

Java 提供了原生的 SPI 机制,使用者需要在 META-INF/services 文件夹下创建文件,文件名称为接口全限定名。而 Dubbo SPI 并未使用 Java SPI,而是实现了自己的 SPI 机制。在 Dubbo 中 SPI 的使用随处可见,使用者可以很容易的对相关功能进行扩展。因此 SPI 机制是学习 Dubbo 的实现前,必须要弄懂的。

接下来我们就来看看 Java SPI 和 Dubbo SPI 的使用方式和实现细节。

1.Java SPI

1.1 使用示例

定义接口如下:

public interface Animal {
    String getName();
}

定义两个实现类:

public class Lion implements Animal {

    @Override
    public String getName() {

        return "I'm Lion.";
    }
}

public class Tiger implements Animal {

    @Override
    public String getName() {

        return "I'm Tiger.";
    }
}

接下来在 META-INF/services 文件夹下创建名称为接口的全限定类名的文件:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第1张图片
image.png

文件内容为实现类的全限定类名:

com.wlm.spi.service.Lion
com.wlm.spi.service.Tiger

Java SPI 使用 ServiceLoader 加载实现类,加载方式如下:

@Test
public void javaSPITest() {

    ServiceLoader serviceLoader = ServiceLoader
        .load(Animal.class);

    Iterator iter = serviceLoader.iterator();

    while (iter.hasNext()) {

        Animal animal = iter.next();
        System.out.println(animal.getName());
    }
}

输出为:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第2张图片
image.png

1.2 源码

调用 ServiceLoader#load 会创建一个 ServiceLoader 对象,用于加载实现类:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第3张图片
image.png

在 ServiceLoader 的构造器中会创建一个迭代器 LazyIterator,顾名思义,是以懒加载的方式加载服务实现类。而 ServiceLoader 类本身也实现了 Iterator 接口,调用 hasNext() 和 next() 方法时,内部调用 LazyIterator 的对应接口。

接下来就看看 LazyIterator 的具体实现。

1.2.1 加载类的全限定名

调用 LazyIterator#hasNext 时,会加载资源文件,实现如下:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第4张图片
image.png

加载的逻辑主要分为四步:

1)如果 nextName 有数据,说明前面已经加载了实现类,直接返回 true;

2)如果 nextName 无数据,则拼接配置文件名称 fullName:PREFIX + service.getName(),此处为 "META-INF/services/com.wlm.spi.service.Animal",由于是绝对路径,因此每个 jar 包都可能有这个文件。

3)根据 fullName 加载文件列表 configs,此时如果 pendings (接口实现类名称列表) 无数据,则判断是否有待解析的文件:

  • 如果无待解析的文件,说明已经加载完了,直接返回 false;
  • 如果有,则调用 parse 方法解析具体的文件,并将解析的结果保存到 pendings 中。

4)最后设置 nextName = pendings.next()。

这里主要看下解析文件的实现:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第5张图片
image.png

通过 Java IO 读取文件流,并循环调用 parseLine 解析每行的数据:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第6张图片
image.png

这里先读取文件行,然后校验类名称的有效性,比如是否为合法的 Java 字符等;如果合法则添加到 names 列表中,否则抛出异常。

因此调用 parse 方法之后,可以得到该配置文件内的所有实现类的全限定名称列表 names,接下来就是实例化的过程。

1.2.2 实例化扩展实现类

实例化的实现在 LazyIterator#next 中:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第7张图片
image.png

其中 nextName 为前面调用 hasNext() 获取到的接口实现类名称。

nextService() 主要有以下几个步骤:

  1. 通过 Class.forName 反射获取 Class 对象 c;
  2. 判断 c 是否是 service 接口的子类;
  3. 反射创建 c 的实例对象,并强制转换成 service 类型后,添加到 providers 列表中。
  4. 如果某一步出错,则抛出 ServiceConfigurationError 异常。

实例化的过程比较简单,使用方通过循环迭代获取并实例化扩展实现类,最终可得到一个扩展对象的列表。

2.Dubbo SPI

Dubbo 并未使用 Java SPI,而是通过 ExtensionLoader 实现了自己的 SPI 机制。

2.1 使用示例

与 Java SPI 不同的是,Dubbo SPI 需要在接口标注 @SPI 注解,可以通过 value 属性指定默认的实现类:

@SPI
public interface Animal {
    String getName();
}

接下来在 META-INF/dubbo 文件夹下创建一个文件,文件名称为接口的全限定类名:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第8张图片
image.png

文件内容如下:

lion=com.wlm.spi.service.Lion
tiger=com.wlm.spi.service.Tiger

与 Java SPI 不同,Dubbo SPI 通过键值对的方式进行配置接口实现类,为每个实现类定义一个别名,使用时通过别名加载实现类:

@Test
public void dubboSPITest() {

    Animal animal = ExtensionLoader
        .getExtensionLoader(Animal.class)
        .getExtension("lion");
    
    System.out.println(animal.getName());
}

输出如下:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第9张图片
image.png

接下来看看 Dubbo SPI 具体的实现。

2.2 源码

首先根据接口 Class 对象获取扩展加载器 ExtensionLoader:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第10张图片
image.png

这里有 2 个校验:type 必须是 interface,并且必须有 @SPI 注解。

获取到 ExtensionLoader 对象后,根据扩展实现类的别名 name 调用 getExtension 方法获取扩展对象:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第11张图片
image.png

如果传入的 name 为 "true",则获取默认的扩展实现类实例,即通过 @SPI 注解的 value 属性指定的默认扩展实现类别名,此时如果未设置 value 值则返回 null。

接着从本地缓存获取扩展实现类实例 instance,如果不存在则创建一个,实现如下:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第12张图片
image.png

创建扩展实现类主要分为以下几个步骤:

  1. 加载所有的扩展实现类;
  2. 反射创建扩展实现类的实例对象;
  3. 实例对象属性注入;
  4. 遍历包装类列表 cachedWrapperClasses,创建包装类实例,并注入依赖。

步骤 4 中,包装类列表 cachedWrapperClasses 为步骤 1 加载时缓存的数据,而注入依赖的部分和步骤 3 一致。因此不单独说明,本篇主要关注步骤 1 和步骤 3。

2.2.1 加载所有的扩展实现类

Dubbo 通过 getExtensionClasses 方法加载所有的扩展实现类,获取到实现类别名和实现类的映射关系:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第13张图片
image.png

先检查本地缓存,如果无数据,则调用 loadExtensionClasses 加载所有扩展实现类:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第14张图片
image.png

这里分为两步:

  1. 缓存默认扩展实现类的别名;
  2. 加载所有扩展实现类。

缓存默认扩展实现类的别名实现如下:

Java SPI 和 Dubbo SPI 的使用方式和实现细节_第15张图片
image.png

这里获取类的 @SPI 注解,如果存在且配置了 value 属性,则缓存到 cachedDefaultName 中。

1.@SPI 配置的 value,只能有 1 个名称,否则抛出 IllegalStateException 异常;
2.前面调用 getExtension 时判断,如果传入的别名为 "true",获取的默认扩展实现类,即通过此处获取。

Dubbo SPI 加载扩展实现类的方式和 Java SPI 类似。首先拼接配置文件的绝对路径 = 指定的文件夹 + 类的全限定名称,然后根据该路径加载所有 jar 包下的配置文件,Dubbo 中指定的文件夹主要分为:

  • META-INF/dubbo/internal/
  • META-INF/dubbo/
  • META-INF/services/

由于 Dubbo 现在的包前缀变为了 "org.apache",之前为 "com.alibaba",因此会根据该路径再加载一次,即:type.getName().replace("org.apache", "com.alibaba")。

加载文件的逻辑在 loadDirectory 中:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第16张图片
image.png

Dubbo 和 Java 一样,通过类加载器加载指定路径下所有的资源文件,然后遍历加载资源文件。

加载资源文件的实现如下:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第17张图片
image.png

这里的实现也和 Java 类似,通过 Java IO 读取文件流,循环读取文件行解析。和 Java 不一样的是,这里需要解析实现类别名和实现类全限定名,然后反射获取实现类 Class 对象,并加载实现类。

加载扩展实现类的实现如下:

Java SPI 和 Dubbo SPI 的使用方式和实现细节_第18张图片
image.png

这里先判断 clazz 是否是 type 的子类,否则抛出异常。然后分为以下三种情况处理:

  1. 实现类有 @Adaptive 注解:则缓存 clazz 对象到 cachedAdaptiveClass;
  2. 实现类是包装类,即含有以 type 为参数的构造器:则缓存 clazz 对象到 cachedWrapperClasses;
  3. 非以上两种情况:缓存 name 和 clazz 的映射关系。

这里主要看下第三种情况的处理,也即普通实现类的处理,主要分为以下三个步骤:

1)首先判断 clazz 对象是否有默认的构造器,无则抛出异常。

2)然后判断是否配置了别名 name,无则根据 clazz 的类名重新计算,具体逻辑如下:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第19张图片
image.png

3)切分 name,缓存各种映射关系,分为以下三种:

  • 如果 clazz 类有 @Activate 注解,则缓存 name 和该 @Activate 注解对象的映射关系到 cachedActivates;
  • 缓存 clazz 和 name 的映射关系到 cachedNames,此处只有一个 clazz 对象,相当于只缓存了 clazz 和 names[0] 的映射关系;
  • 缓存 name 和 clazz 的映射关系到 extensionClass。

缓存映射关系到 extensionClass 需要注意的是,如果配置中出现不同的实现类有相同的别名,会抛出异常。

至此,就获取到系统中所有的扩展类别名和扩展类 Class 对象的映射关系 extensionClasses:


image.png
image.png

2.2.2 实例对象属性注入

Dubbo 通过 injectExtension 为扩展类的实例注入依赖:


Java SPI 和 Dubbo SPI 的使用方式和实现细节_第20张图片
image.png

Dubbo 通过反射获取对象的所有 public 方法,然后遍历方法列表,寻找只有一个参数的 setter 方法;并且判断是否允许注入(@DisableInject 注解),以及参数类型是否是原始类型。

Dubbo 定义的 ”原始类型“ 有:

public static boolean isPrimitive(Class cls) {
    return cls.isPrimitive() || cls == String.class || cls == Boolean.class || cls == Character.class
         || Number.class.isAssignableFrom(cls) || Date.class.isAssignableFrom(cls);
}

如果有符合以上所有条件的 method,则根据属性名称 name 和参数类型 pt 从 objectFactory 获取属性的实例。

其中,objectFactory 为 AdaptiveExtensionFactory 对象,内部维护了一个 ExtensionFactory 列表,通过 AdaptiveExtensionFactory 获取扩展类的实例时,会遍历该列表,调用 getExtension 接口,获取扩展类的实例。

Dubbo 目前提供了 2 种 ExtensionFactory 实现:


image.png
image.png

实现的功能如下:

  • SpiExtensionFactory:根据 type 获取自适应的扩展类实例,是一种自适应扩展机制;
  • SpringExtensionFactory:根据 type 和 name 从 Spring 容器中获取扩展类实例。

注:Dubbo 的自适应扩展机制在下篇进行分析。

综上所述,injectExtension 根据 setter 注入依赖,从本质上来说,是一种 IOC 机制。

3.总结

Java SPI 和 Dubbo SPI 都通过将扩展实现类的全限定名称配置在文件中,并且在运行时动态加载,但用法和实现方式却大有不同:

Java SPI Dubbo SPI
配置文件内容 只需配置实现类的全限定名称 需配置别名和实现类全限定名称的映射关系
加载实现类方式 通过迭代器调用 hasNext 时,再去判断是否解析文件 一次性加载所有的实现类
实例化 反射调用无参构造器实例化 在反射实例化的基础上,还支持以 IOC 的方式注入依赖
获取实现类方式 根据迭代器遍历所有的扩展实现类 可根据别名获取;可获取默认的扩展实现类;可自适应获取

Java SPI 维护的是扩展实现类 List 列表,而 Dubbo SPI 维护的是扩展实现类别名和类之间的 Map 映射关系,如果系统中有多个扩展实现类,且需要灵活的指定不同的实现类,那么 Dubbo SPI 的方式更加适合。

Java SPI 和 Dubbo SPI 的使用方式和实现细节_第21张图片
关注一下吧

你可能感兴趣的:(Java SPI 和 Dubbo SPI 的使用方式和实现细节)