程序员

Dubbo SPI

2023-01-31  本文已影响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 方法执行多次时,会不断创建新的实例对象。

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 的问题

解决:

增加缓存,来降低磁盘 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

步骤:

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

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

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

上一篇下一篇

猜你喜欢

热点阅读