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 文件夹下创建名称为接口的全限定类名的文件:
文件内容为实现类的全限定类名:
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());
}
}
输出为:
1.2 源码
调用 ServiceLoader#load 会创建一个 ServiceLoader 对象,用于加载实现类:
在 ServiceLoader 的构造器中会创建一个迭代器 LazyIterator,顾名思义,是以懒加载的方式加载服务实现类。而 ServiceLoader 类本身也实现了 Iterator 接口,调用 hasNext() 和 next() 方法时,内部调用 LazyIterator 的对应接口。
接下来就看看 LazyIterator 的具体实现。
1.2.1 加载类的全限定名
调用 LazyIterator#hasNext 时,会加载资源文件,实现如下:
加载的逻辑主要分为四步:
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 IO 读取文件流,并循环调用 parseLine 解析每行的数据:
这里先读取文件行,然后校验类名称的有效性,比如是否为合法的 Java 字符等;如果合法则添加到 names 列表中,否则抛出异常。
因此调用 parse 方法之后,可以得到该配置文件内的所有实现类的全限定名称列表 names,接下来就是实例化的过程。
1.2.2 实例化扩展实现类
实例化的实现在 LazyIterator#next 中:
其中 nextName 为前面调用 hasNext() 获取到的接口实现类名称。
nextService() 主要有以下几个步骤:
- 通过 Class.forName 反射获取 Class 对象 c;
- 判断 c 是否是 service 接口的子类;
- 反射创建 c 的实例对象,并强制转换成 service 类型后,添加到 providers 列表中。
- 如果某一步出错,则抛出 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 文件夹下创建一个文件,文件名称为接口的全限定类名:
文件内容如下:
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());
}
输出如下:
接下来看看 Dubbo SPI 具体的实现。
2.2 源码
首先根据接口 Class 对象获取扩展加载器 ExtensionLoader:
这里有 2 个校验:type 必须是 interface,并且必须有 @SPI 注解。
获取到 ExtensionLoader 对象后,根据扩展实现类的别名 name 调用 getExtension 方法获取扩展对象:
如果传入的 name 为 "true",则获取默认的扩展实现类实例,即通过 @SPI 注解的 value 属性指定的默认扩展实现类别名,此时如果未设置 value 值则返回 null。
接着从本地缓存获取扩展实现类实例 instance,如果不存在则创建一个,实现如下:
创建扩展实现类主要分为以下几个步骤:
- 加载所有的扩展实现类;
- 反射创建扩展实现类的实例对象;
- 实例对象属性注入;
- 遍历包装类列表 cachedWrapperClasses,创建包装类实例,并注入依赖。
步骤 4 中,包装类列表 cachedWrapperClasses 为步骤 1 加载时缓存的数据,而注入依赖的部分和步骤 3 一致。因此不单独说明,本篇主要关注步骤 1 和步骤 3。
2.2.1 加载所有的扩展实现类
Dubbo 通过 getExtensionClasses 方法加载所有的扩展实现类,获取到实现类别名和实现类的映射关系:
先检查本地缓存,如果无数据,则调用 loadExtensionClasses 加载所有扩展实现类:
这里分为两步:
- 缓存默认扩展实现类的别名;
- 加载所有扩展实现类。
缓存默认扩展实现类的别名实现如下:
这里获取类的 @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 中:
Dubbo 和 Java 一样,通过类加载器加载指定路径下所有的资源文件,然后遍历加载资源文件。
加载资源文件的实现如下:
这里的实现也和 Java 类似,通过 Java IO 读取文件流,循环读取文件行解析。和 Java 不一样的是,这里需要解析实现类别名和实现类全限定名,然后反射获取实现类 Class 对象,并加载实现类。
加载扩展实现类的实现如下:
这里先判断 clazz 是否是 type 的子类,否则抛出异常。然后分为以下三种情况处理:
- 实现类有 @Adaptive 注解:则缓存 clazz 对象到 cachedAdaptiveClass;
- 实现类是包装类,即含有以 type 为参数的构造器:则缓存 clazz 对象到 cachedWrapperClasses;
- 非以上两种情况:缓存 name 和 clazz 的映射关系。
这里主要看下第三种情况的处理,也即普通实现类的处理,主要分为以下三个步骤:
1)首先判断 clazz 对象是否有默认的构造器,无则抛出异常。
2)然后判断是否配置了别名 name,无则根据 clazz 的类名重新计算,具体逻辑如下:
3)切分 name,缓存各种映射关系,分为以下三种:
- 如果 clazz 类有 @Activate 注解,则缓存 name 和该 @Activate 注解对象的映射关系到 cachedActivates;
- 缓存 clazz 和 name 的映射关系到 cachedNames,此处只有一个 clazz 对象,相当于只缓存了 clazz 和 names[0] 的映射关系;
- 缓存 name 和 clazz 的映射关系到 extensionClass。
缓存映射关系到 extensionClass 需要注意的是,如果配置中出现不同的实现类有相同的别名,会抛出异常。
至此,就获取到系统中所有的扩展类别名和扩展类 Class 对象的映射关系 extensionClasses:
2.2.2 实例对象属性注入
Dubbo 通过 injectExtension 为扩展类的实例注入依赖:
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 实现:
实现的功能如下:
- 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 的方式更加适合。