SPI 全称为 Service Provider Interface,是一种服务发现机制。当程序运行调用接口时,会根据配置文件或默认规则信息加载对应的实现类。所以在程序中并没有直接指定使用接口的哪个实现,而是在外部进行装配。
要想了解 Dubbo 的设计与实现,其中 Dubbo SPI 加载机制是必须了解的,在 Dubbo 中有大量功能的实现都是基于 Dubbo SPI 实现解耦,同时也使得 Dubbo 获得如此好的可扩展性。
Java SPI
通过完成一个 Java SPI 的操作来了解它的机制。
- 创建一个 AnimalService 接口及 category 方法
- 创建一个实现类 Cat
- 创建 META-INF/services 目录,并在该目录下创建一个文件,文件名为 AnimalService 的全限定名作为文件名
- 在文件中添加实现类 Cat 的全限定名
Animal 接口
1 | public interface AnimalService { |
Cat 实现类
1 | public class Cat implements AnimalService { |
在 META-INF/services 目录下的 top.ytao.demo.spi.AnimalService 文件中添加:
1 | top.ytao.demo.spi.Cat |
加载 SPI 的实现:
1 | public class JavaSPITest { |
执行结果:
就这样,一个 Java SPI 就实现完成了,通过 ServiceLoader.load
获取加载所有接口已配置的接口实现类,然后可以遍历找出需要的实现。
Dubbo SPI
本文 Dubbo 版本为2.7.5
Dubbo SPI 相较于 Java SPI 更为强大,并且都是由自己实现的一套 SPI 机制。其中主要的改进和优化:
- 相对于 Java SPI 一次性加载所有实现,Dubbo SPI 是按需加载,只加载需要使用的实现类。同时带有缓存支持。
- 更为详细的扩展加载失败信息。
- 增加了对扩展 IOC 和 AOP的支持。
Dubbo SPI 示例
Dubbo SPI 的配置文件放在 META-INF/dubbo 下面,并且实现类的配置方式采用 K-V 的方式,key 为实例化对象传入的参数,value 为扩展点实现类全限定名。例如 Cat 的配置文件内容:
1 | cat = top.ytao.demo.spi.Cat |
Dubbo SPI 加载过程中,对 Java SPI 的目录也是可以被兼容的。
同时需要在接口上增加 @SPI 注解,@SPI 中可以指定 key 值,加载 SPI 如下:
1 | public class DubboSPITest { |
执行结果如下:
获取 ExtensionLoader 实例
获取 ExtensionLoader 实例是通过上面 getExtensionLoader 方法,具体实现代码:
1 | public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) { |
上面获取扩展类加载器过程主要是检查传入的 type 是否合法,以及从扩展类加载器缓存中是否存在当前类型的接口,如果不存在则添加当前接口至缓存中。ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS
是扩展类加载器的缓存,它是以接口作为 key, 扩展类加载器作为 value 进行缓存。
获取扩展类对象
获取扩展类对象的方法ExtensionLoader#getExtension
,在这里完成扩展对象的缓存及创建工作:
1 | public T getExtension(String name) { |
获取 holder 对象是从缓存ConcurrentMap<String, Holder<Object>> cachedInstances
中获取,如果不存在,则以扩展名 key,创建一个 Holder 对象作为 value,设置到扩展对象缓存。
如果是新创建的扩展对象实例,那么 holder.get() 一定是 null ,扩展对象为空时,经过双重检查锁,创建扩展对象。
创建扩展对象
创建扩展对象过程:
1 | private T createExtension(String name) { |
上面创建扩展过程中,里面有个 Wrapper 类,这里使用到装饰器模式,该类是没有具体的实现,而是把通用逻辑进行抽象。
创建这个过程是从所有扩展类中获取当前扩展名对应映射关系的扩展类,以及向当前扩展对象注入依赖。
获取所有扩展类:
1 | private Map<String, Class<?>> getExtensionClasses() { |
检查普通扩展类缓存是否为空,如果不为空则重新加载,真正加载扩展类在loadExtensionClasses
中:
1 |
|
上面获取 @SPI 扩展名,以及指定要加载的文件。从上面静态常量中,我们可以看到,Dubbo SPI 也是支持加载 Java SPI 的目录,同时还加载 META-INF/dubbo/internal (该目录为 Dubbo 的内部扩展类目录),在 loadDirectory 加载目录配置文件。
1 | private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst) { |
这里获取文件名后加载所有同名文件,然后迭代各个文件,逐个加载文件内容。
1 | private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) { |
上面代码完成文件内容加载和解析,接下来通过 loadClass
加载扩展类。
1 | private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException { |
至此,getExtensionClasses() 加载扩展类方法分析完成,接下分析注入依赖 injectExtension() 方法。
1 | private T injectExtension(T instance) { |
通过遍历扩展类所有方法,找到相对应的依赖,然后使用反射调用 settter 方法来进行设置依赖。
objectFactory 对象如图:
其中找到相应依赖是在 SpiExtensionFactory 或 SpringExtensionFactory 中,同时,这两个 Factory 保存在 AdaptiveExtensionFactory 中进行维护。
1 |
|
以上是对 Dubbo SPI 扩展类简单加载过程分析完成。
自适应加载机制
为 Dubbo 更加灵活的使一个接口不通过硬编码加载扩展机制,而是通过使用过程中进行加载,Dubbo 的另一加载机制——自适应加载。
自适应加载机制使用 @Adaptive 标注:
1 |
|
Adaptive 的值是一个数组,可以配置多个 key。初始化时,遍历所有 key 进行匹配,如果没有则匹配 @SPI 的值。
当 Adaptive 注解标注在类上时,则简单对应该实现。如果注解标注在接口方法上时,则会根据参数动态生成代码来获取扩展点的实现。
类上注解处理还是比较好理解,方法上的注解加载相对比较有研读性。通过调用ExtensionLoader#getAdaptiveExtension
来进行获取扩展实现。
1 | public T getAdaptiveExtension() { |
上面代码完成了扩展类对象是否存在缓存中,如果不存在,则通过创建自适应扩展,并将实例注入依赖后,设置在实例化后的自适应扩展对象中。
其中getAdaptiveExtensionClass
是比较核心的流程。
1 | private Class<?> getAdaptiveExtensionClass() { |
这里完成的工作主要是,加载全部扩展类,代表所有扩展接口类的实现类,在其加载过程中,如果有 @Adaptive 标注的类,会保存到 cachedAdaptiveClass 中。通过自动生成自适应扩展代码,并被编译后,获取扩展类实例化对象。
上面编译器类型是可以指定的,通过 compiler 进行指定,例如:<dubbo:application name="taomall-provider" compiler="jdk" />
,该编译器默认使用 javassist 编译器。
在 generate 方法中动态生成代码:
1 | public String generate() { |
上面是生成类信息的方法,生成设计原理是按照已设置好的模板,进行替换操作,生成类。具体信息不代码很多,但阅读还是比较简单。
自适应加载机制,已简单分析完,咋一眼看,非常复杂,但是了解整体结构和流程,再去细研的话,相对还是好理解。
总结
从 Dubbo 设计来看,其良好的扩展性,比较重要的一点是得益于 Dubbo SPI 加载机制。在学习它的设计理念,对可扩展性方面的编码思考也有一定的启发。