Dubbo SPI

本贴最后更新于 505 天前,其中的信息可能已经物是人非

Dubbo 在设计之初,为了架构的灵活性,在它架构中的每层为每个功能点都提供了一个 SPI 扩展接口(Dubbo 增强 SPI),Dubbo 框架在使用扩展点功能的时候是对接口进行依赖,而一个扩展接口对应了一系列的扩展实现类,那么如何选择具体的实现类呢?让我们来看看源码吧!

我们以 Protocol 接口为例:

可以看到 SPI 注解中的值为 dubbo,Dubbo 在运行过程中会使用动态编译技术为接口 Protocol 生成一个适配器类 Protocol$Adaptive 的实例,而这个实例的实现就对应 SPI 注解中所配置的 dubbo 对应的类的实现(具体是在 META-INFO/dubbo、META-INFO/dubbo/internal、META-INFO/services 这三个文件夹下的配置中配置的实现类),下面让我们看看 Dubbo 到底是怎么生成这个 Adaptive 的实例的吧!

@SPI("dubbo")
public interface Protocol {

    int getDefaultPort();

    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    void destroy();

    default List<ProtocolServer> getServers() {
        return Collections.emptyList();
    }
}

在 Provider 方我们会用到一个类叫 ServiceConfig,而在 ServiceConfig 中维护了一个静态内部常量 protocol,它使用了 ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension() 来获取是配置对象。

640.png

我们进入 getExtensionLoader 方法中可以看到这是一个静态方法,并且它为每一个 Dubbo 扩展接口都维护了一个 ExtensionLoader 实例,通过这个方法可以拿到对应 Dubbo 扩展接口的 ExtensionLoader。

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    if (type == null) {
        throw new IllegalArgumentException("Extension type == null");
    }
    if (!type.isInterface()) {
        throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
    }
    if (!withExtensionAnnotation(type)) {
        throw new IllegalArgumentException("Extension type (" + type +
                                           ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
    }

    ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    if (loader == null) {
        // 根据扩展接口的class来获取loader
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}

然后通过调用 ExtensionLoader 的实例方法 getAdaptiveExtension() 来获取适配器实例,这里的调用链比较长。

首先 ExtensionLoader 中维护了一个实例的缓存 cachedAdaptiveInstance,先判断缓存中是否存在,如果存在直接返回,然后通过双重校验的方式保证只会实例化一次,通过调用 createAdaptiveExtension() 方法来创建实例。

public T getAdaptiveExtension() {
    // 缓存
    Object instance = cachedAdaptiveInstance.get();
    if (instance == null) {
        if (createAdaptiveInstanceError == null) {
            synchronized (cachedAdaptiveInstance) {
                instance = cachedAdaptiveInstance.get();
                if (instance == null) {
                    try {
                        // 获取实例
                        instance = createAdaptiveExtension();
                        cachedAdaptiveInstance.set(instance);
                    } catch (Throwable t) {
                        createAdaptiveInstanceError = t;
                        throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
                    }
                }
            }
        } else {
            throw new IllegalStateException("Failed to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
        }
    }

    return (T) instance;
}

进入到 createAdaptiveExtension() 方法,这里的 injectExtension 方法主要是为 Extension$Adaptive 实例注入依赖,而构建实例的方法是 getAdaptiveExtensionClass().newInstance(),其中 getAdaptiveExtensionClass() 是生成动态编译的类,将该类注入到对应的 ClassLoader 中,而生成实例则是调用 newInstance() 方法。

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() 方法中,看看动态编译的类是怎么生成的。
可以看到 Dubbo 会先从 META-INF 路径下读取配置文件并加载 class 文件,在这里面会读取扩展接口上的 @SPI 的值,这里有个不一样的地方,大家知道这个适配器是为了选择自己想要的扩展,但是 Dubbo 将实现这个功能中的 Compiler(动态编译)也弄成了一个扩展,如果按照之前设计的走向,那么就会变成套娃无限循环,就是选择 Compiler 的一个实例需要一个 Compiler 实例去动态编译它,所以 Dubbo 又扩展了一个注解 @Adaptive,也就是这里的第二步,Adaptive 注解的作用用通俗的说法就是适配器的适配器,拿 Compiler 举例,@SPI 的适配器并不会选择最终的 Compiler 去实例化,而是会选择打上了 Adaptive 注解的 Compiler 去实例化,而这个打上了 Adaptive 注解的类会去实例化我们最终选择的 Compiler 类,可以在 dubbo-common 项目的 META-INFO/dubbo.internal 文件夹中找到它,而这个 AdaptiveCompiler 的 compile 方法会再去获取一次 SPI 注解上配置的 Compiler,然后用这个 Compiler 进行动态编译。

// ExtensionLoader.java
private Class<?> getAdaptiveExtensionClass() {
    // 1 从META-INF路径下的文件中读取定义的class文件
    // 这里会从几个固定的路径去读取配置文件信息
    getExtensionClasses();
    // 2 如果存在@Adaptive注解的类,则优先使用这个类
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    // 3 通过扩展接口上的注解配置名称来创建Adaptive的Class对象
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
private Class<?> createAdaptiveExtensionClass() {
    String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
    ClassLoader classLoader = findClassLoader();
    // 获取Compiler,如果没有Compiler则会先初始化Compiler
    org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    return compiler.compile(code, classLoader);
}
// ExtensionLoader.java
private Map<String, Class<?>> loadExtensionClasses() {
    // 获取SPI注解上的扩展名
    cacheDefaultExtensionName();

    Map<String, Class<?>> extensionClasses = new HashMap<>();
    // 从固定的几个路径中读取配置的class
    // loadDirectory中会处理Adaptive注解的class,会将其缓存到cachedAdaptiveClass变量中
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
    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;
}

641.png

// AdaptiveCompiler.java
public Class<?> compile(String code, ClassLoader classLoader) {
    Compiler compiler;
    ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
    String name = DEFAULT_COMPILER; // copy reference
    if (name != null && name.length() > 0) {
        compiler = loader.getExtension(name);
    } else {
        // 这里获取@SPI上配置的Compiler
        compiler = loader.getDefaultExtension();
    }
    return compiler.compile(code, classLoader);
}

所以可以看到,在第一次获取 SPI 的实例对象的时候,预加载完 class 文件后,如果没有找到对应的 Adaptive 类则会进行 class 的构建,而第一次构建的时候 Compiler 并没有被初始化,这个时候会先走一遍 Compiler 的初始化,将 AdaptiveCompiler 作为 Compiler 的对应实例,然后通过 AdaptiveCompiler 去初始化 Compiler 接口 SPI 注解中的对应实例,最后通过 Compiler 对需要的实例类进行初始化,最后通过 newInstance 获取实例。

ok,源码到这里就分析结束了,谢谢~

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3167 引用 • 8207 回帖
  • SPI

    Service Provider Interface

    12 引用 • 2 回帖
  • What
    6 引用
  • Dubbo

    Dubbo 是一个分布式服务框架,致力于提供高性能和透明化的 RPC 远程服务调用方案,是 [阿里巴巴] SOA 服务化治理方案的核心框架,每天为 2,000+ 个服务提供 3,000,000,000+ 次访问量支持,并被广泛应用于阿里巴巴集团的各成员站点。

    60 引用 • 82 回帖 • 603 关注

相关帖子

回帖

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • 心情

    心是产生任何想法的源泉,心本体会陷入到对自己本体不能理解的状态中,因为心能产生任何想法,不能分出对错,不能分出自己。

    59 引用 • 369 回帖
  • uTools

    uTools 是一个极简、插件化、跨平台的现代桌面软件。通过自由选配丰富的插件,打造你得心应手的工具集合。

    5 引用 • 13 回帖
  • Hexo

    Hexo 是一款快速、简洁且高效的博客框架,使用 Node.js 编写。

    21 引用 • 140 回帖 • 25 关注
  • V2Ray
    1 引用 • 15 回帖
  • 小说

    小说是以刻画人物形象为中心,通过完整的故事情节和环境描写来反映社会生活的文学体裁。

    28 引用 • 108 回帖 • 2 关注
  • Bootstrap

    Bootstrap 是 Twitter 推出的一个用于前端开发的开源工具包。它由 Twitter 的设计师 Mark Otto 和 Jacob Thornton 合作开发,是一个 CSS / HTML 框架。

    18 引用 • 33 回帖 • 685 关注
  • OnlyOffice
    4 引用 • 27 关注
  • 房星科技

    房星网,我们不和没有钱的程序员谈理想,我们要让程序员又有理想又有钱。我们有雄厚的房地产行业线下资源,遍布昆明全城的 100 家门店、四千地产经纪人是我们坚实的后盾。

    6 引用 • 141 回帖 • 553 关注
  • Firefox

    Mozilla Firefox 中文俗称“火狐”(正式缩写为 Fx 或 fx,非正式缩写为 FF),是一个开源的网页浏览器,使用 Gecko 排版引擎,支持多种操作系统,如 Windows、OSX 及 Linux 等。

    7 引用 • 30 回帖 • 455 关注
  • Angular

    AngularAngularJS 的新版本。

    26 引用 • 66 回帖 • 511 关注
  • Unity

    Unity 是由 Unity Technologies 开发的一个让开发者可以轻松创建诸如 2D、3D 多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

    25 引用 • 7 回帖 • 250 关注
  • 分享

    有什么新发现就分享给大家吧!

    242 引用 • 1746 回帖 • 1 关注
  • iOS

    iOS 是由苹果公司开发的移动操作系统,最早于 2007 年 1 月 9 日的 Macworld 大会上公布这个系统,最初是设计给 iPhone 使用的,后来陆续套用到 iPod touch、iPad 以及 Apple TV 等产品上。iOS 与苹果的 Mac OS X 操作系统一样,属于类 Unix 的商业操作系统。

    84 引用 • 139 回帖 • 1 关注
  • SQLServer

    SQL Server 是由 [微软] 开发和推广的关系数据库管理系统(DBMS),它最初是由 微软、Sybase 和 Ashton-Tate 三家公司共同开发的,并于 1988 年推出了第一个 OS/2 版本。

    19 引用 • 31 回帖 • 3 关注
  • 导航

    各种网址链接、内容导航。

    37 引用 • 168 回帖
  • JSON

    JSON (JavaScript Object Notation)是一种轻量级的数据交换格式。易于人类阅读和编写。同时也易于机器解析和生成。

    51 引用 • 190 回帖 • 2 关注
  • 星云链

    星云链是一个开源公链,业内简单的将其称为区块链上的谷歌。其实它不仅仅是区块链搜索引擎,一个公链的所有功能,它基本都有,比如你可以用它来开发部署你的去中心化的 APP,你可以在上面编写智能合约,发送交易等等。3 分钟快速接入星云链 (NAS) 测试网

    3 引用 • 16 回帖 • 5 关注
  • Facebook

    Facebook 是一个联系朋友的社交工具。大家可以通过它和朋友、同事、同学以及周围的人保持互动交流,分享无限上传的图片,发布链接和视频,更可以增进对朋友的了解。

    4 引用 • 15 回帖 • 454 关注
  • 单点登录

    单点登录(Single Sign On)是目前比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

    9 引用 • 25 回帖 • 3 关注
  • 书籍

    宋真宗赵恒曾经说过:“书中自有黄金屋,书中自有颜如玉。”

    76 引用 • 390 回帖 • 1 关注
  • frp

    frp 是一个可用于内网穿透的高性能的反向代理应用,支持 TCP、UDP、 HTTP 和 HTTPS 协议。

    15 引用 • 7 回帖 • 9 关注
  • Mobi.css

    Mobi.css is a lightweight, flexible CSS framework that focus on mobile.

    1 引用 • 6 回帖 • 696 关注
  • Maven

    Maven 是基于项目对象模型(POM)、通过一小段描述信息来管理项目的构建、报告和文档的软件项目管理工具。

    185 引用 • 318 回帖 • 348 关注
  • danl
    61 关注
  • Rust

    Rust 是一门赋予每个人构建可靠且高效软件能力的语言。Rust 由 Mozilla 开发,最早发布于 2014 年 9 月。

    57 引用 • 22 回帖 • 2 关注
  • SEO

    发布对别人有帮助的原创内容是最好的 SEO 方式。

    35 引用 • 200 回帖 • 24 关注
  • AngularJS

    AngularJS 诞生于 2009 年,由 Misko Hevery 等人创建,后为 Google 所收购。是一款优秀的前端 JS 框架,已经被用于 Google 的多款产品当中。AngularJS 有着诸多特性,最为核心的是:MVC、模块化、自动化双向数据绑定、语义化标签、依赖注入等。2.0 版本后已经改名为 Angular。

    12 引用 • 50 回帖 • 422 关注