【Dubbo】SPI 机制原理分析 --静态扩展点

逃离我推掉我的手 2023-01-07 11:27 257阅读 0赞

在了解 Dubbo 的 SPI 之前 先来了解一下 JAVA 自带的 SPI。SPI 是 JDK 内置的一种服 务提供发现机制。目前市面上有很多框架都是用它来做服务的扩展发现。简单来说,它是一种动态替换发现的机制。

举个简单的例子,我们想在运行时动态给它添加实现,只需要添加一个实现,然后把新的实现描述给 JDK 知道就行了。比如JDBC、日志框架都有用到。

实现 SPI 需要遵循的标准:

  1. 需要在 classpath 下创建一个目录,该目录命名必须是:META-INF/service
  2. 在该目录下创建一个 properties 文件,该文件需要满足以下几个条件

    • 文件名必须是扩展的接口的全路径名称
    • 文件内部描述的是该扩展接口的所有实现类
    • 文件的编码格式是 UTF-8
  3. 通过 java.util.ServiceLoader 的加载机制来发现

关于 Java SPI 的详解及示例可以参考我的这篇文章…

  • 三个重要概念:扩展点:一个Java接口 ; 扩展:扩展点的实现类 ;扩展实例:扩展点实现类的实例
  • SPI机制实际上就是“基于接口的编程+策略模式+配置文件”组合实现的一种动态加载机制

Dubbo 的 SPI 扩展机制在 JAVA 自带的 SPI 基础上加入了扩展点的名称,即每个实现类都会对应至一个扩展点名称,其目的是应用可基于此名称进行相应的装配(因为一个服务可能会有多种实现)。

  1. 扩展点要加@SPI注解

    • 是由 Dubbo 提供的注解
    • @SPI(“value”)可以指定扩展点的默认实现
  2. 需要在 resource 目录下配置 META-INF/dubbo 或者 META-INF/dubbo/internal 或者 META-INF/services目录下,并基于 SPI 接口去创建一个文件

    • 文件名称和接口名称保持一致
    • 文件内容和 SPI 有差异,内容是 KEY 对应 Value ,key也可以理解成扩展的别名
  3. ExtensionLoader

    • 负责扩展的加载和生命周期维护
    • 类似于 Java SPI 的 ServiceLoader

Dubbo 针对的扩展点非常多,可以针对协议、拦截、集群、路由、负载均衡、序列化、容器… 几乎里面用到的所有功能,都可以实现自己的扩展,这个是 dubbo 比较强大的一点。


我们这篇就来看看 Dubbo 静态扩展点…

所谓静态扩展点就和静态代理很类似,即直接通过 name 去加载相应的扩展实例。

1.代码示例

示例:自定义协议 MyProtocol,然后配置到 dubbo.protocol。实现步骤如下:

1)自定义 MyProtocol 实现 Dubbo 的 Protocol 接口

  1. public class MyProtocol implements Protocol {
  2. @Override
  3. public int getDefaultPort() {
  4. return 8080;
  5. }
  6. @Override
  7. public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
  8. return null;
  9. }
  10. @Override
  11. public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
  12. return null;
  13. }
  14. @Override
  15. public void destroy() {
  16. }
  17. }

注意,我们上面不是说了 Dubbo 中,所有的扩展点都要有 @SPI 注解吗,那 Protocol 接口有吗?我们打开看看:

在这里插入图片描述

可以看到 Protocol 确实是 Dubbo 中内置的一个扩展点,并且指定了默认采用 dubbo 协议。

在这里插入图片描述

2)创建 META-INF/dubbo目录,并创建我们自己的 org.apache.dubbo.rpc.Protocol 文件

在这里插入图片描述

3)通过 ExtensionLoader 去获取自定义协议对象

  1. // ExtensionLoader 拿到具体实现类对象
  2. Protocol p = ExtensionLoader.getExtensionLoader(Protocol.class) // 根据Protocol.class找到对应文件
  3. .getExtension("myProtocol"); // 根据key myProtocol找到对应类
  4. // 调用具体实现
  5. System.out.print(protocol.getDefaultPort)

可以看到运行结果,是执行的自定义的协议

在这里插入图片描述
4)配置协议为 myProtocol

  1. <dubbo:protocol name="myProtocol"/>

2.源码分析

所谓的扩展,就是通过指定目录下配置一个对应接口的实现类,然后程序会进行查找和解析。那么这里就涉及两个问题:

  1. 怎么解析?
  2. 被加载的类的实例对象如何存储和使用?

我们下面就从ExtensionLoader.getExtensionLoader.getExtension着手,看到它底做了什么事情,能实现扩展协议的查找和加载。

getExtensionLoader()

  1. public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
  2. // 如果参数为空,报异常
  3. if (type == null) {
  4. throw new IllegalArgumentException("Extension type == null");
  5. }
  6. // 如果不是个接口,报异常
  7. if (!type.isInterface()) {
  8. throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
  9. }
  10. // 如果扩展点(接口)没有@SPI注解(上面Protocol是有的),报异常
  11. if (!withExtensionAnnotation(type)) {
  12. throw new IllegalArgumentException("Extension type (" + type
  13. +") is not an extension, because it is NOT annotated with @"
  14. + SPI.class.getSimpleName() + "!");
  15. }
  16. // 初始化ExtensionLoader
  17. ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
  18. if (loader == null) {
  19. EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
  20. loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
  21. }
  22. return loader;
  23. }

ExtensionLoader()

实例化ExtensionLoader,传入接口的class对象

  1. private ExtensionLoader(Class<?> type) {
  2. this.type = type;
  3. // 如果当前的 type=ExtensionFactory,type,那么 objectFactory=null
  4. objectFactory = (type == ExtensionFactory.class ? // 否则会创建一个自适应扩展点给到 objectFacotry
  5. null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
  6. }

注:objectFacotry 在自适应扩展点中分析

getExtension()

根据扩展的 key(别名),获取扩展实例

  1. // name是当前扩展点下扩展的key
  2. // 这里name就是myprotocol,而返回的实现类应该就是 MyProtocol
  3. public T getExtension(String name) {
  4. if (StringUtils.isEmpty(name)) {
  5. throw new IllegalArgumentException("Extension name == null");
  6. }
  7. // name=true,返回一个默认扩展
  8. if ("true".equals(name)) {
  9. return getDefaultExtension();
  10. }
  11. Holder<Object> holder = getOrCreateHolder(name);
  12. // 先尝试从缓存中获取,如果有就直接读取
  13. Object instance = holder.get();
  14. if (instance == null) {
  15. synchronized (holder) {
  16. instance = holder.get();
  17. if (instance == null) {
  18. // 根据名称创建实例对象
  19. instance = createExtension(name);
  20. holder.set(instance);
  21. }
  22. }
  23. }
  24. return (T) instance;
  25. }

createExtension()

创建扩展实例

  1. private T createExtension(String name) {
  2. // 加载指定路径下的所有文件,获取其中配置的k,v信息
  3. Class<?> clazz = getExtensionClasses().get(name);
  4. // 如果传入的name(myProtocol)配置的全类名(com...MyProtocol)没找到class,抛异常
  5. if (clazz == null) {
  6. throw findException(name);
  7. }
  8. try {
  9. // EXTENSION_INSTANCES是一个concurrentHashMap,用来缓存class,及其实例对象
  10. T instance = (T) EXTENSION_INSTANCES.get(clazz);
  11. if (instance == null) {
  12. EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
  13. instance = (T) EXTENSION_INSTANCES.get(clazz);
  14. }
  15. injectExtension(instance);
  16. Set<Class<?>> wrapperClasses = cachedWrapperClasses;
  17. if (CollectionUtils.isNotEmpty(wrapperClasses)) {
  18. for (Class<?> wrapperClass : wrapperClasses) {
  19. // 创建指定name的实例,并进行依赖注入(该实例对象的某个成员属性也是扩展点)
  20. instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
  21. }
  22. }
  23. return instance;
  24. } catch (Throwable t) {
  25. throw new IllegalStateException("Extension instance (name: " + name + ", class: "
  26. + type + ") couldn't be instantiated: " + t.getMessage(), t);
  27. }
  28. }

在这里插入图片描述

getExtensionClasses()

加载当前扩展点的所有扩展,保存在一个hashMap中

  1. 查找指定目录 /META-INF/dubbo 和 /META-INF/services 下指定的文件(文件名为接口全类名)

在这里插入图片描述

  1. 扫描这个文件下的所有配置信息。然后保存到一个 HashMap 中(classes)

    • key=扩展别名(key); myprotocol
    • value=对应配置的类的实例;MyProtocol.class

    private Map> getExtensionClasses() {

    1. Map<String, Class<?>> classes = cachedClasses.get();
    2. // 如果 cachedApdaptiveClas!=null ,直接返回这个 cachedAdaptiveClass
    3. if (classes == null) {
    4. synchronized (cachedClasses) {
    5. classes = cachedClasses.get();
    6. if (classes == null) {
    7. classes = loadExtensionClasses(); // 具体加载过程
    8. cachedClasses.set(classes);
    9. }
    10. }
    11. }
    12. return classes;
    13. }

cachedAdaptiveClass 是一个类级别自适应扩展点,表示告诉 dubbo spi loader,“我是一个自适应扩展点,来加载我吧” ,具体情况会在自适应扩展点中分析

injectExtension()

这个方法是用来实现依赖注入的,如果被加载的实例中,有成员属性本身也是一个扩展点,则会通过 set 方法进行注入

  1. private T injectExtension(T instance) {
  2. try {
  3. if (objectFactory != null) {
  4. // objectFactory这里用到了
  5. // 获得实例对应的方法,判断方法是否是一个set方法
  6. for (Method method : instance.getClass().getMethods()) {
  7. if (isSetter(method)) {
  8. // 可以选择禁用依赖注入
  9. if (method.getAnnotation(DisableInject.class) != null) {
  10. continue;
  11. }
  12. // 获得方法的参数,这个参数必须是一个对象类型并且是一个扩展点
  13. Class<?> pt = method.getParameterTypes()[0]; // 获得这个方法的参数类型
  14. if (ReflectUtils.isPrimitives(pt)) {
  15. // 如果不是对象类型,就跳过
  16. continue;
  17. }
  18. try {
  19. // 获得这个方法的属性名称
  20. String property = getSetterProperty(method);
  21. // 根据class以及name,使用自适应扩展点进行加载,并赋值到当前set方法中
  22. Object object = objectFactory.getExtension(pt, property);
  23. if (object != null) {
  24. // 调用set方法进行赋值
  25. method.invoke(instance, object);
  26. }
  27. } catch (Exception e) {
  28. logger.error("Failed to inject via method " + method.getName()
  29. + " of interface " + type.getName() + ": " + e.getMessage(), e);
  30. }
  31. }
  32. }
  33. }
  34. } catch (Exception e) {
  35. logger.error(e.getMessage(), e);
  36. }
  37. return instance;
  38. }

所谓的扩展点,套路都一样,不管是 springfactorieyLoader,还是 Dubbo 的 spi。实际上,Dubbo 的功能会更加强大,比如自适应扩展点,比如依赖注入。

关于自适应扩展点和激活扩展点的分析请看下一篇文章…

发表评论

表情:
评论列表 (有 0 条评论,257人围观)

还没有评论,来说两句吧...

相关阅读