Dubbo 自适应SPI

news2024/11/26 10:22:21

Dubbo 自适应SPI

1. 原理

在 Dubbo 中,很多拓展都是通过 SPI 机制进行加载的,比如 Protocol、Cluster、LoadBalance 等。有时,有些拓展并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。这听起来有些矛盾。拓展未被加载,那么拓展方法就无法被调用(静态方法除外)。拓展方法未被调用,拓展就无法被加载。对于这个矛盾的问题,Dubbo 通过自适应拓展机制很好的解决了。自适应拓展机制的实现逻辑比较复杂,首先 Dubbo 会为拓展接口生成具有代理功能的代码。然后通过 javassist 或 jdk 编译这段代码,得到 Class 类。最后再通过反射创建代理类。那么在调用接口的方法的时候,就是通过代理类来进行调用。这就是Dubbo 自适应SPI的原理。

注意:本次的源码分析是基于2.7.8.

2. 原理

Adaptive 接口

 @Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
    String[] value() default {};

}

根据 value()方法的注释,可以简单的理解为:根据URL中的某个参数的值,来决定注入哪个拓展对象。而参数的名称就由value()这个方法来决定。 如果根据value()方法给定的参数名称,无法从URL中获取到值,那么就会使用默认拓展。

获得自适应拓展

getAdaptiveExtension 方法是获取自适应拓展的入口方法。

  public T getAdaptiveExtension() {
        // 从缓存中获取自适应拓展
        Object instance = cachedAdaptiveInstance.get();
        // 缓存未命中
        if (instance == null) {
            if (createAdaptiveInstanceError != null) {
                throw new IllegalStateException("Failed to create adaptive instance: " +
                        createAdaptiveInstanceError.toString(),
                        createAdaptiveInstanceError);
            }

            // 根据双重检测锁创建自适应拓展,并且加载到缓存中
            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);
                    }
                }
            }
        }

        return (T) instance;
    }ersion>
    </dependency>

getAdaptiveExtension 方法首先会检查缓存,缓存未命中,则调用 createAdaptiveExtension 方法创建自适应拓展。下面,我们看一下 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);
        }
    }

createAdaptiveExtension 方法的代码比较少,但却包含了三个逻辑,分别如下:

调用 getAdaptiveExtensionClass 方法获取自适应拓展 Class 对象
通过反射进行实例化
调用 injectExtension 方法向拓展实例中注入依赖
前两个逻辑比较好理解,第三个逻辑用于向自适应拓展对象中注入依赖。这个逻辑看似多余,但有存在的必要,这里简单说明一下。前面说过,Dubbo 中有两种类型的自适应拓展,一种是手工编码的,一种是自动生成的。手工编码的自适应拓展中可能存在着一些依赖,而自动生成的 Adaptive 拓展则不会依赖其他类。这里调用 injectExtension 方法的目的是为手工编码的自适应拓展注入依赖,这一点需要大家注意一下。关于 injectExtension 方法,前文已经分析过了,这里不再赘述。接下来,分析 getAdaptiveExtensionClass 方法的逻辑。

 private Class<?> getAdaptiveExtensionClass() {

        // 通过 SPI 获取所有的拓展类
        getExtensionClasses();

        // 检查缓存,若缓存不为空,则直接返回缓存
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }

        // 创建自适应拓展类
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

这三个逻辑看起来平淡无奇,似乎没有多讲的必要。但是这些平淡无奇的代码中隐藏了着一些细节,需要说明一下。首先从第一个逻辑说起,getExtensionClasses 这个方法用于获取某个接口的所有实现类。比如该方法可以获取 Protocol 接口的 DubboProtocol、HttpProtocol、InjvmProtocol 等实现类。在获取实现类的过程中,如果某个实现类被 Adaptive 注解修饰了,那么该类就会被赋值给 cachedAdaptiveClass 变量。此时,上面步骤中的第二步条件成立(缓存不为空),直接返回 cachedAdaptiveClass 即可。如果所有的实现类均未被 Adaptive 注解修饰,那么执行第三步逻辑,创建自适应拓展类。相关代码如下:

   private Class<?> createAdaptiveExtensionClass() {

        // 构建自适应拓展代码
        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
        ClassLoader classLoader = findClassLoader();
        // 获取编译器实现类
        org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        // 编译代码,生成 Class
        return compiler.compile(code, classLoader);
    }

createAdaptiveExtensionClass 方法用于生成自适应拓展类,该方法首先会生成自适应拓展类的源码,然后通过 Compiler 实例(Dubbo 默认使用 javassist 作为编译器)编译源码,得到代理类 Class 实例。接下来,我们把重点放在代理类代码生成的逻辑上,其他逻辑大家自行分析。

自适应拓展代码生成

自适应拓展代码是在如下一行代码生成的。

        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();

来看一下 generate 的主要逻辑。过 Adaptive 注解检测,即可开始生成代码。代码生成的顺序与 Java 文件内容顺序一致,首先会生成 package 语句,然后生成 import 语句,紧接着生成类名,生成方法等代码。如下:

  public String generate() {
        // no need to generate adaptive class since there's no adaptive method found.
        // 检测接口的所有方法是否至少有一个接口标注了 @Adaptive注解
        if (!hasAdaptiveMethod()) {
            throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
        }

        StringBuilder code = new StringBuilder();
        // 生成 package 代码:package + type 所在包,例如 package org.apache.dubbo.rpc;
        code.append(generatePackageInfo());
        // 生成 import 代码:import + ExtensionLoader 全限定名,例如 import org.apache.dubbo.common.extension.ExtensionLoader;
        code.append(generateImports());
        // 生成类代码:public class + type简单名称 + $Adaptive + implements + type全限定名 + {,
        // 例如: public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
        code.append(generateClassDeclaration());

        // 生成方法
        Method[] methods = type.getMethods();
        for (Method method : methods) {
            code.append(generateMethod(method));
        }
        code.append("}");

        if (logger.isDebugEnabled()) {
            logger.debug(code.toString());
        }
//        logger.info("generate code = {}", code.toString());
        System.out.println("generate code = " + JSONObject.toJSONString(code));
        return code.toString();
    }

来看一下生成方法 (generate Method) 的主要逻辑。首先会生成方法返回值,然后生成方法名称,接着生成方法内容,方法参数,异常信息,最后进行拼接。如下:

  /**
     * generate method declaration
     */
    private String generateMethod(Method method) {
        //方法的返回值: 例如 org.apache.dubbo.rpc.Exporter
        String methodReturnType = method.getReturnType().getCanonicalName();
        //方法名称, 例如export
        String methodName = method.getName();
        //方法内容
        String methodContent = generateMethodContent(method);
        //方法参数 org.apache.dubbo.rpc.Invoker arg0
        String methodArgs = generateMethodArguments(method);
        //异常信息 throws org.apache.dubbo.rpc.RpcException
        String methodThrows = generateMethodThrows(method);
        return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
    }

来看一下 生成方法内容 (generateMethodContent) 的主要逻辑。如下:

 /**
     * generate method content
     */
    private String generateMethodContent(Method method) {
        // 检测方法是否有 Adaptive注解
        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
        StringBuilder code = new StringBuilder(512);
        if (adaptiveAnnotation == null) {
            // dubbo 不会为没有标注 Adaptive 注解的方法生成代理逻辑,对于该种类型的方法,仅会生成一句抛出异常的代码。生成逻辑如下:
            return generateUnsupported(method);
        } else {
            // 因为在自适应拓展中,必须从URL中提取目标拓展的名称。因此代码生成逻辑的一个重要的任务是从方法的参数列表或者其他参数中获取 URL 数据。
            int urlTypeIndex = getUrlTypeIndex(method);

            // found parameter in URL type
            if (urlTypeIndex != -1) {
                // Null Point check
                // 如果有URL参数,为 URL 类型参数生成判空代码,例如: 在Protocol 接口的 refer方法:
                // if (arg1 == null) throw new IllegalArgumentException("url == null");
                // org.apache.dubbo.common.URL url = arg1;
                code.append(generateUrlNullCheck(urlTypeIndex));
            } else {
                // did not find parameter in URL type
                // 如果不包含URL参数,那么需要从其它参数中获取得到URL参数,例如在Protocol 接口的 expore方法,
                // 就是从 Invoker 这个参数中获取 URL参数的
                // 生成的代码如下:
                // if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
                //if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
                //org.apache.dubbo.common.URL url = arg0.getUrl();
                code.append(generateUrlAssignmentIndirectly(method));
            }

            // 获取 拓展名称
            String[] value = getMethodAdaptiveValue(adaptiveAnnotation);

            // 此段逻辑是检测方法列表中是否存在 Invocation 类型的参数,若存在,则为其生成判空代码和其他一些代码
            boolean hasInvocation = hasInvocationArgument(method);

            code.append(generateInvocationArgumentNullCheck(method));

            // 例如: String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
            code.append(generateExtNameAssignment(value, hasInvocation));
            // 例如: if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name
            // from url (" + url.toString() + ") use keys([protocol])");
            code.append(generateExtNameNullCheck(value));
            // 例如: org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader
            // (org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
            code.append(generateExtensionAssignment());

            // 例如:return extension.export(arg0);
            code.append(generateReturnAndInvocation(method));
        }

        return code.toString();
    }

来看一下最后构建自适应拓展代码的示例。此例子是生成 Protocol 接口的代码。

 package org.apache.dubbo.rpc;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
public void destroy()  {
throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
}
public int getDefaultPort()  {
throw new UnsupportedOperationException("The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
}
public java.util.List getServers()  {
throw new UnsupportedOperationException("The method public default java.util.List org.apache.dubbo.rpc.Protocol.getServers() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
}
public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {
if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
org.apache.dubbo.common.URL url = arg0.getUrl();
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.export(arg0);
}
public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
if (arg1 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg1;
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.refer(arg0, arg1);
}
}

采用装饰器模式进行功能增强

采用装饰器模式进行功能增强,自动包装实现,这种实现的类一般是自动激活的,常用于包装类,比如:Protocol的两个实现类:ProtocolFilterWrapper、ProtocolListenerWrapper。

例如:接口A的另一个实现者AWrapper1。大体内容如下:

private A a;
AWrapper1(A a){
   this.a=a;
}

因此,当在获取某一个接口A的实现者A1的时候,已经自动被AWrapper1包装了。

接下来来看一下源码中是如何实现的。

dubbo中存在 一种对于扩展的封装类,其功能是将各扩展实例串联起来,形成扩展链,比如过滤器链,监听链。当调用ExtensionLoader的getExtension方法时,会做拦截处理,如果存在封装器,则返回封装器实现,而将真实实现通过构造方法注入到封装器中。

在这里插入图片描述

这里有个injectExtension方法,其作用是:

如果当前扩展实例存在其他的扩展属性,则通过反射调用其set方法设置扩展属性。若该扩展属性是适配器类型,也是通过ExtensionLoader获取的。

3. 总结

到此,关于自适应拓展的原理,实现就分析完了。总的来说自适应拓展整个逻辑还是很复杂的,并不是很容易弄懂。因此,大家在阅读该部分源码时,耐心一些。同时多进行调试,也可以通过生成好的代码思考代码的生成逻辑。

总结起来说,为啥 Dubbo 自适应SPI可以做到根据运行时参数进行加载呢?因为 在调用getAdaptiveExtension() 这个方法的时候,会根据 JavassistProxy 生成一个代理类。在调用接口的方法,会被代理到 代理类。代理类会根据 URL 中制定的参数名称的值来动态加载某个拓展实例,从而实现了动态加载的目标。

4. 鸣谢

核心源码-SPI扩展

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/167291.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

录屏软件无水印免费,分享一款功能强大且免费的录屏软件

市面上多数录屏软件&#xff0c;只能试用版录制几分钟的视频&#xff0c;且带有水印。想要长时间录制电脑屏幕、录制无水印的录屏&#xff0c;需要解锁才可以。那有没有一款录屏软件试用版就能无水印&#xff1f;当然有啦。小编今天给大家分享一款不限制录制时长&#xff0c;且…

springboot整合Freemarker模板引擎

2.2 模板引擎 2.2.1 什么是模板引擎 根据前边的数据模型分析&#xff0c;课程预览就是把课程的相关信息进行整合&#xff0c;在课程预览界面进行展示&#xff0c;课程预览界面与课程发布的课程详情界面一致&#xff0c;保证了教学机构人员发布前看到什么样&#xff0c;发布后…

【Win11 + VSCode配置OpenCV C++一站式开发调试环境教程】

Win11 VSCode配置OpenCV C一站式开发调试环境教程1 下载1.1 版本介绍&#xff1a;1.2 对应三个软件的连接&#xff1a;2 环境配置3 编译1 下载 需要下载三个软件&#xff1a;OpenCV 、MInGW、CMake 1.1 版本介绍&#xff1a; 打开 OpenCV-MinGW-Build&#xff1a;OpenCV-4.…

Android启动流程源码分析(基于Android S)

从上图我们可以清楚的看到Android系统的启动分为以下几个步骤 启动电源以及启动系统 当我们按下电源键时, 引导芯片代码开始从预定义的地方(固化在ROM)开始执行, 加载引导程序到RAM, 然后执行 引导程序 引导程序是在Android操作系统开始运行前的一个小程序. 引导程序是运行的…

图片转PDF怎么弄?这几个方法值得你试一试

PDF是一种特殊的文件格式&#xff0c;它可以在任何设备和平台上进行传输&#xff0c;并且能够保证文件版式不被修改&#xff0c;此外&#xff0c;还可以兼容不同的系统&#xff0c;因为它的这些优势&#xff0c;大多数的人就喜欢将自己编辑好的WORD、PPT、EXCEL、图片等文件转换…

MySQL InnoDB的MVCC实现机制

MySQL InnoDB的MVCC实现机制1.MVCC概述2.MVCC的实现原理隐式字段undo日志Read View(读视图)RR隔离级别的Read View方案1.MVCC概述 什么是MVCC&#xff1f; MVCC&#xff0c;即多版本并发控制。MVCC是一种并发控制的方法&#xff0c;一般在数据库管理系统中&#xff0c;实现对…

YOLOV8——快速训练指南(上手教程、自定义数据训练)

概述 本篇主要用于说明如何使用自己的训练数据&#xff0c;快速在YOLOV8 框架上进行训练。当前&#xff08;20230116&#xff09;官方文档和网上的资源主要都是在开源的数据集上进行测试&#xff0c;对于算法“小白”或者“老鸟”如何快速应用到自己的项目中&#xff0c;这…

操作系统IO控制方式

操作系统I&O控制方式 视频地址&#xff1a;https://www.bilibili.com/video/BV1YE411D7nH?p64 I&O设备按照信息交换的单位可以分为以下两类&#xff1a; 块设备 数据传输的基本单位是块&#xff0c;传输速率较高&#xff0c;可寻址&#xff0c;可随机读写任意一块。…

78.循环神经网络(RNN)

1. 潜变量自回归模型 2. 循环神经网络 计算损失是比较ot和xt之间来计算损失&#xff0c;但是xt是用来更新ht&#xff0c;使得其挪到下一个单元。 用一个额外的whh来存时序信息。 3. 使用循环神经网络的语言模型 4. 困惑度&#xff08;perplexity&#xff09; 5. 梯度剪裁 g表…

《Stealth秘密行动》游戏开发记录

游戏开发的学习记录项目&#xff1a;Stealth秘密行动开始时间&#xff1a;2022.12.30一、新学到的&#xff1a;二、遇到的问题&#xff1a;三、成品部分展示&#xff1a;游戏开发的学习记录⑧ 项目&#xff1a;Stealth秘密行动 开始时间&#xff1a;2022.12.30 &#xff08;…

数据分析-深度学习Pytorch Day6

卷积神经网络如何运用到图片分类问题感受野 Receptive Field步长Stride填充Padding参数共享share parameter最大池化MaxPoolingCNN全过程仅个人理解学习引言CNN卷积神经网络最初主要是用于计算机视觉和图像处理中&#xff0c;比如图像分类&#xff1a;最终的分类数绝对维度&…

【代码随想录】哈希表-golang

哈希表 from 代码随想录 hash表解法可以是slice,map…&#xff0c;目的是将时间复杂度降为O(1) 有效的字母异位词 给定两个字符串 s 和 t &#xff0c;编写一个函数来判断 t 是否是 s 的字母异位词。 排序 思路&#xff1a;直接重新声明字符的字节形式&#xff0c;然后对其进行…

Vue的依赖收集和性能问题

什么是依赖收集Vue能够实现当一个数据变更时&#xff0c;视图就进行刷新&#xff0c;而且用到这个数据的其他地方也会同步变更&#xff1b;而且&#xff0c;这个数据必须是在有被依赖的情况下&#xff0c;视图和其他用到数据的地方才会变更。 所以&#xff0c;Vue要能够知道一个…

逻辑思维训练1200题-蓝桥杯计算思维参考

黑格尔曾说过&#xff0c;逻辑是一切思考的基础。逻辑思维能力强的人能迅速、准确地把握住问题的实质&#xff0c;面对纷繁复杂的事情能更容易找到解决的办法。《逻辑思维训练1200 题》介绍了排除法、递推法、倒推法、作图法、假设法、计算法、分析法、类比法、推理法、判断法、…

自动化 | 这些常用测试平台,你们公司在用的是哪些呢?

本文节选自霍格沃兹测试学院内部教材测试管理平台是贯穿测试整个生命周期的工具集合&#xff0c;它主要解决的是测试过程中团队协作的问题。在整个测试过程中&#xff0c;需要对测试用例、Bug、代码、持续集成等等进行管理。下面分别从这四个方面介绍现在比较流行的管理平台。6…

四、template模板

模板 之前的案例中&#xff0c;能够返回简单的字符串信息给浏览器。那如果想要返回html页面给浏览器该怎么做呢&#xff1f; 当然&#xff0c;我们可以这么写&#xff1a; def index(request):return HttpResponse(<h1 style"color:red">我是硬编码的</h…

23年3月如何准备pmp考试?

首先要把PMP考试如何报名、考试内容等都要了解清楚&#xff0c;再去备考。<<PMP入门知识>>PMP考试时长&#xff1a;230分钟。PMP考试形式&#xff1a;笔试。PMP考试题型&#xff1a;题型包括单选题和多选题&#xff0c;多选题将说明需选择几个正确选项。PMP考试题量…

NEUQ week11题解

P1796 汤姆斯的天堂梦 汤姆斯的天堂梦 题目描述 汤姆斯生活在一个等级为 000 的星球上。那里的环境极其恶劣&#xff0c;每天 121212 小时的工作和成堆的垃圾让人忍无可忍。他向往着等级为 NNN 的星球上天堂般的生活。 有一些航班将人从低等级的星球送上高一级的星球&#…

【Java寒假打卡】Java基础-网络编程UDP和TCP

【Java寒假打卡】Java基础-网络编程UDP和TCP网络编程的三要素网络编程的常见命令InetAddress类端口协议UDP发送数据UDP接受数据UDP通信程序的练习TCP通信程序-发送数据TCP通信程序-接受数据网络编程的三要素 网络编程的常见命令 ipconfig 查看本机IP地址ping IP地址&#xff1…

【系列03】方法的学习 方法重载 命令行传参 递归 简单计算机 [有目录]

方法的学习 什么是方法 方法是解决一类问题的步骤的有序组合包含于类或者对象之中方法在程序中被创建,在其他地方被引用 就比如输出方法如:System.out.println(); 就是被封装好的方法 方法设计原则:一个方法完成一个功能,利于后期扩展 [原子性] 使用方法: public class D…