前言
我们之前说过SPI机制,不仅谈到过SPI与API的区别,也讲了JAVA中的SPI机制实例。其实一切都是为了今天作铺垫,没错,今天我们要来讲讲Dubbo的重要设计——Dubbo-SPI机制。这一次直接爆肝2W字长文,从示例到源码,力求讲清讲透。事不宜迟,现在就开始我们的学习吧.....
一、Dubbo为什么要自己实现SPI
其实这个问题应该分成两部分:
Dubbo为什么要用SPI机制
Dubbo为什么不用原生的JAVA-SPI实现
1. Dubbo为什么需要SPI机制
要回答这个问题,我们必须先复习一遍SPI机制的特点与好处:解耦合、扩展性强、兼容性好
但我们如果仅使用Dubbo而不做任何改造的话,SPI的作用其实就相当于策略模式,一个接口内置了多个实现,可以根据入参或配置选择一个实现类。如果仅是如此,Dubbo的SPI机制显得就完全多余,可以由设计模式来替代了。
所以。Dubbo使用SPI机制的主要原因是为了实现可插拔的扩展性。 具体来说,Dubbo 的 SPI 机制不仅可以使内置的几种实现类能灵活选用,还可以让用户通过配置文件或者注解的方式,自定义实现某个接口的实现类,然后在运行时自动通过 SPI 机制来加载并实例化对应的实现类。这样可以大大提高 Dubbo 的灵活性和可扩展性,同时也方便了 Dubbo 的用户进行自定义定制。在 Dubbo 中,所有内部实现和第三方实现都是平等的,用户可以基于自身业务需求,替换 Dubbo 提供的原生实现。
2. 为什么不用原生的JAVA-SPI实现
知道了Dubbo的SPI机制主要是为了可插拔的扩展性,那为什么不直接用JAVA自带的SPI机制呢,主要其实还是JAVA-SPI无法满足Dubbo的设计意图:
JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
原生的SPI,获得实现类后,不支持dubbo的SPI机制增加了对IOC、AOP的支持,一个扩展点可以直接通过setter注入到其他扩展点。
二、Dubbo 到底哪些地方定了SPI
1. 官网介绍
我们说Dubbo 是为了支持可插拔的扩展,那么我们先来看看,Dubbo到底有哪些地方能支持扩展?我们在官网看到了下图:
协议与编码扩展:通信协议、序列化编码协议等
流量管控扩展:集群容错策略、路由规则、负载均衡、限流降级、熔断策略等
服务治理扩展:注册中心、配置中心、元数据中心、分布式事务、全链路追踪、监控系统等
诊断与调优扩展:流量统计、线程池策略、日志、QoS 运维命令、健康检查、配置加载等
2. 代码查阅
我们以其中的协议 Protocol 为例来看看。可以看出Protocol 为其定义在rpc包下的一个接口,而其内置的实现类数量也很多。有通用的Http协议、自研的Dubbo协议,java内置的Rmi协议,当然也支持其他框架的协议如thrift、grpc。
再比如让调用方、被调用方注册的注册中心,也有多种容器可选,如常见的Redis、Zookeeper、Nacos以及自己的Dubbo注册中心
可以说,Dubbo几乎是每个关键部件都提供了扩展的功能。我们过往会有很多公司有自研框架,自研协议。这种场景使用Dubbo,就只需要在有限的几个地方对接好其 SPI,就能很轻松的用上Dubbo,这对于开发者和架构来说,是个非常实用的设计。
三、Dubbo - SPI的使用及原理
1. JAVA 与 SPI 使用方式对比
我们先复习一遍JAVA 的原生SPI使用方式
JAVA 的原生使用
在jar包的 META-INF/services/ 下填入我们对SPI的实现类的全限定名,如 com.mysql.jdbc.Driver
在代码中使用 ServiceLoader loader = ServiceLoader.load(Driver.class); 就能获得一个加载了所有驱动实现类的对象
Dubbo中的使用.
Dubbo中的使用较为复杂,我们先来看它最基本的用法
如果我们自定义了RPC协议,那就在 META-INF/dubbo/org.apache.dubbo.rpc.Protocol 文件里填上我们的协议类的简称和它的全限定名,如myprotocol=com.zhanfu.samples.protocol.MyProtocol
在代码中使用 Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(“myprotocol”); 就能准确获取到我们指定的协议
在代码中使用 Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); 就能获取到自适应指定的的协议
可以看到,Dubbo 实现 SPI机制的核心是 ExtensionLoader,它取代了 JDK 自带的 ServiceLoader,我们慢慢来看其用法及实现原理
2. 配置文件
(1)配置规则
Dubbo 规定的三个配置目录分别为 META-INF/services/ 、META-INF/dubbo/internal/ 、META-INF/dubbo/,我们注意到 META-INF/services/ 其实就是原生JAVA - SPI 使用的目录、META-INF/dubbo/internal/则是Duubo自己内部实现的一些扩展配置,其实三个目录本身没有功能的区别,默认是全部都扫描的,一般我们建议将配置文件放在META-INF/dubbo/下
配置文件的文件名:必须是扩展点的全限定名,比如我们想使用自定义的协议,即protocol,我们就需要在上述某个目录下加上名为 org.apache.dubbo.rpc.Protocol 的文件
文件内容:采用“简称 = 类名”的形式,一行一个,比如 http=org.apache.dubbo.rpc.protocol.http.HttpProtocol
示例:下图就是Dubbo类型转换器的扩展配置
(2)加载配置与解析的原理
参考过去的JAVA原生的ServiceLoader,以及 getExtension 和 getAdaptiveExtension 两个方法,我们不难得出,扩展加载器至少有这么几个功能:
加载并保存各个扩展组件
能按照简称获得指定的扩展实现类
能提供默认的扩展实现类
需要注意的是,一个ExtensionLoader实例只包含一个扩展点,比如“协议”扩展点,那么该实例中,就只会加载“协议”接口的实现类
其实其中的 1 和 2,相当于原生SPI的略微改动,原生JAVA的SPI 会在启动后实例化所有扩展实现类,并保存。而 Dubbo 则是启动后保存<简称 ,类信息>的Map,你给定某个简称,我再为你实例化某个实现类。
那么其具体如何做的,我们直接来看其源码实现(默认实现类又是什么呢?我们又该如何指定默认实现类呢?这些问题请看@SPI部分)
// 按照指定简称,返回实现类 public T getExtension(String name) { if (name == null || name.length() == 0) throw new IllegalArgumentException("Extension name == null"); // 当传入的名称为“true”时,获取默认实现类 if ("true".equals(name)) { return getDefaultExtension(); } Holder<Object> holder = cachedInstances.get(name); if (holder == null) { cachedInstances.putIfAbsent(name, new Holder<Object>()); holder = cachedInstances.get(name); } Object instance = holder.get(); if (instance == null) { synchronized (holder) { instance = holder.get(); if (instance == null) { // 为指定简称创建实现类 instance = createExtension(name); holder.set(instance); } } } return (T) instance; } private T createExtension(String name) { // 先获取指定简称在配置文件中对应的类 Class<?> clazz = getExtensionClasses().get(name); if (clazz == null) { throw findException(name); } try { T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { // 实例化实现类 EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } // 如果该实现类内引用了其他扩展点,则自动为其注入扩展点,类似于Spring体系下的IOC injectExtension(instance); Set<Class<?>> wrapperClasses = cachedWrapperClasses; // 如果设定了装饰器类,则把实现类扔进这个装饰器,再把装饰器实例返回,如果有多个装饰器类,就会造成层层套壳 // 与Spring中的AOP类似,但此处 wrapperClass 存在 ConcurrentHashSet,无法指定套壳的顺序 if (wrapperClasses != null && !wrapperClasses.isEmpty()) { for (Class<?> wrapperClass : wrapperClasses) { instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); } } return instance; } catch (Throwable t) { throw new IllegalStateException("Extension instance(name: " + name + ", class: " + type + ") could not be instantiated: " + t.getMessage(), t); } } // 获取加载扩展点<简称,类>的映射 private Map<String, Class<?>> getExtensionClasses() { Map<String, Class<?>> classes = cachedClasses.get(); if (classes == null) { synchronized (cachedClasses) { classes = cachedClasses.get(); if (classes == null) { classes = loadExtensionClasses(); cachedClasses.set(classes); } } } return classes; } // 从配置文件中加载扩展点的信息 private Map<String, Class<?>> loadExtensionClasses() { // 获取本扩展点的默认实现 cacheDefaultExtensionName(); Map<String, Class<?>> extensionClasses = new HashMap<>(); // 遍历策略,加载所有目录下指定的扩展点的信息。 // 注意,只会扫描文件末尾是扩展点名的文件,比如“协议”扩展点,只会扫描类似 org.apache.dubbo.rpc.Protocol 文件 for (LoadingStrategy strategy : strategies) { loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages()); loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages()); } return extensionClasses; }
其中策略有三个实现类
分别对应着下列目录
META-INF/services/
META-INF/dubbo/internal/
META-INF/dubbo/
所以,至此,我们已经明白了其实说到底,还是从上述三个目录中获取某个扩展点的所有实现情况。
而其具体从配置文件解析的过程,源码如下
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name, boolean overridden) throws NoSuchMethodException { if (!type.isAssignableFrom(clazz)) { throw new IllegalStateException("Error occurred when loading extension class (interface: " + type + ", class line: " + clazz.getName() + "), class " + clazz.getName() + " is not subtype of interface."); } // 实现类上有@Adaptive注解,表示这个实现类就为自适应实现类,详见第3小节 if (clazz.isAnnotationPresent(Adaptive.class)) { cacheAdaptiveClass(clazz, overridden); // 实现类上有带一个同类型入参的构造方法,表示这个实现类就为装饰器类,详见第2小节 } else if (isWrapperClass(clazz)) { cacheWrapperClass(clazz); } else { // 普通的实现类 clazz.getConstructor(); if (StringUtils.isEmpty(name)) { name = findAnnotationName(clazz); if (name.length() == 0) { throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL); } } String[] names = NAME_SEPARATOR.split(name); if (ArrayUtils.isNotEmpty(names)) { cacheActivateClass(clazz, names[0]); for (String n : names) { cacheName(clazz, n); saveInExtensionClass(extensionClasses, clazz, n, overridden); } } } }
可以看到,都是实现类,却也有不同的情况。可以是普通实现类,可以是装饰器类,也可以是自适应实现类,后两者我们在下面来讲。