Dubbo的SPI原理

接口扩展策略注解 @SPI

Dubbo中的很多扩展接口,如 Protocol、Transporter、Filter 等,都是通过 JDK 的 SPI 机制实现的,也就是说这些功能都可被用户自定义的扩展所替换,接口扩展点由注解@SPI定义。
JDK 中 SPI(Service Provider Interface)的设计与策略模式如出一辙,开发者可以替换掉 Dubbo 原扩展接口的默认实现,完成自定义需求,即可以自定义实现策略。
Dubbo 在 JDK 现有 SPI 实现的基础上做了如下改进:

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

那么 Dubbo 的 SPI 机制是怎么实现的呢?以协议扩展为例,Dubbo 中协议被抽象为 Protocol 接口。

读取扩展点

ServiceConfig#protocol

1
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension()

Dubbo 使用 ExtensionLoader 实现扩展点加载。

  • ExtensionLoader#getExtensionLoader()
    获取 ExtensionLoader 实现,保证每种扩展点一个单例。
  • ExtensionLoader#getAdaptiveExtension()
    根据不同的 SPI 扩展点,即不同的 interface,生成不同的 Adaptive 实例的代码。
    -> getAdaptiveExtensionClass()
    -> getExtensionClasses()
    -> loadExtensionClasses()
    加载所有的扩展点实现,直到扩展点方法执行时才决定调用是一个扩展点实现,即从众多的实现策略中决定具体使用哪一个策略。
    ExtensionLoader 会依次从META-INF/dubbo/internal(Dubbo 内部实现)、META-INF/dubbo/(开发者自定义策略)、META-INF/services/这几个目录下读取扩展点实现,目录下的同名文件配置了对应扩展点的实现策略,调用 loadFile 来加载对应的扩展策略。
    -> loadFile(Map<String, Class<?>> extensionClasses, String dir)

生成 Adaptive 实例

  • ExtensionLoader#loadFile
    -> String fileName = dir + type.getName()
    拼接文件路径
    -> ClassLoader classLoader = findClassLoader()
    拿到 ExtensionLoader 的类加载器。
    -> Class<?> clazz = Class.forName(line, true, classLoader);
    文件每行是一个实现类的全路径名,通过反射加载并拿到具体类型。
    -> extensionClasses.put(n, clazz)
    添加到 map 里返回。
  • ExtensionLoader#cachedClasses
    -> cachedClasses.set(classes)
    添加到缓存。
  • ExtensionLoader#createAdaptiveExtensionClass
    -> ExtensionLoader#createAdaptiveExtensionClassCode
    生成 Adaptive 类。
    -> compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension()
    拿到编译接口扩展点的一个具体实现,dubbo 内部支持 jdk 和 javassist,默认是 javassist。
    -> compiler.compile(code, classLoader)
    编译代码,生成 Adaptive 实例类。

上面提到 Compiler 也是一个扩展点,同样也依赖这个流程来实例化,在运行时生成 Adaptive 实例的时候,需要生成 Compiler 接口的 Adaptive 实例,即运行生成 Adaptive 实例的时候需要先有一个 Compiler 接口的 Adaptive 实例,那这样岂不是陷入了死循环,这里就要提到显示指定 Adaptive 实例的情况。@Adaptive注解支持类级别和方法级别:

1
2
1、类级别:只能拥有一个,注解打在接口实现类上,显示的注册一个Adaptive实例,在编译期就存在,如`AdaptiveCompiler`,解决了上面的死循环问题,由`AdaptiveCompiler`依据dubbo配置决定使用哪个编译类;
2、方法级别:在运行期动态的生成Adaptive实例。

通过 URL 动态选择协议

ExtensionLoader#createAdaptiveExtensionClassCode
生成的 Protocol 的 Adaptive 实例类,依据 URL 中 protocol key-value 的值,选择对应的 Protocol 策略来暴露和引用服务。
扩展点方法调用会有 URL 参数(或是参数有 URL 成员),这样依赖的扩展点可以从 URL 拿到配置信息,所有的扩展点自己定好配置的 Key 后,配置信息从 URL 上从最外层传入,URL 在配置传递上即是一条总线。
以 dubbo+zookeeper 为例,暴露和引用远程服务都是注册在 zookeeper 上的,服务注册在 zookeeper 上本质其实是一个 URL,远程服务调用的过程中依据 URL 的 key-value 来动态决定执行 Protocol、Filter 等接口扩展点的执行策略。
下面是 Provider 端暴露 HelloService 服务时在 zookeeper 上注册的 URL,在 zookeeper 上的路径为/dubbo/com.dubbo.test.service.HelloService/providers,URL 表示了采用 dubbo 协议,接口为 com.dubbo.test.service.HelloService,方法为 say,要执行的 Filter 为 whiteFilter 等。

1
2
[zk: localhost:2181(CONNECTED) 1] Is /dubbo/com.dubbo.test.service.HelloService/providers
[dubbo://127.0.0.1:2O881/com.dubbo.test.service.HelloService?anyhost=true&application=dubbo-test-service&dubbo=2.4.10&group=test-prod&interface=com.dubbo.test.service.HelloService&methods=say&pid=21242&revision=l.0&service.filter=whiteFilter&side=providerxtamp=1495436105078&version=l.0]

缓存

  • volatile Class<~> cachedAdaptiveClass
    这个是缓存 AdaptiveClass,如果一个扩展类的类上面带有 @Adaptive 注解,那么这个类就会被缓存在这个地方,每一种类型的扩展类只有一个 AdaptiveClass,如果发现有多个,则会报错。另外,当通过 getAdaptiveExtensionClass 来获取自适应扩展类时,如果当前还没有 AdaptiveClass,则会自动创建一个(动态生成 Java 代码,再编译,典型的比如 Protocol$Adaptive 就是这么生成的)
  • Set<~> cachedWrapperClasses
    这个是缓存包装类的,Dubbo 判断一个扩展类是否是包装类比较简单,通过构造函数来判断,如果这个扩展类有一个构造函数,其中参数是当前扩展类的类型,那么就是包装类,举个例子,ProtocolFilterWrapper 就是 protocol 扩展类的包装类,因为有这个构造函数:public ProtocolFilterWrapper(Protocol protocol)
  • Map<~> cachedActivates
    这个是缓存激活的扩展类,当然,@Activate 注解还可以规定激活的条件和时机
  • Holder<~> cachedClasses
    这个是缓存 Adaptive 和 Wrapper 扩展类之外的普通扩展类

扩展类被加载后会根据一定的规则放入以上 4 个缓存中,比如带有 @Adaptive 注解的会被放入 cachedAdaptiveClass。