技术进阶:深入Dubbo SPI 机制剖析

作者:匠丶

链接:https://www.jianshu.com/p/ac71bcd91544

从上一篇 技术进阶:深入Java SPI 机制剖析 可以知道 Java SPI 的一些劣势。Dubbo 的扩展点加载从 Java SPI 扩展点发现机制加强而来。

Dubbo 改进了 Java SPI 的以下问题:

  • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
  • 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。

本文从以下几个方面,深入解析 Dubbo SPI 机制:

  • Dubbo SPI 特性
  • Dubbo SPI 的一些定义
  • Dubbo SPI 源码解析

Dubbo SPI 特性

扩展点自动包装

自动包装扩展点的 Wrapper 类。ExtensionLoader 在加载扩展点时,如果加载到的扩展点有拷贝构造函数,则判定为扩展点 Wrapper 类。Wrapper类内容:

package com.alibaba.xxx;
 
import com.alibaba.dubbo.rpc.Protocol;
 
public class XxxProtocolWrapper implements Protocol {
 Protocol impl;
 
 public XxxProtocolWrapper(Protocol protocol) { impl = protocol; }
 
 // 接口方法做一个操作后,再调用extension的方法
 public void refer() {
 //... 一些操作
 impl.refer();
 // ... 一些操作
 }
}

Wrapper 类同样实现了扩展点接口,但是 Wrapper 不是扩展点的真正实现。它的用途主要是用于从 ExtensionLoader 返回扩展点时,包装在真正的扩展点实现外。即从 ExtensionLoader 中返回的实际上是 Wrapper 类的实例,Wrapper 持有了实际的扩展点实现类。

扩展点的 Wrapper 类可以有多个,也可以根据需要新增。通过 Wrapper 类可以把所有扩展点公共逻辑移至 Wrapper 中。新加的 Wrapper 在所有的扩展点上添加了逻辑,有些类似 AOP,即 Wrapper 代理了扩展点。

扩展点自动装配

加载扩展点时,自动注入依赖的扩展点。加载扩展点时,扩展点实现类的成员如果为其它扩展点类型,ExtensionLoader 在会自动注入依赖的扩展点。ExtensionLoader 通过扫描扩展点实现类的所有 setter 方法来判定其成员。即 ExtensionLoader 会执行扩展点的拼装操作。

示例:有两个为扩展点 CarMaker(造车者)、WheelMaker (造轮者)

public interface CarMaker {
 Car makeCar();
}
 
public interface WheelMaker {
 Wheel makeWheel();
}

CarMaker 的一个实现类:

public class RaceCarMaker implements CarMaker {
 WheelMaker wheelMaker;
 
 public setWheelMaker(WheelMaker wheelMaker) {
 this.wheelMaker = wheelMaker;
 }
 
 public Car makeCar() {
 // ...
 Wheel wheel = wheelMaker.makeWheel();
 // ...
 return new RaceCar(wheel, ...);
 }
}

ExtensionLoader加载 CarMaker的扩展点实现RaceCar时,setWheelMaker方法的 WheelMaker也是扩展点则会注入WheelMaker的实现。

这里带来另一个问题,ExtensionLoader要注入依赖扩展点时,如何决定要注入依赖扩展点的哪个实现。在这个示例中,即是在多个WheelMaker的实现中要注入哪个。这个问题在下面一点扩展点自适应 中说明。

扩展点自适应

ExtensionLoader 注入的依赖扩展点是一个 Adaptive 实例,直到扩展点方法执行时才决定调用是一个扩展点实现。

Dubbo 使用 URL 对象(包含了Key-Value)传递配置信息。扩展点方法调用会有URL参数(或是参数有URL成员)。这样依赖的扩展点也可以从URL拿到配置信息,所有的扩展点自己定好配置的Key后,配置信息从URL上从最外层传入。URL在配置传递上即是一条总线。

当上面执行:

// ...
Wheel wheel = wheelMaker.makeWheel(url);
// ...

注入的 Adaptive 实例可以提取约定 Key 来决定使用哪个 WheelMaker 实现来调用对应实现的真正的 makeWheel 方法。如提取 wheel.type, key 即 url.get("wheel.type") 来决定 WheelMake 实现。Adaptive 实例的逻辑是固定,指定提取的 URL 的 Key,即可以代理真正的实现类上,可以动态生成。

在 Dubbo 的 ExtensionLoader 的扩展点类对应的 Adaptive 实现是在加载扩展点里动态生成。指定提取的 URL 的 Key 通过 @Adaptive 注解在接口方法上提供。

Dubbo SPI 的一些定义

@SPI注解,被此注解标记的接口,就表示是一个可扩展的接口。

@Adaptive注解,有两种注解方式:一种是注解在类上,一种是注解在方法上。

1、注解在类上,而且是注解在实现类上,目前dubbo只有AdaptiveCompiler和AdaptiveExtensionFactory类上标注了此注解,这是些特殊的类,ExtensionLoader需要依赖他们工作,所以得使用此方式。

2、注解在方法上,注解在接口的方法上,除了上面两个类之外,所有的都是注解在方法上。ExtensionLoader根据接口定义动态的生成适配器代码,并实例化这个生成的动态类。被Adaptive注解的方法会生成具体的方法实现。没有注解的方法生成的实现都是抛不支持的操作异常UnsupportedOperationException。被注解的方法在生成的动态类中,会根据url里的参数信息,来决定实际调用哪个扩展。

ExtensionLoader,是dubbo的SPI机制的查找服务实现的工具类,类似与Java的ServiceLoader,可做类比。dubbo约定扩展点配置文件放在classpath下的/META-INF/dubbo,/META-INF/dubbo/internal,/META-INF/services目录下,配置文件名为接口的全限定名,配置文件内容为配置名=扩展实现类的全限定名。

比如说这段代码:

private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

当上面代码执行的时候,我们其实还不知道要真正使用的Protocol是什么,可能是具体的实现DubboProtocol,也可能是其他的具体实现的Protocol,那么这时候protocol到底是什么呢?protocol其实是在调用getAdaptiveExtension()方法时候,自动生成的一个类,代码如下:

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

可以看到被@Adaptive注解的方法都生成了具体的实现,并且实现逻辑都相同。而没有被注解的方法直接抛出不支持操作的异常。

当我们使用protocol调用方法的时候,其实是调用生成的类Protocol$Adpative中的方法,这里面的方法根据url中的参数配置来找到具体的实现类,找具体实现类的方式还是通过dubbo的扩展机制。比如url中可能会有protocol=dubbo,此时就可以根据这个dubbo来确定我们要找的类是DubboProtocol。可以查看下生成的代码中getExtension(extName)这里是根据具体的名字去查找实现类。

Dubbo SPI 源码解析

了解源码结构,建立一个全局认识。结果图如下:

技术进阶:深入Dubbo SPI 机制剖析

下面以Protocol分析扩展点的加载

//先获取ExtensionLoader实例,然后加载自适应的Protocol扩展点
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
//发布服务
protocol.export(Invoker<T> invoker);

获取ExtensionLoader实例

getExtensionLoader(Protocol.class),根据要加载的接口Protocol,创建出一个ExtensionLoader实例,加载完的实例会被缓存起来,下次再加载Protocol的ExtensionLoader的时候,会使用已经缓存的这个,不会再新建一个实例:

@SuppressWarnings("unchecked")
 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 interface!");
 }
 //只有注解了@SPI的才会解析
 if(!withExtensionAnnotation(type)) {
 throw new IllegalArgumentException("Extension type(" + type + 
 ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
 }
 
 ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
 if (loader == null) {
 EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
 loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
 }
 return loader;
 }

获取自适应实现

上面返回一个ExtensionLoader的实例之后,开始加载自适应实现,加载是在调用getAdaptiveExtension()方法中进行的:

技术进阶:深入Dubbo SPI 机制剖析

第一步,从cache中获取自适应扩展点。可以关注一下,这里用了双重校验锁,Dubbo源码很多地方都用了这种方式。

@SuppressWarnings("unchecked")
 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("fail to create adaptive instance: " + t.toString(), t);
 }
 }
 }
 }
 }

第二步,缓存中不存在自适应扩展的实例,则调用createAdaptiveExtension()方法创建自适应扩展点。大家一定了解过适配器设计模式,而这个自适应扩展点实际上就是一个适配器。

private T createAdaptiveExtension() {
 try {
 //先通过getAdaptiveExtensionClass获取AdaptiveExtensionClass
 //然后获取其实例
 //最后进行注入处理
 return injectExtension((T) getAdaptiveExtensionClass().newInstance());
 } catch (Exception e) {}
}

第三步,getAdaptiveExtensionClass()获取自适应扩展点

private Class<?> getAdaptiveExtensionClass() {
 //加载当前Extension的所有实现(这里举例是Protocol,只会加载Protocol的所有实现类),如果有@Adaptive类型的实现类,会赋值给cachedAdaptiveClass
 //目前只有AdaptiveExtensionFactory和AdaptiveCompiler两个实现类是被注解了@Adaptive
 //除了ExtensionFactory和Compiler类型的扩展之外,其他类型的扩展都是下面动态创建的的实现
 getExtensionClasses();
 //加载完所有的实现之后,发现有cachedAdaptiveClass不为空
 //也就是说当前获取的自适应实现类是AdaptiveExtensionFactory或者是AdaptiveCompiler,就直接返回,这两个类是特殊用处的,不用代码生成,而是现成的代码
 if (cachedAdaptiveClass != null) {
 return cachedAdaptiveClass;
 }
 //没有找到Adaptive类型的实现,动态创建一个
 //比如Protocol的实现类,没有任何一个实现是用@Adaptive来注解的,只有Protocol接口的方法是有注解的
 //这时候就需要来动态的生成了,也就是生成Protocol$Adaptive
 return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

第四步,getExtensionClasses()加载所有的扩展类(注意:这里加载的是类,不是初始化类的实例):

private Map<String, Class<?>> getExtensionClasses() {
 //从缓存中获取,cachedClasses也是一个Holder,Holder这里持有的是一个Map,key是扩展点实现名,value是扩展点实现类
 //这里会存放当前扩展点类型的所有的扩展点的实现类
 //这里以Protocol为例,就是会存放Protocol的所有实现类
 //比如key为dubbo,value为com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
 //cachedClasses扩展点实现名称对应的实现类
 Map<String, Class<?>> classes = cachedClasses.get();
 //如果为null,说明没有被加载过,就会进行加载,而且加载就只会进行这一次
 if (classes == null) {
 synchronized (cachedClasses) {
 classes = cachedClasses.get();
 if (classes == null) {
 //如果没有加载过Extension的实现,进行扫描加载,完成后缓存起来
 //每个扩展点,其实现的加载只会这执行一次
 classes = loadExtensionClasses();
 cachedClasses.set(classes);
 }
 }
 }
 return classes;
}

loadExtensionClasses()方法,该函数的作用就是扫描classpath底下的配置文件,加载该interface对应的所有的扩展点,并将扩展点进行分类(Adaptive,Activate),以及生成包装类等。在扫描的过程中,如果发现该扩展类为Adaptive类型,则将该class缓存到cachedAdaptiveClass中。如果所有的扩展类均不是Adaptive类型,则调用createAdaptiveExtensionClass生成一个Adaptive类型的扩展类。

private Map<String, Class<?>> loadExtensionClasses() {
 final SPI defaultAnnotation = type.getAnnotation(SPI.class);
 if(defaultAnnotation != null) {
 //当前Extension的默认实现名字
 //比如说Protocol接口,注解是@SPI("dubbo")
 //这里dubbo就是默认的值
 String value = defaultAnnotation.value();
 //只能有一个默认的名字,如果多了,谁也不知道该用哪一个实现了。
 if(value != null && (value = value.trim()).length() > 0) {
 String[] names = NAME_SEPARATOR.split(value);
 if(names.length > 1) {
 throw new IllegalStateException();
 }
 //默认的名字保存起来
 if(names.length == 1) cachedDefaultName = names[0];
 }
 }
 //下面就开始从配置文件中加载扩展实现类
 Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
 //从META-INF/dubbo/internal目录下加载
 loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
 //从META-INF/dubbo/目录下加载
 loadFile(extensionClasses, DUBBO_DIRECTORY);
 //从META-INF/services/下加载
 loadFile(extensionClasses, SERVICES_DIRECTORY);
 return extensionClasses;
}

从各个位置的配置文件中加载实现类,对于Protocol来说加载的文件是以com.alibaba.dubbo.rpc.Protocol为名称的文件,文件的内容是:

registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
memcached=memcom.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol

loadFile()这个函数非常长,它会加载Class类。根据不同的类型加载到不同的缓存,大概的处理流程是对配置文件中的各个扩展点进行如下操作 :

  1. 判断该扩展点是否是要加载的interface的子类,如果不是则忽略
  2. 如果该class带有Adaptive的注解,则缓存到cachedAdaptiveClass中
  3. 如果该class具有拷贝构造函数,则缓存到cachedWrapperClasses中
  4. 如果该class带有Activate注解,则缓存到cachedActivates中
  5. 将所有的扩展点缓存到cachedClasses中

第五步,createAdaptiveExtensionClass() ,动态创建自适应扩展点Protocol$Adaptive。

//创建一个适配器扩展点。(创建一个动态的字节码文件)
 private Class<?> createAdaptiveExtensionClass() {
 //生成字节码代码
 String code = createAdaptiveExtensionClassCode();
 //获得类加载器
 ClassLoader classLoader = findClassLoader();
 com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
 //动态编译字节码(默认情况下使用的是javassist)
 return compiler.compile(code, classLoader);
 }

第六步,创建完Protocol$Adaptive后,injectExtension()自动注入到容器。

技术进阶:深入Dubbo SPI 机制剖析

Protocol$Adaptive的主要功能 :

1、 从url或扩展接口获取扩展接口实现类的名称

2、根据名称,获取实现类ExtensionLoader.getExtensionLoader(扩展接口类).getExtension(扩展接口实现类名称),然后调用实现类的方法。

需要明白一点dubbo的内部传参基本上都是基于Url来实现的,也就是说Dubbo是基于URL驱动的技术。所以,适配器类的目的是在运行期获取扩展的真正实现来调用,解耦接口和实现。

相关推荐