Java 原生 SPI 机制会加载所有的实现类,而往往我们只需要其中一个,因此造成资源浪费。 Dubbo SPI 不仅解决了这种资源浪费,而且还进行了扩展和修改。
Dubbo 按照 SPI 配置文件的用途,将其分成了三类目录:
-
META-INF/services/ 目录:该目录下的 SPI 配置文件用来兼容 JDK SPI 。
-
META-INF/dubbo/ 目录:该目录用于存放用户自定义 SPI 配置文件。
-
META-INF/dubbo/internal/ 目录:该目录用于存放 Dubbo 内部使用的 SPI 配置文件。
Dubbo 将 SPI 配置文件改成了 KV 格式,key 为扩展名,value 为具体实现类,这样我们可以直接通过扩展名来获取指定实现。以 org.apache.dubbo.rpc.Protocol为例:
registry=org.apache.dubbo.registry.integration.InterfaceCompatibleRegistryProtocol
service-discovery-registry=org.apache.dubbo.registry.integration.RegistryProtocol
1. 核心实现
Dubbo SPI 的核心逻辑在类 org.apache.dubbo.common.extension.ExtensionLoader 里。
1.1 @SPI 注解
@SPI 用于修饰接口,代表该接口是扩展接口,注解的 value 为默认扩展名。
/**
* Protocol. (API/SPI, Singleton, ThreadSafe)
*/
@SPI("dubbo")
public interface Protocol {
// 省略方法
}
具体实现获取方式:
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("dubbo");
1.2 LoadingStrategy
扩展类加载策略,对应上面说的三类 SPI 配置文件
- org.apache.dubbo.common.extension.ServicesLoadingStrategy
- org.apache.dubbo.common.extension.DubboLoadingStrategy
- org.apache.dubbo.common.extension.DubboInternalLoadingStrategy
1.3 创建扩展类实例
org.apache.dubbo.common.extension.ExtensionLoader#createExtension 方法核心流程如下:
- 根据扩展名获取具体实现类
- 根据类从缓存获取实例,如获取成功马上返回,否则利用反射创建实例
- 自动注入实例对象中的属性(injectExtension),调用实例的 setter 方法进行注入
- 自动包装,类似装饰器模式
- 对实现了
org.apache.dubbo.common.context.Lifecycle的实例进行初始化(initExtension)
1.4 @Adaptive 注解与适配器
@Adaptive 注解用来实现 Dubbo 的适配器功能。@Adaptive 注解可以修饰接口或方法。
- 修饰接口。Dubbo 中的 ExtensionFactory 接口有三个实现类,如下图所示,ExtensionFactory 接口上有 @SPI 注解,AdaptiveExtensionFactory 实现类上有 @Adaptive 注解。AdaptiveExtensionFactory 不包含任何具体实现,只是负责选择具体哪一个 ExtensionFactory 接口实现来创建扩展实例。
- 修饰接口方法,Dubbo 会动态生成适配器类。例如,
Transporter接口有两个被@Adaptive注解修饰的方法:
@SPI("netty")
public interface Transporter {
@Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
RemotingServer bind(URL url, ChannelHandler handler) throws RemotingException;
@Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
Client connect(URL url, ChannelHandler handler) throws RemotingException;
}
Dubbo 会生成一个 Transporter$Adaptive 适配器类,该类继承了 Transporter 接口:
public class Transporter$Adaptive implements Transporter {
public org.apache.dubbo.remoting.Client connect(URL arg0, ChannelHandler arg1) throws RemotingException {
if (arg0 == null) throw new IllegalArgumentException("url == null");
URL url = arg0;
// 确定扩展名,优先从URL中的client参数获取,其次是transporter参数
// 这两个参数名称由@Adaptive注解指定,最后是@SPI注解中的默认值
String extName = url.getParameter("client",
url.getParameter("transporter", "netty"));
if (extName == null)
throw new IllegalStateException("...");
// 通过ExtensionLoader加载Transporter接口的指定扩展实现
Transporter extension = (Transporter) ExtensionLoader
.getExtensionLoader(Transporter.class)
.getExtension(extName);
return extension.connect(arg0, arg1);
}
... // 省略bind()方法
}
动态生成适配器类的逻辑在 org.apache.dubbo.common.extension.ExtensionLoader#createAdaptiveExtensionClass
适配器类及其实例保存在 ExtensionLoader 类的 cachedAdaptiveClass 和 cachedAdaptiveInstance 属性里。
1.5 自动注入 injectExtension
实例创建以后 Dubbo 会对实例进行自动注入。关键逻辑在 org.apache.dubbo.common.extension.ExtensionLoader#injectExtension
满足以下条件的方法会被自动注入:
- 方法被
public修饰 - 以
set开头 - 只有一个参数且不是基础类型
- 方法没有被
@DisableInject注解修饰
Dubbo 依赖 ExtensionFactory 接口来实现注入,目前有三个实现类:
- SpringExtensionFactory:根据属性名注入 Spring Bean
- SpiExtensionFactory:根据扩展接口获取适配器实现
- AdaptiveExtensionFactory:ExtensionFactory 的适配器实现
1.6 自动包装
Dubbo 将多个扩展实现类的公共逻辑,抽象到“包装类”中,“包装类”与普通的扩展实现类一样,也实现了扩展接口,在获取真正的扩展实现对象时,在其外面包装一层“包装类”对象,可以理解成一层装饰器。
判断是“包装类”的逻辑:包含的构造函数只有一个参数且为扩展接口类型
private boolean isWrapperClass(Class<?> clazz) {
try {
clazz.getConstructor(type);
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
包装的关键逻辑:
List<Class<?>> wrapperClassesList = new ArrayList<>();
if (cachedWrapperClasses != null) {
wrapperClassesList.addAll(cachedWrapperClasses);
wrapperClassesList.sort(WrapperComparator.COMPARATOR);
Collections.reverse(wrapperClassesList);
}
if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
for (Class<?> wrapperClass : wrapperClassesList) {
Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
if (wrapper == null
|| (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
}
1.7 @Activate注解与自动激活特性
利用这个特性可以根据特定的URL请求情况返回特定的扩展。
扫描类时 Dubbo 把被 @Activate 注解修饰的实现类缓存到集合 cachedActivates 里。
@Activate 注解标注在扩展实现类上,有 group、value 以及 order 三个属性。
-
group 属性:修饰的实现类是在 Provider 端被激活还是在 Consumer 端被激活。
-
value 属性:修饰的实现类只在 URL 参数中出现指定的 key 时才会被激活。
-
order 属性:用来确定扩展实现类的排序。
关键逻辑在 org.apache.dubbo.common.extension.ExtensionLoader#getActivateExtension(org.apache.dubbo.common.URL, java.lang.String[], java.lang.String)
public List<T> getActivateExtension(URL url, String[] values, String group) {
...
}
获取扩展流程:
- 获取默认激活的扩展集合。遍历
cachedActivates,获取出满足以下条件的扩展,添加到activateExtensions集合里。- 扩展上的
@Activate注解指定的 group 属性与当前 group 匹配 - 扩展名没有出现在入参 values 中(既未在入参 values 中明确指定,也未在入参 values 中明确指定删除,删除使用 “-扩展名” 来表示)
- 扩展上的
@Activate注解指定的 value 属性与 URL 中的出现的请求参数匹配,具体判断逻辑在方法ExtensionLoader#isActive
- 扩展上的
- 根据指定入参 values 获取扩展
- 如果 values 包含 default,那么以 default 作为分界线,把位于 default 前的扩展名对应的扩展放在 activateExtensions 集合的前面,其余放到后面。
- 如果 values 不包含 default,那么把扩展都放在
activateExtensions集合的后面。
最后举个简单的例子说明上述处理流程,假设 cachedActivates 集合缓存的扩展实现如下表所示:
在 Provider 端调用 getActivateExtension() 方法时传入的 values 配置为 "demoFilter3、-demoFilter2、default、demoFilter1",那么根据上面的逻辑:
-
得到默认激活的扩展实实现集合中有 [ demoFilter4, demoFilter6 ];
-
排序后为 [ demoFilter6, demoFilter4 ];
-
按序添加自定义扩展实例之后得到 [ demoFilter3, demoFilter6, demoFilter4, demoFilter1 ]。
参考
Dubbo 2.7 源码