分布式专题 -14、Dubbo-SPI
1. JDK-SPI
JDK-1、SPI1.1. 优点
使用 Java SPI 机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
1.2. 缺点
- JDK 标准的 SPI 会一次性加载实例化扩展点的所有实现,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费;
- 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类;
2. DubboSPI
2.1. 改进之处
SPI(Service Provider Interface) 是服务发现机制,Dubbo 没有使用 jdk SPI 而对其增强和扩展:
- 按需加载,Dubbo SPI 配置文件采用 KV 格式存储,key 被称为扩展名,当我们在为一个接口查找具体实现类时,可以指定扩展名来选择相应的扩展实现,只实例化这一个扩展实现即可,无须实例化 SPI 配置文件中的其他扩展实现类,避免资源浪费,此外通过 KV 格式的 SPI 配置文件,当我们使用的一个扩展实现类所在的 jar 包没有引入到项目中时,Dubbo SPI 在抛出异常的时候,会携带该扩展名信息,而不是简单地提示扩展实现类无法加载。这些更加准确的异常信息降低了排查问题的难度,提高了排查问题的效率。;
- 增加扩展类的 IOC 能力,Dubbo 的扩展能力并不仅仅只是发现扩展服务实现类,而是在此基础上更进一步,如果该扩展类的属性依赖其他对象,则 Dubbo 会自动的完成该依赖对象的注入功能;
- 增加扩展类的 AOP 能力,Dubbo 扩展能力会自动的发现扩展类的包装类,完成包装类的构造,增强扩展类的功能;
2.2. DubboSPI 规范
- 编写接口,接口必须加@SPI 注解,代表它是一个可扩展的接口。
- 编写实现类。
- 在 ClassPath 下的
META-INF/dubbo
目录创建以接口全限定名命名的文件,文件内容为 Key=Value 格式,Key 是扩展点的名称,Value 是扩展点实现类的全限定名。 - 通过 ExtensionLoader 类获取扩展点实现。
Dubbo 默认会扫描 META-INF/services
、META-INF/dubbo
、META-INF/dubbo/internal
三个目录下的配置,第一个是为了兼容 Java SPI,第三个是 Dubbo 内部使用的扩展点。
Dubbo SPI 支持四种特性:自动包装、自动注入、自适应、自动激活。
2.3. 重要组件
2.3.1. ExtensionLoader
ExtensionLoader 位于 dubbo-common 模块中的 extension 包中,功能类似于 JDK SPI 中的 java.util.ServiceLoader。Dubbo SPI 的核心逻辑几乎都封装在 ExtensionLoader 之中(==其中就包括 @SPI 注解的处理逻辑==),其使用方式如下所示:
1 |
|
ExtensionLoader 的实例字段
type(Class<?>类型)
:当前 ExtensionLoader 实例负责加载扩展接口。cachedDefaultName(String类型)
:记录了 type 这个扩展接口上 @SPI 注解的 value 值,也就是默认扩展名。cachedNames(ConcurrentMap<Class<?>, String>类型)
:缓存了该 ExtensionLoader 加载的扩展实现类与扩展名之间的映射关系。cachedClasses(Holder<Map<String, Class<?>>>类型)
:缓存了该 ExtensionLoader 加载的扩展名与扩展实现类之间的映射关系。cachedNames 集合的反向关系缓存。cachedInstances(ConcurrentMap<String, Holder>类型)
:缓存了该 ExtensionLoader 加载的扩展名与扩展实现对象之间的映射关系。
2.3.2. strategies
LoadingStrategy 接口有三个实现(通过 JDK SPI 方式加载的),如下图所示,分别对应前面介绍的三个 Dubbo SPI 配置文件所在的目录,且都继承了 Prioritized 这个优先级接口,默认优先级是
1 |
|
2.3.3. EXTENSION_LOADERS
ConcurrentMap<Class, ExtensionLoader>
类型:Dubbo 中一个扩展接口对应一个 ExtensionLoader 实例,该集合缓存了全部 ExtensionLoader 实例,其中的 Key 为扩展接口,Value 为加载其扩展实现的 ExtensionLoader 实例。
2.3.4. EXTENSION_INSTANCES
ConcurrentMap<Class<?>, Object>
类型:该集合缓存了扩展实现类与其实例对象的映射关系。比如在前文示例中,Key 为 Class,Value 为 DubboProtocol 对象。
2.4. 运行逻辑
❕ ^z97rrg
2.4.1. getExtensionLoader
ExtensionLoader.getExtensionLoader
的方法,完成 ExtensionLoader 创建,该方法内部完成一些关于 type 的校验,然后根据 EXTENSION_LOADERS
缓存获取 ExtensionLoader 的实例,EXTENSION_LOADERS
集合缓存了全部 ExtensionLoader 实例,其中的 Key 为扩展接口,Value 为加载其扩展实现的 ExtensionLoader 实例,如果不存在缓存的时候,则主动创建一个并放入 EXTENSION_LOADERS
。
2.4.2. getExtension
得到接口对应的 ExtensionLoader 对象之后会调用其 getExtension() 方法,根据传入的扩展名称从 cachedInstances 缓存中查找扩展实现的实例,最终将其实例化后返回
2.4.3. createExtension
在 createExtension() 方法中完成了 SPI 配置文件的查找以及相应扩展实现类的实例化,同时还实现了自动装配以及自动 Wrapper 包装等功能。其核心流程是这样的:
- 获取 cachedClasses 缓存,根据扩展名从
cachedClasses
缓存中获取扩展实现类。如果 cachedClasses 未初始化,则会扫描前面介绍的三个 SPI 目录获取查找相应的 SPI 配置文件,然后加载其中的扩展实现类,最后将扩展名和扩展实现类的映射关系记录到 cachedClasses 缓存中。这部分逻辑在 loadExtensionClasses() 和 loadDirectory() 方法中。 - 根据扩展实现类从
EXTENSION_INSTANCES
缓存中查找相应的实例。如果查找失败,会通过反射创建扩展实现对象。 - 自动装配 扩展实现对象中的属性(即调用其 setter)。这里涉及 ExtensionFactory 以及自动装配的相关内容,本课时后面会进行详细介绍。
- 自动包装 扩展实现对象。这里涉及 Wrapper 类以及自动包装特性的相关内容,本课时后面会进行详细介绍。
- 如果扩展实现类实现了 Lifecycle 接口,在 initExtension() 方法中会调用 initialize() 方法进行初始化。
2.5. @Adaptive
https://blog.51cto.com/u_15288542/3030280
AdaptiveExtensionFactory 不实现任何具体的功能,而是用来适配 ExtensionFactory 的 SpiExtensionFactory 和 SpringExtensionFactory 这两种实现。AdaptiveExtensionFactory 会根据运行时的一些状态来选择具体调用 ExtensionFactory 的哪个实现。
@Adaptive 注解还可以加到接口方法之上,Dubbo 会动态生成适配器类。例如,Transporter 接口有两个被 @Adaptive 注解修饰的方法:
#todo2.5.1. 标注类 - 将该类作为最佳适配类
2.5.2. 标注方法 - 静态代理该方法
@Adapter 标签标注在方法上可以对该方法进行静态代理
如果接口的某个实现类上标注了@Adapter 注解,将直接调用该实现类,而不会进行静态代理
2.6. 自动装配
在 createExtension() 方法中我们看到,Dubbo SPI 在拿到扩展实现类的对象(以及 Wrapper 类的对象)之后,还会调用 injectExtension() 方法扫描其全部 setter 方法,并根据 setter 方法的名称以及参数的类型,加载相应的扩展实现,然后调用相应的 setter 方法填充属性,这就实现了 Dubbo SPI 的自动装配特性。简单来说,自动装配属性就是在加载一个扩展点的时候,将其依赖的扩展点一并加载,并进行装配。
2.7. 自动包装
Dubbo 中的一个扩展接口可能有多个扩展实现类,这些扩展实现类可能会包含一些相同的逻辑,如果在每个实现类中都写一遍,那么这些重复代码就会变得很难维护。Dubbo 提供的自动包装特性,就可以解决这个问题。 Dubbo 将多个扩展实现类的公共逻辑,抽象到 Wrapper 类中,Wrapper 类与普通的扩展实现类一样,也实现了扩展接口,在获取真正的扩展实现对象时,在其外面包装一层 Wrapper 对象,你可以理解成一层装饰器。
3. IOC
IOC:对应 injectExtension 方法 查找 set 方法,根据参数找到依赖对象则注入。
4. AOP
AOP:对应 WrapperClass,包装类是因为一个扩展接口可能有多个扩展实现类,而这些扩展实现类会有一个相同的或者公共的逻辑,如果每个实现类都写一遍代码就重复了,并且比较不好维护。因此就搞了个包装类,Dubbo 里帮你自动包装,只需要某个扩展类的构造函数只有一个参数,并且是扩展接口类型,就会被判定为包装类,然后记录到配置文件,用来包装别的实现类。
5. 实战经验
6. 参考与感谢
6.1. 源码原理
https://blog.51cto.com/u_15288542/3030280
https://www.cnblogs.com/grimmjx/p/10970643.html
https://www.cnblogs.com/wtzbk/p/16618487.html
https://juejin.cn/post/7040443722101850148#heading-3
6.2. 使用方法
6.2.1. Dubbo SPI 使用姿势
❕ ^u1bn4l
https://www.jianshu.com/p/de465d70f63f
示例代码:
1 |
|