Dubbo的SPI原理
接口扩展策略注解 @SPI
Dubbo中的很多扩展接口,如 Protocol、Transporter、Filter 等,都是通过 JDK 的 SPI 机制实现的,也就是说这些功能都可被用户自定义的扩展所替换,接口扩展点由注解@SPI
定义。
JDK 中 SPI(Service Provider Interface)的设计与策略模式如出一辙,开发者可以替换掉 Dubbo 原扩展接口的默认实现,完成自定义需求,即可以自定义实现策略。
Dubbo 在 JDK 现有 SPI 实现的基础上做了如下改进:
- JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
- 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName();获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
- 增加了对扩展点 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 | 1、类级别:只能拥有一个,注解打在接口实现类上,显示的注册一个Adaptive实例,在编译期就存在,如`AdaptiveCompiler`,解决了上面的死循环问题,由`AdaptiveCompiler`依据dubbo配置决定使用哪个编译类; |
通过 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 | [zk: localhost:2181(CONNECTED) 1] Is /dubbo/com.dubbo.test.service.HelloService/providers |
缓存
- 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。