Dubbo SPI

作者: 我可能是个假开发 | 来源:发表于2023-01-31 23:17 被阅读0次

    一、SPI

    SPI,英文全称是 Service Provider Interface,按每个单词翻译就是:服务提供接口。
    这里的“服务”泛指任何一个可以提供服务的功能、模块、应用或系统,这些“服务”在设计接口或规范体系时,往往会预留一些比较关键的口子或者扩展点,让调用方按照既定的规范去自由发挥实现,而这些所谓的“比较关键的口子或者扩展点”,就叫“服务”提供的“接口”。

    二、实现SPI

    image.png

    在 Web 应用成功启动的时刻,预加载 Dubbo 框架的一些资源。

    为了讲究通用性,开源团队也只会提供一个口子,定义一种规范约束,给上层开发人员实现该口子做一些定制化逻辑,一般会这样做:


    image.png
    
    ///////////////////////////////////////////////////
    // web-fw.jar 插件的启动类,在“应用成功启动”时刻提供一个扩展口子
    ///////////////////////////////////////////////////
    public class WebFwBootApplication {
        // web-fw.jar 插件的启动入口
        public static void run(Class<?> primarySource, String... args) {
            // 开始启动中,此处省略若干行代码...
            // 环境已准备好,此处省略若干行代码...
            // 上下文已实例化,此处省略若干行代码...
            // 上下文已准备好,此处省略若干行代码...
            
            // 应用成功启动
            onCompleted();
            
            // 应用已准备好,此处省略若干行代码...
        }
        
        // 应用成功启动时刻,提供一个扩展口子
        private static void onCompleted() {
            // 加载 ApplicationStartedListener 接口的所有实现类
            ServiceLoader<ApplicationStartedListener> loader = 
                    ServiceLoader.load(ApplicationStartedListener.class);
            // 遍历 ApplicationStartedListener 接口的所有实现类,并调用里面的 onCompleted 方法
            Iterator<ApplicationStartedListener> it = loader.iterator();
            while (it.hasNext()){
                // 获取其中的一个实例,并调用 onCompleted 方法
                ApplicationStartedListener instance = it.next();
                instance.onCompleted();
            }
        }
    }
    
    ///////////////////////////////////////////////////
    // web-fw.jar 插件的“应用启动成功的监听器接口”,定制一种接口规范
    ///////////////////////////////////////////////////
    public interface ApplicationStartedListener {
        // 触发完成的方法
        void onCompleted();
    }
    
    ///////////////////////////////////////////////////
    // app-web 后台应用的启动类代码
    ///////////////////////////////////////////////////
    public class Dubbo14JdkSpiApplication {
        public static void main(String[] args) {
            // 模拟 app-web 调用 web-fw 框架启动整个后台应用
            WebFwBootApplication.run(Dubbo14JdkSpiApplication.class, args);
        }
    }
    
    ///////////////////////////////////////////////////
    // app-web 后台应用的资源目录文件
    // 路径为:/META-INF/services/com.hmilyylimh.cloud.jdk.spi.ApplicationStartedListener
    ///////////////////////////////////////////////////
    com.hmilyylimh.cloud.jdk.spi.PreloadDubboResourcesListener
    

    代码中定义了一个应用启动成功的监听器接口(ApplicationStartedListener),接着 app-web 自定义一个预加载 Dubbo 资源监听器(PreloadDubboResourcesListener)来实现该接口。
    在插件应用成功启动的时刻,会寻找 ApplicationStartedListener 接口的所有实现类,并将所有实现类全部执行一遍,这样,插件既提供了一种口子的规范约束,又能满足业务诉求在应用成功启动时刻做一些事情。
    其实插件在指定标准接口规范的这件事情上,就是 SPI 的思想体现,只不过是 JDK 通过 ServiceLoader 实现了这套思想,也就是我们耳熟能详的 JDK SPI 机制。

    三、JDK SPI

    ServiceLoader 大致的核心代码流程:


    image.png
    • 第一块,将接口传入到 ServiceLoader.load 方法后,得到了一个内部类的迭代器。
    • 第二块,通过调用迭代器的 hasNext 方法,去读取“/META-INF/services/ 接口类路径”这个资源文件内容,并逐行解析出所有实现类的类路径。
    • 第三块,将所有实现类的类路径通过“Class.forName”反射方式进行实例化对象。

    使用 ServiceLoader 的 load 方法执行多次时,会不断创建新的实例对象。

    public static void main(String[] args) {
        // 模拟进行 3 次调用 load 方法并传入同一个接口
        for (int i = 0; i < 3; i++) {
            // 加载 ApplicationStartedListener 接口的所有实现类
            ServiceLoader<ApplicationStartedListener> loader 
                   = ServiceLoader.load(ApplicationStartedListener.class);
            // 遍历 ApplicationStartedListener 接口的所有实现类,并调用里面的 onCompleted 方法
            Iterator<ApplicationStartedListener> it = loader.iterator();
            while (it.hasNext()){
                // 获取其中的一个实例,并调用 onCompleted 方法
                ApplicationStartedListener instance = it.next();
                instance.onCompleted();
            }
        }
    }
    

    调用 3 次 ServiceLoader 的 load 方法,并且每一次传入的都是同一个接口,运行编写好的代码,打印出如下信息:

    预加载 Dubbo 框架的一些资源, com.hmilyylimh.cloud.jdk.spi.PreloadDubboResourcesListener@300ffa5d
    预加载 Dubbo 框架的一些资源, com.hmilyylimh.cloud.jdk.spi.PreloadDubboResourcesListener@1f17ae12
    预加载 Dubbo 框架的一些资源, com.hmilyylimh.cloud.jdk.spi.PreloadDubboResourcesListener@4d405ef7
    

    每次调用 load 方法传入同一个接口的话,打印出来的引用地址都不一样,说明创建出了多个实例对象。

    JDK SPI 的问题

    • 使用 load 方法频率高,容易影响 IO 吞吐和内存消耗。
    • 使用 load 方法想要获取指定实现类,需要自己进行遍历并编写各种比较代码。

    解决:

    • 有方法被大量调用,我们的尝试是看看是否可以缓存起来。有 N 次调用,如果第一次通过读取文件、解析文件、反射实例化拿到接口的所有实现类并缓存起来,后面 N - 1 次就可以直接从缓存读取,大大降低了各种耗时的操作,性能有质的提升。
    • 每次需要遍历找到想要的实现类,可以以空间换时间,叠加哈希算法进行快速寻址查找。

    增加缓存,来降低磁盘 IO 访问以及减少对象的生成;使用 Map 的 hash 查找,来提升检索指定实现类的性能。

    四、Dubbo SPI

    Dubbo 也定义出了自己的一套 SPI 机制逻辑,既要通过 O(1) 的时间复杂度来获取指定的实例对象,还要控制缓存创建出来的对象,做到按需加载获取指定实现类,并不会像 JDK SPI 那样一次性实例化所有实现类。
    Dubbo 设计出了一个 ExtensionLoader 类,实现了 SPI 思想,也被称为 Dubbo SPI 机制。

    ///////////////////////////////////////////////////
    // Dubbo SPI 的测试启动类
    ///////////////////////////////////////////////////
    public class Dubbo14DubboSpiApplication {
        public static void main(String[] args) {
            ApplicationModel applicationModel = ApplicationModel.defaultModel();
            // 通过 Protocol 获取指定像 ServiceLoader 一样的加载器
            ExtensionLoader<IDemoSpi> extensionLoader = applicationModel.getExtensionLoader(IDemoSpi.class);
            
            // 通过指定的名称从加载器中获取指定的实现类
            IDemoSpi customSpi = extensionLoader.getExtension("customSpi");
            System.out.println(customSpi + ", " + customSpi.getDefaultPort());
            
            // 再次通过指定的名称从加载器中获取指定的实现类,看看打印的引用是否创建了新对象
            IDemoSpi customSpi2 = extensionLoader.getExtension("customSpi");
            System.out.println(customSpi2 + ", " + customSpi2.getDefaultPort());
        }
    }
    
    ///////////////////////////////////////////////////
    // 定义 IDemoSpi 接口并添加上了 @SPI 注解,
    // 其实也是在定义一种 SPI 思想的规范
    ///////////////////////////////////////////////////
    @SPI
    public interface IDemoSpi {
        int getDefaultPort();
    }
    
    ///////////////////////////////////////////////////
    // 自定义一个 CustomSpi 类来实现 IDemoSpi 接口
    // 该 IDemoSpi 接口被添加上了 @SPI 注解,
    // 其实也是在定义一种 SPI 思想的规范
    ///////////////////////////////////////////////////
    public class CustomSpi implements IDemoSpi {
        @Override
        public int getDefaultPort() {
            return 8888;
        }
    }
    
    ///////////////////////////////////////////////////
    // 资源目录文件
    // 路径为:/META-INF/dubbo/internal/com.hmilyylimh.cloud.dubbo.spi.IDemoSpi
    ///////////////////////////////////////////////////
    customSpi=com.hmilyylimh.cloud.dubbo.spi.CustomSpi
    

    打印结果:

    com.hmilyylimh.cloud.dubbo.spi.CustomSpi@143640d5, 8888
    com.hmilyylimh.cloud.dubbo.spi.CustomSpi@143640d5, 8888
    

    步骤:

    • 第一,定义一个 IDemoSpi 接口,并在该接口上添加 @SPI 注解。
    • 第二,定义一个 CustomSpi 实现类来实现该接口,然后通过 ExtensionLoader 的 getExtension 方法传入指定别名来获取具体的实现类。
    • 最后,在“/META-INF/services/com.hmilyylimh.cloud.dubbo.spi.IDemoSpi”这个资源文件中,添加实现类的类路径,并为类路径取一个别名(customSpi)。

    通过别名去获取指定的实现类时,打印的实例对象的引用是同一个,说明 Dubbo 框架做了缓存处理。而且整个操作,只通过一个简单的别名,就能从 ExtensionLoader 中拿到指定实现类,简单方便。

    SPI 的思想:
    主要是通过定制底层规范接口,在不同的业务场景,封装底层逻辑不变性,提供扩展点给到上层应用做不同的自定义开发实现,既可以用来提供框架扩展,也可以用来替换组件。

    极客时间《Dubbo 源码剖析与实战》学习笔记Day17 - http://gk.link/a/11VBp

    相关文章

      网友评论

        本文标题:Dubbo SPI

        本文链接:https://www.haomeiwen.com/subject/fumbhdtx.html