Dubbo SPI

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

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 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3201 引用 • 8216 回帖 • 4 关注
  • SPI

    Service Provider Interface

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

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

    60 引用 • 82 回帖 • 615 关注

相关帖子

回帖

欢迎来到这里!

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

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

推荐标签 标签

  • 印象笔记
    3 引用 • 16 回帖
  • 大数据

    大数据(big data)是指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合,是需要新处理模式才能具有更强的决策力、洞察发现力和流程优化能力的海量、高增长率和多样化的信息资产。

    89 引用 • 113 回帖
  • 域名

    域名(Domain Name),简称域名、网域,是由一串用点分隔的名字组成的 Internet 上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位(有时也指地理位置)。

    43 引用 • 208 回帖
  • Lute

    Lute 是一款结构化的 Markdown 引擎,支持 Go 和 JavaScript。

    29 引用 • 202 回帖 • 28 关注
  • Ant-Design

    Ant Design 是服务于企业级产品的设计体系,基于确定和自然的设计价值观上的模块化解决方案,让设计者和开发者专注于更好的用户体验。

    17 引用 • 23 回帖 • 4 关注
  • Elasticsearch

    Elasticsearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

    117 引用 • 99 回帖 • 194 关注
  • abitmean

    有点意思就行了

    34 关注
  • IDEA

    IDEA 全称 IntelliJ IDEA,是一款 Java 语言开发的集成环境,在业界被公认为最好的 Java 开发工具之一。IDEA 是 JetBrains 公司的产品,这家公司总部位于捷克共和国的首都布拉格,开发人员以严谨著称的东欧程序员为主。

    181 引用 • 400 回帖 • 2 关注
  • 链滴

    链滴是一个记录生活的地方。

    记录生活,连接点滴

    180 引用 • 3878 回帖
  • 反馈

    Communication channel for makers and users.

    120 引用 • 906 回帖 • 280 关注
  • V2EX

    V2EX 是创意工作者们的社区。这里目前汇聚了超过 400,000 名主要来自互联网行业、游戏行业和媒体行业的创意工作者。V2EX 希望能够成为创意工作者们的生活和事业的一部分。

    16 引用 • 236 回帖 • 251 关注
  • GAE

    Google App Engine(GAE)是 Google 管理的数据中心中用于 WEB 应用程序的开发和托管的平台。2008 年 4 月 发布第一个测试版本。目前支持 Python、Java 和 Go 开发部署。全球已有数十万的开发者在其上开发了众多的应用。

    14 引用 • 42 回帖 • 820 关注
  • Kubernetes

    Kubernetes 是 Google 开源的一个容器编排引擎,它支持自动化部署、大规模可伸缩、应用容器化管理。

    118 引用 • 54 回帖 • 3 关注
  • Shell

    Shell 脚本与 Windows/Dos 下的批处理相似,也就是用各类命令预先放入到一个文件中,方便一次性执行的一个程序文件,主要是方便管理员进行设置或者管理用的。但是它比 Windows 下的批处理更强大,比用其他编程程序编辑的程序效率更高,因为它使用了 Linux/Unix 下的命令。

    125 引用 • 74 回帖 • 1 关注
  • Spark

    Spark 是 UC Berkeley AMP lab 所开源的类 Hadoop MapReduce 的通用并行框架。Spark 拥有 Hadoop MapReduce 所具有的优点;但不同于 MapReduce 的是 Job 中间输出结果可以保存在内存中,从而不再需要读写 HDFS,因此 Spark 能更好地适用于数据挖掘与机器学习等需要迭代的 MapReduce 的算法。

    74 引用 • 46 回帖 • 568 关注
  • FreeMarker

    FreeMarker 是一款好用且功能强大的 Java 模版引擎。

    23 引用 • 20 回帖 • 465 关注
  • Latke

    Latke 是一款以 JSON 为主的 Java Web 框架。

    71 引用 • 535 回帖 • 829 关注
  • Log4j

    Log4j 是 Apache 开源的一款使用广泛的 Java 日志组件。

    20 引用 • 18 回帖 • 34 关注
  • API

    应用程序编程接口(Application Programming Interface)是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。

    79 引用 • 431 回帖
  • Solo

    Solo 是一款小而美的开源博客系统,专为程序员设计。Solo 有着非常活跃的社区,可将文章作为帖子推送到社区,来自社区的回帖将作为博客评论进行联动(具体细节请浏览 B3log 构思 - 分布式社区网络)。

    这是一种全新的网络社区体验,让热爱记录和分享的你不再感到孤单!

    1443 引用 • 10082 回帖 • 497 关注
  • TensorFlow

    TensorFlow 是一个采用数据流图(data flow graphs),用于数值计算的开源软件库。节点(Nodes)在图中表示数学操作,图中的线(edges)则表示在节点间相互联系的多维数据数组,即张量(tensor)。

    20 引用 • 19 回帖
  • Angular

    AngularAngularJS 的新版本。

    26 引用 • 66 回帖 • 559 关注
  • PWL

    组织简介

    用爱发电 (Programming With Love) 是一个以开源精神为核心的民间开源爱好者技术组织,“用爱发电”象征开源与贡献精神,加入组织,代表你将遵守组织的“个人开源爱好者”的各项条款。申请加入:用爱发电组织邀请帖
    用爱发电组织官网:https://programmingwithlove.stackoverflow.wiki/

    用爱发电组织的核心驱动力:

    • 遵守开源守则,体现开源&贡献精神:以分享为目的,拒绝非法牟利。
    • 自我保护:使用适当的 License 保护自己的原创作品。
    • 尊重他人:不以各种理由、各种漏洞进行未经允许的抄袭、散播、洩露;以礼相待,尊重所有对社区做出贡献的开发者;通过他人的分享习得知识,要留下足迹,表示感谢。
    • 热爱编程、热爱学习:加入组织,热爱编程是首当其要的。我们欢迎热爱讨论、分享、提问的朋友,也同样欢迎默默成就的朋友。
    • 倾听:正确并恳切对待、处理问题与建议,及时修复开源项目的 Bug ,及时与反馈者沟通。不抬杠、不无视、不辱骂。
    • 平视:不诋毁、轻视、嘲讽其他开发者,主动提出建议、施以帮助,以和谐为本。只要他人肯努力,你也可能会被昔日小看的人所超越,所以请保持谦虚。
    • 乐观且活跃:你的努力决定了你的高度。不要放弃,多年后回头俯瞰,才会发现自己已经成就往日所仰望的水平。积极地将项目开源,帮助他人学习、改进,自己也会获得相应的提升、成就与成就感。
    1 引用 • 487 回帖 • 1 关注
  • CodeMirror
    2 引用 • 17 回帖 • 166 关注
  • OpenCV
    15 引用 • 36 回帖 • 7 关注
  • MongoDB

    MongoDB(来自于英文单词“Humongous”,中文含义为“庞大”)是一个基于分布式文件存储的数据库,由 C++ 语言编写。旨在为应用提供可扩展的高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似 JSON 的 BSON 格式,因此可以存储比较复杂的数据类型。

    91 引用 • 59 回帖 • 5 关注
  • 心情

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

    59 引用 • 369 回帖 • 2 关注