Dubbo SPI

Dubbo SPI

 

前言

大家好,今天开始给大家分享 — Dubbo 专题之 Dubbo SPI。在前面上个章节中我们讨论了 Dubbo 服务在线测试,了解了服务测试的基本使用和其实现的原理:其核心原理是通过元数据和使用 GenericService API 在不依赖接口 jar 包情况下发起远程调用。那本章节我们主要讨论在 Dubbo 中SPI拓展机制,那什么是SPI?以及其在我们的项目中有什么作用呢?那么我们在本章节中进行讨论。下面就让我们快速开始吧!

 

1. Dubbo SPI 简介

什么是 Dubbo SPI 呢?其本质是从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。下面来自官方的介绍:Dubbo 改进了 JDK 标准的 SPI 的以下问题:

  1. JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。

  2. 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。

  3. 增加了对扩展点 IoCAOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。

我们可以简单总结:Dubbo 中 SPI 按需加载节省资源、修复了 Java SPI 因类加载类失败异常被忽略问题、增加对 IoCAOP 的支持。

Dubbo SPI 支持的拓展点:

  1. 协议扩展

  2. 调用拦截扩展

  3. 引用监听扩展

  4. 暴露监听扩展

  5. 集群扩展

  6. 路由扩展

  7. 负载均衡扩展

  8. 合并结果扩展

  9. 注册中心扩展

  10. 监控中心扩展

  11. 扩展点加载扩展

  12. 动态代理扩展

  13. 编译器扩展

  14. Dubbo 配置中心扩展

  15. 消息派发扩展

  16. 线程池扩展

  17. 序列化扩展

  18. 网络传输扩展

  19. 信息交换扩展

  20. 组网扩展

  21. Telnet 命令扩展

  22. 状态检查扩展

  23. 容器扩展

  24. 缓存扩展

  25. 验证扩展

  26. 日志适配扩展

     

    2. 使用方式

    在 Dubbo 中有三个路径来存放这些拓展配置:META-INF/dubboMETA-INF/dubbo/internalMETA-INF/services/第二个目录是用来存放 Dubbo 内部的 SPI 拓展使用,第一个和第三个目录是我们可以使用的目录。拓展文件内容为:配置名=扩展实现类全限定名,多个实现类用换行符分隔,文件名为类全限定名。例如:

    |- resources

    |- META-INF

    |- dubbo

    | - org.apache.dubbo.rpc.Filter

    | - custom=com.muke.dubbocourse.spi.custom.CustomFilter

    Tips:META-INF/services/是 JDK 提供的 SPI 路径。

     

    3. 使用场景

    Dubbo 的拓展点是 Dubbo 成为最热门的 RPC 框架原因之一,它把灵活性、可拓展性发挥到了极致。在我们定制 Dubbo 框架的时候非常有用,我们执行简单的拓展和配置即可实现强大的功能。下面我们列举日常工作中常使用到的场景:

    1. 日志打印:在服务方法调用进入打印入参日志,方法调用完成返回前打印出参日志。

    2. 性能监控:在方法调用进入开始计时,方法调用完成返回前记录整个调用耗费时间。

    3. 链路追踪:在 Dubbo RPC 调用链路中传递每个系统的调用trace id,通过整合其它的链路追踪系统进行链路监控。

     

    4. 示例演示

    下面我们同样使用一个获取图书列表实例进行演示,同时我们自定义一个Filter在调用服务前后为我们输出日志。项目的结构如下:

    Dubbo SPI

    上面的结构中我们自定义了CustomFilter代码如下:

    /**
    * @author <a href="http://youngitman.tech">青年IT男</a>
    * @version v1.0.0
    * @className CustomFilter
    * @description 自定义过滤器
    * @JunitTest: {@link }
    * @date 2020-12-06 14:28
    **/
    public class CustomFilter implements Filter {
       @Override
       public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {

           System.out.println("自定义过滤器执行前");

           Result result = invoker.invoke(invocation);

           System.out.println("自定义过滤器执行后");

           return result;
      }
    }

    我们实现Filter并且在调用Invoker前后打印日志输出。下面我们看看服务提供端dubbo-provider-xml.xml配置:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
          xmlns="http://www.springframework.org/schema/beans"
          xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
          http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

       <dubbo:protocol port="20880" />

       <!--指定 filetr key = custom -->
       <dubbo:provider filter="custom"/>

       <dubbo:application name="demo-provider" metadata-type="remote"/>

       <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

       <bean id="bookFacade" class="com.muke.dubbocourse.spi.provider.BookFacadeImpl"/>

       <!--暴露服务为Dubbo服务-->
       <dubbo:service interface="com.muke.dubbocourse.common.api.BookFacade" ref="bookFacade" />

    </beans>

    上面的配置中我们配置<dubbo:provider filter="custom"/>指定使用custom自定义过滤器。接下来我们在resources->META-INF.dubbo目录下新建org.apache.dubbo.rpc.Filter文件配置内容如下:

    custom=com.muke.dubbocourse.spi.custom.CustomFilter

    其中custom为我们的拓展key与我们在 XML 中配置保持一致。

     

    5. 原理分析

    Dubbo 中的 SPI 拓展加载使用 ExtensionLoader下面我们简单的通过源码来分析下。首先入口为静态函数org.apache.dubbo.common.extension.ExtensionLoader#ExtensionLoader代码如下:

       private ExtensionLoader(Class<?> type) {
           //加载的拓展类类型
           this.type = type;
           //容器工厂,如果不是加载ExtensionFactory对象先执行ExtensionFactory加载再执行 getAdaptiveExtension
           objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
      }

    上面的方法很简单就是获得ExtensionLoader对象,值得注意的是这里是一个层层递归的调用直到加载类型为ExtensionFactory时终止。接下来我们看看getAdaptiveExtension代码:

       public T getAdaptiveExtension() {
           //缓存获取
           Object instance = cachedAdaptiveInstance.get();
           if (instance == null) {
              //...
               //加锁判断
               synchronized (cachedAdaptiveInstance) {
                   //再次获取 双重锁检测
                   instance = cachedAdaptiveInstance.get();
                   if (instance == null) {
                       try {
                           //创建拓展实例
                           instance = createAdaptiveExtension();
                           //进行缓存
                           cachedAdaptiveInstance.set(instance);
                      } catch (Throwable t) {
                        //...
                      }
                  }
              }
          }
           return (T) instance;
      }

    我们解析看看createAdaptiveExtension方法是怎样创建实例:

       private T createAdaptiveExtension() {
           try {
               //首先创建拓展实例,然后注入依赖
               return injectExtension((T) getAdaptiveExtensionClass().newInstance());
          } catch (Exception e) {
               throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
          }
      }

    getAdaptiveExtensionClass方法代码如下:

       private Class<?> getAdaptiveExtensionClass() {
           //获取拓展类
           getExtensionClasses();
           if (cachedAdaptiveClass != null) {
               return cachedAdaptiveClass;
          }
           //动态生成Class
           return cachedAdaptiveClass = createAdaptiveExtensionClass();
      }

    我们主要分析getExtensionClasses核心代码如下:

    /***
        *
        * 获取所有的拓展Class
        *
        * @author liyong
        * @date 20:18 2020-02-27
        * @param
        * @exception
        * @return java.util.Map<java.lang.String,java.lang.Class<?>>
        **/
       private Map<String, Class<?>> getExtensionClasses() {
           Map<String, Class<?>> classes = cachedClasses.get();
           if (classes == null) {
               synchronized (cachedClasses) {
                   classes = cachedClasses.get();
                   if (classes == null) {
                       //开始从资源路径加载Class
                       classes = loadExtensionClasses();
                      //设置缓存
                       cachedClasses.set(classes);
                  }
              }
          }
           return classes;
      }

    loadExtensionClasses代码如下:


       /**
        *
        * CLASS_PATH=org.apache.dubbo.common.extension.ExtensionFactory
        *
        * 1.META-INF/dubbo/internal/${CLASS_PATH} Dubbo内部使用路径
        * 2.META-INF/dubbo/${CLASS_PATH} 用户自定义扩展路径
        * 3.META-INF/services/{CLASS_PATH} JdkSPI路径
        *
        * synchronized in getExtensionClasses
        * */
       private Map<String, Class<?>> loadExtensionClasses() {
           cacheDefaultExtensionName();

           Map<String, Class<?>> extensionClasses = new HashMap<>();
           // internal extension load from ExtensionLoader's ClassLoader first
           loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName(), true);
           //兼容处理 由于dubbo捐献给apache
           loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"), true);

           loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
           loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
           loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
           loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
           return extensionClasses;
      }

    接下来我们看到真正进行资源加载的方法loadDirectory

    private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, booleanextensionLoaderClassLoaderFirst) {
           String fileName = dir + type;
           try {
               Enumeration<java.net.URL> urls = null;
               ClassLoader classLoader = findClassLoader();
               
               // try to load from ExtensionLoader's ClassLoader first
               if (extensionLoaderClassLoaderFirst) {
                   ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
                   //这里首先使用ExtensionLoader的类加载器,有可能是用户自定义加载
                   if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                       urls = extensionLoaderClassLoader.getResources(fileName);
                  }
              }
               
               if(urls == null || !urls.hasMoreElements()) {
                   if (classLoader != null) {//使用AppClassLoader加载
                       urls = classLoader.getResources(fileName);
                  } else {
                       urls = ClassLoader.getSystemResources(fileName);
                  }
              }

               if (urls != null) {
                   while (urls.hasMoreElements()) {
                       java.net.URL resourceURL = urls.nextElement();
                       //加载资源
                       loadResource(extensionClasses, classLoader, resourceURL);
                  }
              }
          } catch (Throwable t) {
               logger.error("Exception occurred when loading extension class (interface: " +
                       type + ", description file: " + fileName + ").", t);
          }
      }

    我们看看资源内容的加载逻辑方法loadResource核心代码如下:

       //加载文件值转换为Class到Map
       private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
           try {
               try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
                   String line;
                   //读取一行数据
                   while ((line = reader.readLine()) != null) {
                       final int ci = line.indexOf('#');//去掉注释
                       if (ci >= 0) {
                           line = line.substring(0, ci);
                      }
                       line = line.trim();
                       if (line.length() > 0) {
                           try {
                               String name = null;
                               int i = line.indexOf('=');
                               if (i > 0) {
                                   name = line.substring(0, i).trim();
                                   line = line.substring(i + 1).trim();
                              }
                               if (line.length() > 0) {
                                   //加载Class
                                   loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                              }
                          } catch (Throwable t) {
                              //...
                          }
                      }
                  }
              }
          } catch (Throwable t) {
              //...
          }
      }

    上面的方法通过循环加载每一行数据,同时解析出=后面的路径进行Class的装载。由此循环加载自定资源路径下面的所有通过配置文件配置的类。

     

    6. 小结

    在本小节中我们主要学习了 Dubbo 中 SPI,首先我们知道 Dubbo SPI 其本质是从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来,同时解决了 Java 中 SPI 的一些缺陷。我们也通过简单的使用案例来介绍我们日常工作中怎样去拓展,以及从源码的角度去解析 SPI 的加载原理其核心入口类为ExtensionLoader`。

    本节课程的重点如下:

  • 理解 Dubbo 中 SPI

  • 了解 Dubbo SPI 与 Java SPI 区别

  • 了解 Dubbo 怎样使用 SPI 进行拓展

  • 了解 SPI 实现原理

作者

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。关注公众号:青年 IT 男 获取最新技术文章推送!

 

博客地址: http://youngitman.tech

 

微信公众号:

 

Dubbo SPI
上一篇:JAVA SPI(Service Provider Interface)原理、设计及源码解析(其一)


下一篇:常用技术之SPI ServiceLoader