1. JDK-SPI

JDK-1、SPI

1.1. 优点

使用 Java SPI 机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

1.2. 缺点

  1. JDK 标准的 SPI 会一次性加载实例化扩展点的所有实现,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费;
  2. 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类;

2. DubboSPI

2.1. 改进之处

 SPI(Service Provider Interface) 是服务发现机制,Dubbo 没有使用 jdk SPI 而对其增强和扩展:

  1. 按需加载,Dubbo SPI 配置文件采用 KV 格式存储,key 被称为扩展名,当我们在为一个接口查找具体实现类时,可以指定扩展名来选择相应的扩展实现,只实例化这一个扩展实现即可,无须实例化 SPI 配置文件中的其他扩展实现类,避免资源浪费,此外通过 KV 格式的 SPI 配置文件,当我们使用的一个扩展实现类所在的 jar 包没有引入到项目中时,Dubbo SPI 在抛出异常的时候,会携带该扩展名信息,而不是简单地提示扩展实现类无法加载。这些更加准确的异常信息降低了排查问题的难度,提高了排查问题的效率。;
  2. 增加扩展类的 IOC 能力,Dubbo 的扩展能力并不仅仅只是发现扩展服务实现类,而是在此基础上更进一步,如果该扩展类的属性依赖其他对象,则 Dubbo 会自动的完成该依赖对象的注入功能;
  3. 增加扩展类的 AOP 能力,Dubbo 扩展能力会自动的发现扩展类的包装类,完成包装类的构造,增强扩展类的功能;

2.2. DubboSPI 规范

  • 编写接口,接口必须加@SPI 注解,代表它是一个可扩展的接口。
  • 编写实现类。
  • 在 ClassPath 下的 META-INF/dubbo 目录创建以接口全限定名命名的文件,文件内容为 Key=Value 格式,Key 是扩展点的名称,Value 是扩展点实现类的全限定名。
  • 通过 ExtensionLoader 类获取扩展点实现。

Dubbo 默认会扫描 META-INF/servicesMETA-INF/dubboMETA-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
2
Protocol protocol = ExtensionLoader 
.getExtensionLoader(Protocol.class).getExtension("dubbo");

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
DubboInternalLoadingStrategy > DubboLoadingStrategy > ServicesLoadingStrateg

image.png

image.png

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. 运行逻辑

%%
▶10.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230407-1737%%
❕ ^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 缓存中查找扩展实现的实例,最终将其实例化后返回
image.png

2.4.3. createExtension

在 createExtension() 方法中完成了 SPI 配置文件的查找以及相应扩展实现类的实例化,同时还实现了自动装配以及自动 Wrapper 包装等功能。其核心流程是这样的:

  1. 获取 cachedClasses 缓存,根据扩展名从 cachedClasses 缓存中获取扩展实现类。如果 cachedClasses 未初始化,则会扫描前面介绍的三个 SPI 目录获取查找相应的 SPI 配置文件,然后加载其中的扩展实现类,最后将扩展名和扩展实现类的映射关系记录到 cachedClasses 缓存中。这部分逻辑在 loadExtensionClasses() 和 loadDirectory() 方法中。
  2. 根据扩展实现类EXTENSION_INSTANCES 缓存中查找相应的实例。如果查找失败,会通过反射创建扩展实现对象。
  3. 自动装配 扩展实现对象中的属性(即调用其 setter)。这里涉及 ExtensionFactory 以及自动装配的相关内容,本课时后面会进行详细介绍。
  4. 自动包装 扩展实现对象。这里涉及 Wrapper 类以及自动包装特性的相关内容,本课时后面会进行详细介绍。
  5. 如果扩展实现类实现了 Lifecycle 接口,在 initExtension() 方法中会调用 initialize() 方法进行初始化。

image.png

2.5. @Adaptive

https://blog.51cto.com/u_15288542/3030280

image.png
image.png
AdaptiveExtensionFactory 不实现任何具体的功能,而是用来适配 ExtensionFactory 的 SpiExtensionFactory 和 SpringExtensionFactory 这两种实现。AdaptiveExtensionFactory 会根据运行时的一些状态来选择具体调用 ExtensionFactory 的哪个实现。

@Adaptive 注解还可以加到接口方法之上,Dubbo 会动态生成适配器类。例如,Transporter 接口有两个被 @Adaptive 注解修饰的方法:

- [ ] 🚩 - 待研究:https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/Dubbo%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB%E4%B8%8E%E5%AE%9E%E6%88%98-%E5%AE%8C/04%20%20Dubbo%20SPI%20%E7%B2%BE%E6%9E%90%EF%BC%8C%E6%8E%A5%E5%8F%A3%E5%AE%9E%E7%8E%B0%E4%B8%A4%E6%9E%81%E5%8F%8D%E8%BD%AC%EF%BC%88%E4%B8%8B%EF%BC%89.md - 🏡 2023-04-01 13:04 #todo

2.5.1. 标注类 - 将该类作为最佳适配类

image.png

image.png

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

04 Dubbo SPI 精析,接口实现两极反转(下)

https://juejin.cn/post/7040443722101850148#heading-3

6.2. 使用方法

6.2.1. Dubbo SPI 使用姿势

%%
▶3.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230328-1213%%
❕ ^u1bn4l

https://www.jianshu.com/p/de465d70f63f
示例代码:

1
/Users/taylor/Nutstore Files/Obsidian_data/pages/002-schdule/001-Arch/001-Subject/013-DemoCode/dubbo-demo

https://segmentfault.com/a/1190000040208433