Dubbo之SPI、Adaptive机制详解

Dubbo(八)内核解析(DubboSPI机制Adaptive机制、Wrapper机制、Activate机制 Dubbo 的内核解析 所谓 Dubbo 的内核是指,Dubbo 中所有功能都是基于它之上完成的,都是由它作为基础的。Dubbo 内核的工作原理由四部分构成:服务发现机制 SPI、自适应机制 Adaptive、包装机制 Wrapper 与激活机制 Activate。 Dubbo 通过这四种机制实现了对插件的 IoC、AOP,实现了对自动生成类的动态编译 Compile。 1. JDK 的 SPI 机制 1.1 简介 SPI,Service Provider Interface,服务提供者接口,是一种 阅读详情

前言

SPI是Dubbo框架的核心机制之一,其利用自定义SPI实现各组件的高扩展性,本文尝试对SPI机制原理进行分析并和JDK、Spring中的SPI机制进行横向对比,并详细介绍Dubbo 中Adaptive机制的实现机理。

JDK SPI

JDK中自带的SPI机制主要通过java.util.ServiceLoader类实现,其是一个A simple service-provider loading facility.,即:一个简单的服务提供者加载设施,说明ServiceLoader类是用来加载service-provider的。这里的service通常是一个well-known set of interfaces,而service-provider则是某个具体实现。service-provider通常通过jar files形式通过extension directories或者class path等方式进行加载安装。并建议service-provider并不是一个完整的实现类,而是一个proxy形式,其包含足够多的信息用来创建真正用于处理实际请求的actual provider,这个描述比较符合Dubbo中的@Adaptive机制。ServiceLoader对于service-provider的唯一要求是provider classes必须有zero-argument constructor-无参构造器,这样才能被ServiceLaoder实例化进行加载。

ServiceLoader

ServiceLoader属性如下:

    private static final String PREFIX = "META-INF/services/";
    private final Class<S> service;
    private final ClassLoader loader;
    private final AccessControlContext acc;
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    private LazyIterator lookupIterator;
  • PREFIX
    PREFIX指明了SPI配置文件的路径,以目标接口限定名为文件名,里面包含具体实现,换行分割。
  • service
    service就是要加载的service,通常是一个接口或者抽象类,例如java.sql.Driver
  • loader
    类加载器,默认使用当前线程中获取到的AppClassLoader,也可以自定义:
    ClassLoader cl = Thread.currentThread().getContextClassLoader();,在c = Class.forName(cn, false, loader);处调用。
  • acc
    AccessControllerContext是Java的一个安全策略,其主要根据调用栈的快照来进行权限检查,具体可参考Java之AccessController安全模型介绍。
  • providers
    存储已经加载service-provider,使用LinkedHashMap,说明加载有序的,key是实现类的完全限定名,value则是加载后的provider对象实例。
  • lookupIterator
    迭代器,是真正实现触发service-provider加载的入口,其本身是ServiceLoader的一个实现了迭代器接口的内部类,在 next()方法中真正实现类的加载: c = Class.forName(cn, false, loader);

ServiceLoader特点

最大的特点是其真正加载service-provider的入口在迭代器的next()方法中,这就导致遍历会把所有的provider类都加经过实例化进行加载,不是按需加载,不够灵活。某些比较消耗资源的provider会导致资源浪费。

Spring SPI

Spring的SPI机制是在SpringBoot中引入的,通过在class path中引入META-INF/spring.factories配置文件,把所有的SPI配置放置在一个配置文件中,在其中通过interface限定名=实现类名方式注明,但在SpringBoot3中不再支持这一机制。
一个示例如下:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

通过org.springframework.core.io.support.SpringFactoriesLoader进行加载,SpringFactoriesLoader就如同ServiceLoader,其提供如下代码进行加载:

List<EnableAutoConfiguration> strings = SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, this.getClass().getClassLoader());

同JDK SPI机制一样,SpringFactoriesLoader也只提供加载全部实现类的API,其加载指定实现类的API是定义为private的,外部无法调用。

Dubbo SPI

dubbo SPI 中负责加载实现类的模块名为org.apache.dubbo.common.extension.ExtensionLoader,配置文件则是在class path下的META-INF/services/、META-INF/dubbo/、META-INF/dubbo/internal路径,通JDK中一样,每个interface都有一个配置文件,且以全限定名为文件名,但配置内容是以key=value方式,key是某个实现类的标识,value则是实现类的完全限定名,例如接口org.apache.dubbo.common.threadpool.ThreadPool,其配置内容如下:

fixed=org.apache.dubbo.common.threadpool.support.fixed.FixedThreadPool
cached=org.apache.dubbo.common.threadpool.support.cached.CachedThreadPool
limited=org.apache.dubbo.common.threadpool.support.limited.LimitedThreadPool
eager=org.apache.dubbo.common.threadpool.support.eager.EagerThreadPool

每个接口都使用一个ExtensionLoader实例进行处理:

ExtensionLoader loader = ExtensionLoader.getExtensionLoader(ThreadPool.class);
ThreadPool tp = loader.getExtension("key");//key为配置文件中某个实现类的标识key.

由此Dubbo SPI机制实现了按需加载,而避免了JDK和Spring中一次加载所有实现类的弊端。

Adaptive机制

既然Dubbo SPI提供按需加载实现类的机制,同样带来一个新问题:什么情况下加载哪个具体实现类?显然这里需要一个逻辑判断模块,可以根据一些条件来指定加载的实现类。Dubbo中并没新设计一个模块,而是使用代理类的模式来解决这件事,类似适配器模式的运用,而且支持通过请求中携带的参数动态匹配到目标实现类,这称为dubbo的自适应机制。下面从常规方法开始,一步一步推导Dubbo是如何设计自适应机制的。
假设我们有一个目标接口如下:

public interface com.example.SomeService{
	String method1();
	Integer method2();
}

并且提供过了两个实现类service1 = SomeService1service2 = SomeService2

public class SomeService1 implements SomeService{
	pulbic String method1(){
		return "method1-service1";
	}
	pulbic Integer method2(){
		return 1;
	}
}

public class SomeService2 implements SomeService{
	pulbic String method1(){
		return "method1-service2";
	}
	pulbic Integer method2(){
		return 2;
	}
}

现在有一个请求携带参数request来调用SomeService.method1()方法,但具体由哪个实现类处理需要根据参数确定,因此我们需要一个控制器类进行分发,假设命名为Dispatch:

public class Dispatch<SomeService>{
	private ExtentionLoader<SomeService> loader;
	public T getExtensionService(Request request){
		//根据请求参数,判断应该返回哪个实现类
		if(someCondition1(request)){
			return loader.getExtension("service1");
		}else if(someCondition2(request)){
			return loader.getExtension("service2");
		}else{
			//默认使用service1
			return loader.getExtension("service1");
		}
	}
}

则完整的调用流程如下:

public class Service{
	private Dispatch  dispatch;
	public String doJob(Request request){
		SomeService service = dispatch.getExtensionService(request);
		return service.method1();//或者service.method2()
    }
 }

使用代理类完成派发

为了减少不必要的设计,我们将丢弃Dispatch类,而是把派发的逻辑放入代理类 SomeServiceProxy中,并且将判断的维度下沉到接口方法上

public class SomeServiceProxy implements SomeService{
	private ExtentionLoader<SomeService> loader;
	pulbic String method1(Request request){
		if someCondition1 then return loader.get("service1").method1(request);
	}
	pulbic Integer method2(){
		if someCondition2 then return loader.get("service2").method1(request);
	}
}

SomeServiceProxy类就是所谓的自适应类,在dubbo中这个类并不是手动编码得到的,而是在运行时由dubbo自动生成的,dubbo在启动时扫描@Adaptive注解,然后生成一个代理类的源代码,其也会实现目标接口并实现方法,具体是在org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generateClassDeclaration方法中完成,其源码模板为:"public class %s$Adaptive implements %s {\n";,这表明将会生成一个实现目标接口的代理类,且类名为:接口名$Adaptive。
实现方法的代码也是生成的,所有标注了@Adaptive注解的方法都会生成一个带判断逻辑的方法代码,具体是在org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generateMethod方法中实现,具体逻辑这里不展开,不过其思路和上面的例子是一样的,只不过dubbo中使用URL作为参数传递的载体,因此要求所有自适应接口都必须有URL类型参数。而用于逻辑判断的条件参数是通过@Adaptive的value属性配置的。

字节码编译

生成了自适应代理类后,得到的只是一个类的字符串源码,还需要把它转换为运行时对象,这一步是通过org.apache.dubbo.common.extension.ExtensionLoader#createAdaptiveExtensionClass方法中的return compiler.compile(code, classLoader);触发,将会返回一个Class类对象,用于上层的实例化、注入等工作。而这里的compiler本身也是一个SPI接口,默认使用org.apache.dubbo.common.compiler.support.JavassistCompiler类完成。

使用自适应类

相比于原始的手动指定key的dubbo SPI使用方式,有了自适应类后,使用方式有了一点变化:

ThreadPool tp = ExtensionLoader.getExtensionLoader(ThreadPool.class).getAdaptiveExtension();

相比于原来的代码,增加了.getAdaptiveExtension()方法调用,这里获取到的tp其实就是ThreadPool接口的自适应代理类,对其的调用最终会委托到某个具体实现类去完成。那么具体的派发逻辑是如何实现的呢?
梳理下我们已经掌握的情况:

  • dubbo SPI支持通过key获取指定的实现类,例如:loader.getExtension("key")
  • @Adaptive注解支持配置动态参数名param,从请求URL中获取对应的参数值,例如URL为"param11=value1&param2=value2", @Adaptive("param2"),显然我们可以通过配置的param2从URL中获取到对应的参数值value2
  • dubbo现在具有在运行时生成代码、编译、实例化类的功能
    结合上述三个现状条件,我们很容易想到一个实现方案:
    1、扫描@Adaptive注解,通过配置的key去URL中获取参数值,命名为 extName。
    2、调用ExtensionLoader.getExtension(extName)方法,获取对应的具体实现类实例,命名为extension
    3、将请求转交extension实现。

Dubbo中确实是这么做的,步骤1的获取参数值部分代码在org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generateExtNameAssignment方法中实现,其最后的return语句模板为:"String extName = %s;\n";,表明已经拿到参数值并命名为extName了。

步骤2中的语句在org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generateExtensionAssignment方法中实现,关键源码模板:"%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);\n"; ,其中调用extension = ...getExtension(extName)表明就是利用步骤1中获得的extName去获取真正的实现类并命名为extension.

步骤3中的语句是在org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generateReturnAndInvocation方法中生成,这里是真正调用的方法,关键语句模板为:extension.%s(%s);\n", method.getName(), args);,假设目标方法为metho1,参数为,request,这个模板生成的语句就是:return extension.method1(request);。完整伪代码代码如下:

Adaptive annotation = method.getAnnotation(Adaptive.class); //扫描Adaptive注解
String[] value = getValue(annotation); //获取配置的动态参数名
String extName = getByURL(value,url);//获取参数值,其对应一个具体的SPI 实现类
ThreadPool extension = loader.get(extName); //获取指定实现类
return extension.method(arg);//委托实现类完成调用

Dubbo SPI总结

通过上面的介绍,我们了解到Dubbo使用了自定义的SPI机制,这种机制可以按需加载实现类,灵活、安全,而且“按需”的过程也是通过@Adaptive注解和URL参数配置自动生成的代理类实现,进一步提高了可控性。

Dubbo的那些SPI接口】 Dubbo中有 @SPI @Adaptive @Activate 三个跟SPI相关的注解@SPI标记这是一个SPI接口@Adaptive作用在SPI接口的成员方法,调用SPI接口方法时根据这个方法的入参字段来决定使用哪个SPI接口实现类@Activate自动激活的实现类。 阅读详情

相关推荐

DubboSPI 机制原理分析 --自适应扩展点(Adaptive

首先来看一个概念,扩展的自适应实例。如果称它为扩展代理类,可能更好理解些,扩展的自适应实例其实就是一个 Extension 的代理,它实现了扩展点接口。在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展。 比如一个 IRepository 的扩展点,有一个 save 方法。有两个实现 MysqlRepository 和 MongoRepository。IRepository的自适应实例在调用接口方法的时候,会根据 save 方法中的参数,来决定要调用哪个 IRepository的实现。 如果方

A minor 3093

Dubbo - DubboSPI机制

SPI是什么? SPI全称service provider interface,比如你有一个接口,现在这个接口有三个实现类,那么在系统运行的时候对这个接口到底选择哪个实现类呢?这就需要SPI了,需要根据指定的配置或者默认的配置,去找到对应的实现类加载进来,然后用这个实现类的实例对象。 举个例子: 你有一个接口A,A1/A2/A3分别是接口A的不同实现,你通过配置接口A = 实现类A2,那么系统在...

yang的博客 3万+

KITTI数据集下载及解析

KITTI数据集下载及解析 版本 更新时间 更新内容 作者 1 V 1.0 xxx 完成主体内容 W. Xiao 2 文章目录KITTI Dataset1 简介1.1 数据采集平台1.2 坐标系2 数据解析2.1 image文件2.2 velodyne文件2.3 calib文件2.4 label文件3 KITTI可视...

u013086672的博客 5万+

Dubbo源码解析-——SPI机制

SPI 全称为 Service Provider Interface,是一种服务发现机制SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。

ZXMSH的博客 3863

Dubbo笔记】dubbo SPIAdaptive机制解读

在很多场合,系统需要根据上下文、参数决定逻辑。例如支付,使用微信、支付宝、银联支付逻辑不一样,比较直接的写法是用 【if】判断,走不同的分支;另外,设计模式里面有一种工厂模式也适用这种场景。【Dubbo】里也有很多这种场景(多协议等等),但是【Dubbo】使用SPI(Service Provider Interface) + Adaptive机制来应付这种场景。这种机制更灵活、更具扩展性。

beFocused的博客 715

dubboSPI机制Adaptive适配

SPI机制Adaptive适配机制 Adaptive适配机制 我们可以使用dubboSPI机制, 将dubbo中的一些扩展点通过注解改变原有的实现SPI(“dubbo”), 除此之外Adaptive适配机制则可以帮助我们从参数级别对dubbo的扩展点做出改变。 接口 @SPI("dubbo") public interface AdaptiveExt2 { @Adaptive() String echo(String msg, URL url); } 实现类 public class

cyt 781

Dubbo-Dubbo SPI 机制实现 默认的Adaptive

Dubbo SPI 机制实现 默认的Adaptive 1、最好的学习就是直接下载源码跟踪了解实现的思路 想要从根了解源码的实现原理,最好的思路就是下载源码debug一下,了解其核心思路的实现原理;这个其实有个前提,不是来不来就开始debug 这样的效率特别的低,最好还是先看看官方的文档或者博客查看其实现的思路到底是什么? 2、根据官方的Test用例中,debug到了生成的默认的 XXXX$Ad...

汪小哥 916

dubbo(二)dubbo spi机制

1 在介绍dubbo spi机制之前,我们先说一下jdk spispi:当服务提供者提供一个接口的多个实现的时候,一般会在META-INF/services/ 下面建立一个与接口全路径同名的文件,在文件里配置接口具体的实现类。当外部调用模块的时候的时候就能实例化对应的实现类而不需要动源码, 2 为什么dubbo不直接用jdk的spi机制,而是自己模仿实现了一个spi机制呢?jdk的spi会在一...

xuws2gj的博客 7549

dubbo源码解析】 --- dubbo spi 机制(@SPI、@Adaptive详解

本文对应源码地址:https://github.com/nieandsun/dubbo-study 文章目录1 @SPI 标签 及其使用简介 上篇文章《【SPI】 — java spi 机制简介》中, 可以看到,java spi 机制非常简单, 就是读取指定的配置文件, 将所有的类都加载到程序中。 而这种机制, 存在很多缺陷, 比如: 所有实现类无论是否使用, 直接被加载, 可能存在浪费 不能够灵活控制什么时候什么时机, 匹配什么实现, 功能太弱 Dubbo 基于自己的需要,对SPI 机制进.

nrsc 1736

Dubbo Adaptive机制详解

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>> ...

爱宝贝丶的博客 343

Dubbo进阶(七)- Dubbo 中默认的 Adaptive类生成过程及例子

Dubbo 默认的 Adatpive 类是如何生成的呢? 如果没有显示指明哪个是 默认的 Adaptive 那该怎么办呢?

anLA_的专栏 1360

dubbo SPI之@SPI、@Adaptive注解, 以及什么时候动态生成$Adaptive代码

本文基于dubbo2.7.7对如下三个问题分析 1. ``@SPI``注解的作用;2. ``@Adaptive``注解的作用,放在Type和Method上的区别和注意点;3. 什么时候动态生成和编译``xxx$Adaptive``代码

Brucelwl的博客 1208

DubboSPI机制的实现的源码解析(0)

DubboSPI机制的实现的源码解析

之城的专栏 559

dubbo源码_四、dubboSPI介绍以及源码剖析

一、SPI介绍SPI 全称为 Service Provider Interface,是一种服务发现机制SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件...

weixin_39686353的博客 69

Dubbo Spi机制 与 Adapative 详解 【转】

https://blog.csdn.net/mubu520/article/details/104209608 https://blog.csdn.net/yangbaggio/article/details/97617750 https://www.jianshu.com/p/96917c6c90fb https://www.jianshu.com/p/dc616814ce98 https://blog.csdn.net/weixin_33967071/article/details/926089

z69183787的专栏 918

dubbo源码阅读 Adaptive机制

Dubbo提供了一种SPI机制用于动态的加载扩展类,Dubbo SPI中提供的Adaptive机制就为解决这个问题提供了一种良好的解决方案,本文首先会通过一个示例来讲解Adaptive机制的用法,然后会从源码的角度对其实现原理进行讲解。 ...

weixin_45069429的博客 201

DubboSPI机制

1. SPI(Service Provider Interface)     1.1 JDK SPI机制 设计目标:面向对象的设计里模块之间是基于接口编程,模块之间不对实现类进行硬编码,一旦代码里涉及具体的实现类就违反了可插拔的原则如果需要替换一种实现就需要修改代码。不在模块中写死代码,这就是一种服务发现机制。这就是SPI机制,将装配的控制权移到代码之外。 Java SPI(Service...

weixin_34005669的博客 221

dubbo spi机制

前言 项目中,我们经常会提及java的spi机制,在指定的文件中编写内容,就可以通过java.util.ServiceLoader类完成文件中实现类的装载。那么dubbo中的spi有什么不同呢?带着这个疑问,我们来看一下dubbo中的spi实现。 dubbo spi demo代码地址 spi是什么 SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有不少框架用它 来做服务的扩展发现,简单来说,它就是一种动态替换发现的机制。使用SPI机制

qq_33195129的博客 281

Dubbo扩展的SPI代码研究

文章目录一,简介二,源码**1,获取指定扩展接口的SPI加载类(ExtensionLoader对象)****2,返回指定名称的扩展****3,getAdaptiveExtension();****4,getActivateExtension(URL url, String[] values, String group);**三 总结四 源码路径 一,简介 dubbo对于java原生的SPI技...

zcswl7961的博客 2448

QGroundControl高级配置与自定义功能实战指南

本文深入探讨了QGroundControl(QGC)的高级配置与自定义功能,为无人机开发者、系统集成商和高级用户提供实战指南。内容涵盖从品牌界面定制、自定义MAVLink消息处理,到插件开发与自动化部署的全流程,旨在帮助用户解锁QGC的隐藏潜力,打造专属的行业解决方案。

weixin_29234509的博客 212

ac620_i2c_control_小梅哥_fpga_i2c_IIC_verilog

使用verilog编写的I2C控制器,非常高效简洁,已经广泛应用于各个场合,如摄像头配置,电容触控屏读取等,基于小梅哥AC620开发板验证通过。

上一篇: 分布式共识算法(1)- Basic Paxos
下一篇: Mysql之B+树索引详解(4)——聚簇索引的生长、凋零
乐山大佛驾到
博客等级 码龄11年 158粉丝 17原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值