Dubbo之SPI、Adaptive机制详解

前言

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参数配置自动生成的代理类实现,进一步提高了可控性。

  • 34
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值