从用法到源码,一篇文章让你精通Dubbo的SPI机制
我们之前说过SPI机制,不仅谈到过SPI与API的区别,也讲了JAVA中的SPI机制实例。其实一切都是为了今天作铺垫,没错,今天我们要来讲讲Dubbo的重要设计——Dubbo-SPI机制。这一次直接爆肝2W字长文,从示例到源码,力求讲清讲透。事不宜迟,现在就开始我们的学习吧
其实这个问题应该分成两部分:
Dubbo为什么要用SPI机制
Dubbo为什么不用原生的JAVA-SPI实现
要回答这个问题,我们必须先复习一遍SPI机制的特点与好处:解耦合、扩展性强、兼容性好
但我们如果仅使用Dubbo而不做任何改造的话,SPI的作用其实就相当于策略模式
,一个接口内置了多个实现,可以根据入参或配置选择一个实现类。如果仅是如此,Dubbo的SPI机制显得就完全多余,可以由设计模式来替代了。
所以。Dubbo使用SPI机制的主要原因是为了实现可插拔的扩展性。 具体来说,Dubbo 的 SPI 机制不仅可以使内置的几种实现类能灵活选用,还可以让用户通过配置文件或者注解的方式,自定义实现某个接口的实现类,然后在运行时自动通过 SPI 机制来加载并实例化对应的实现类。这样可以大大提高 Dubbo 的灵活性和可扩展性,同时也方便了 Dubbo 的用户进行自定义定制。在 Dubbo 中,所有内部实现和第三方实现都是平等的,用户可以基于自身业务需求,替换 Dubbo 提供的原生实现。
知道了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 是为了支持可插拔的扩展,那么我们先来看看,Dubbo到底有哪些地方能支持扩展?我们在官网看到了下图:
- 协议与编码扩展:通信协议、序列化编码协议等
- 流量管控扩展:集群容错策略、路由规则、负载均衡、限流降级、熔断策略等
- 服务治理扩展:注册中心、配置中心、元数据中心、分布式事务、全链路追踪、监控系统等
- 诊断与调优扩展:流量统计、线程池策略、日志、QoS 运维命令、健康检查、配置加载等
我们以其中的协议 Protocol 为例来看看。可以看出Protocol 为其定义在rpc包下的一个接口,而其内置的实现类数量也很多。有通用的Http协议、自研的Dubbo协议,java内置的Rmi协议,当然也支持其他框架的协议如thrift、grpc。
再比如让调用方、被调用方注册的注册中心,也有多种容器可选,如常见的Redis、Zookeeper、Nacos以及自己的Dubbo注册中心
可以说,Dubbo几乎是每个关键部件都提供了扩展的功能。我们过往会有很多公司有自研框架,自研协议。这种场景使用Dubbo,就只需要在有限的几个地方对接好其 SPI,就能很轻松的用上Dubbo,这对于开发者和架构来说,是个非常实用的设计。
我们先复习一遍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,我们慢慢来看其用法及实现原理
- 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类型转换器的扩展配置
参考过去的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);
}
}
}
}
可以看到,都是实现类,却也有不同的情况。可以是普通实现类,可以是装饰器类,也可以是自适应实现类,后两者我们在下面来讲。
wrapper: 我们自定义的某个实现类内,如果有构造方法的入参是传入一个同类型的对象,那么即可认定这个实现类其实是一个装饰器,其实际功能由内置的那个对象来解决,该实现类本身仅相当于一个外壳,执行一些其他功能,如打印日志等。这实际上与Spring里面的功能增强
类似
public class DubboProtocolWrapper implements Protocol {
private Protocol protocol;
// 构造方法入参是一个Protocol类型,将会被视作一个装饰器
public DubboProtocolWrapper(Protocol protocol) {
this.protocol = protocol;
}
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
log.debug("现在开始要导出了");
return protocol.export(invoker);
}
// Protocol 接口的其他方法
......
}
Setter: 我们自定义的某个实现类内,如果有方法是以“set”作为前缀,比如是 setProtocol,并且方法有一个入参且没有 @DisableInject注解,那么就会判断该类入参是否也是一个SPI,如果是就会委托一个 ExtensionFactory 为你去找其实现类并注入,至于寻找哪个实现类,则与你ExtensionFactory的类型有关(没错,ExtensionFactory是个接口,其本身也遵循SPI接口的设计,Dubbo也为你准备了三个实现类)。这一步骤其实与Spring 的 IOC
十分相似
public class MyCluster implements Cluster{
private Protocol protocol;
// 当本类被dubbo实例化时,会执行该方法,主动为我们寻找protocol的实现类并注入
public void setProtocol(Protocol protocol) {
this.protocol = protocol;
}
// Cluster 接口的其他方法
......
}
在实例化扩展点的代码中,我们可以看到有以下两个处理:
- wrapper
如果扩展点实现类有拷贝构造函数,则认为是包装类。包装类的作用是对其他扩展点实现类进行包装,通过包装类可以把所有扩展点的公共逻辑移到包装类,类似AOP。 - setter
扩展点实现类的成员如果为其它扩展点类型,ExtensionLoader 在会自动注入依赖的扩展点。ExtensionLoader 通过扫描扩展点实现类的所有set方法来判定其成员。
//实例化扩展点时,会判断有没有wrapper装饰器,有装饰器,需要拿装饰器套壳上去,并返回装饰器实例
private T createExtension(String name) {
......
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
......
}
//实例化扩展点时,如果实例里有其他扩展点,则也许注入扩展点
private T injectExtension(T instance) {
if (objectFactory == null) {
return instance;
}
try {
for (Method method : instance.getClass().getMethods()) {
if (!isSetter(method)) {
continue;
}
/**
* Check {@link DisableInject} to see if we need auto injection for this property
*/
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
Class<?> pt = method.getParameterTypes()[0];
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
String property = getSetterProperty(method);
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("Failed to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
/**
* return true if and only if:
* <p>
* 1, public
* <p>
* 2, name starts with "set"
* <p>
* 3, only has one parameter
*/
private boolean isSetter(Method method) {
return method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers());
}
使用方式
@SPI
注解使用在接口上,作用时为该接口指定默认实现类,其属性 value 是默认实现的简称,因为是直接标在Dubbo的接口上的,除非我们修改Dubbo的源码,否则这个值就是Dubbo 内置好的(当然你也可以自己定义一个接口,给接口加上该注解)。比如下图,默认的RPC协议就是“dubbo”协议。实现原理
// 加载某个扩展点的所有配置,所谓配置,其实就是<简称,类名>这种配置
private Map<String, Class<?>> loadExtensionClasses() {
// 找到并设置本扩展点的默认实现
cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
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;
}
private void cacheDefaultExtensionName() {
// 如果指定扩展点接口带@SPI注解,且注解中有简称,则把该简称作为默认实现
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation == null) {
return;
}
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
if (names.length > 1) {
throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
if (names.length == 1) {
cachedDefaultName = names[0];
}
}
}
使用方式
①@Adaptive
注解一般在实现类上,表明该类自适应,自适应是什么意思呢?就是其某些方法自己并不实现,而是根据调用该方法时的入参,再去选另一个实现类来执行,相当于一个套壳,但和上面的装饰器类 wrapper 对比,上面的wrapper 会指定某固定类来执行方法,其本身执行别的功能,而本套壳Adaptive类,则并不固定使用哪个实现类来真正执行方法,所以叫自适应
②@Adaptive
如果没有出现在实现类上,那么Duubo会在我们指定要自适应实现类的时候,给我们自己创建个自适应实现类,该实现类的模板为SPI接口本身,此时所有的方法都没有实现,仅有源码方法上有@Adaptive
的部分方法才会帮你编译点方法内容,帮你解析下入参,从入参里解析出另一个实现类来执行实现原理
当@Adaptive
注解在实现类上时,会在实现类被解析时,就存储起来
// 扫描所有目录下的配置时,会对提到的各实现类进行解析(注意,仅仅是分析类信息,并没有实例化)
private volatile Class<?> cachedAdaptiveClass = null;
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
boolean overridden) throws NoSuchMethodException {
......
// 如果类上有 @Adaptive 注解,则表明该类为自适应类, 会存储进变量 cachedAdaptiveClass 中
if (clazz.isAnnotationPresent(Adaptive.class)) {
cacheAdaptiveClass(clazz, overridden);
}
......
}
如果没有指定现成的带@Adaptive
的实现类,那就懒加载,直到有人要获取该扩展点的自适应实现类时再进行创建
private volatile Class<?> cachedAdaptiveClass = null;
// 获取本扩展点的自适应实现类
private Class<?> getAdaptiveExtensionClass() {
getExtensionClasses();
// 判断有没有指定的自适应实现类,如果没有则新建。
// 在前面扫描的时候,如果扫到了某个实现类上标注了@Adaptive,此时就会有自适应类
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
// 如果没有现成的自适应类,dubbo则会为我们创建
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
private Class<?> createAdaptiveExtensionClass() {
// 根据SPI接口的信息,自编一个所谓的自适应实现实例。注意此处的cachedDefaultName,这代表着如果自适应没适应成功,还可以使用兜底的实现类来执行
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
ClassLoader classLoader = findClassLoader();
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}
// 创建自适应实现类时,帮其完善方法
private String generateMethodContent(Method method) {
Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
StringBuilder code = new StringBuilder(512);
// 如果接口方法上没有 @Adaptive注解,直接在方法体内写一个Unsupported异常
if (adaptiveAnnotation == null) {
return generateUnsupported(method);
} else {
// 如果接口方法上有 @Adaptive注解
int urlTypeIndex = getUrlTypeIndex(method);
// 如果方法入参不含有URL类型,方法体内写抛异常
if (urlTypeIndex != -1) {
// Null Point check
code.append(generateUrlNullCheck(urlTypeIndex));
} else {
// did not find parameter in URL type
code.append(generateUrlAssignmentIndirectly(method));
}
// 此处实现较为繁琐,我们直接看其最后生成的类,进行反编译
String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
boolean hasInvocation = hasInvocationArgument(method);
code.append(generateInvocationArgumentNullCheck(method));
code.append(generateExtNameAssignment(value, hasInvocation));
// check extName == null?
code.append(generateExtNameNullCheck(value));
code.append(generateExtensionAssignment());
// return statement
code.append(generateReturnAndInvocation(method));
}
return code.toString();
}
如我们上述提到的 Protocol ,其接口是这样的,有两个方法带了@Adaptive
将其编造的自适应实现类,反编译,得到
- 使用方式
@Activate
注解用于标记一个扩展点实现类在什么条件下可用。通过该注解,我们可以指定这个扩展点的执行顺序、条件等,从而实现对扩展点的控制@Activate
注解可以用在四个地方:在扩展点接口上。在扩展点实现类上。在扩展点实现类的构造方法上。在扩展点实现类的构造方法参数上。@Activate
注解需要指定三个属性:group
:表示扩展点所属的分组。如果不指定,则默认为全局分组,即所有扩展点都属于该分组。value
:表示是否需要激活或禁用扩展点简称,允许填多个。order
:表示扩展点的执行顺序,值小的先执行。如果不指定,则默认为0。
如上图,Filter的一个实现类CacheFilter,类上就标注了@Aactivate
注解,当我们用下面的方法去取实现类时,就会返回CacheFilter
public class Consumer {
public static void main(String[] args) {
// 获取扩展点工厂
ExtensionLoader<TestExtension> extensionLoader = ExtensionLoader.getExtensionLoader(Filter.class);
// 获取Filter的扩展点实现类:
// 1. 此处我们没有指明group,就不会因为group的原因筛除任何实现类,
// 2. 但我们又指定了“cache”, 因此简称为 cache 的Filter的实现类一定会加入返回结果列表里
// 3. 我们指定了url,意味着对于其他的实现类,都要经过url校验,校验匹配才会加入返回结果列表里
List<TestExtension> extensions = extensionLoader.getActivateExtension(URL.valueOf("test://localhost/test?key1=value1&key2=value2"), "cache");
// 调用扩展点方法
for (TestExtension extension : extensions) {
extension.test();
}
}
}
- 原理解析
// 三个重载方法,入参各不相同
public List<T> getActivateExtension(URL url, String key) {
return getActivateExtension(url, key, null);
}
public List<T> getActivateExtension(URL url, String[] values) {
return getActivateExtension(url, values, null);
}
public List<T> getActivateExtension(URL url, String key, String group) {
String value = url.getParameter(key);
return getActivateExtension(url, StringUtils.isEmpty(value) ? null : COMMA_SPLIT_PATTERN.split(value), group);
}
public List<T> getActivateExtension(URL url, String[] values, String group) {
List<T> activateExtensions = new ArrayList<>();
// 把values里提到的简称都集合起来
List<String> names = values == null ? new ArrayList<>(0) : asList(values);
if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
getExtensionClasses();
// 遍历所有带有@Activate的实现类
for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Object activate = entry.getValue();
String[] activateGroup, activateValue;
if (activate instanceof Activate) {
// 获取@Activate注解上的 group 和 value 信息
activateGroup = ((Activate) activate).group();
activateValue = ((Activate) activate).value();
} else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
} else {
continue;
}
// 当遍历到一些没有明确想要,也没有明确排除的实现类,要进行判断:
// 它必须满足组的要求,必须满足url的校验,才能算作校验通过
if (isMatchGroup(group, activateGroup)
&& !names.contains(name)
&& !names.contains(REMOVE_VALUE_PREFIX + name)
&& isActive(activateValue, url)) {
activateExtensions.add(getExtension(name));
}
}
// 按照order排序
activateExtensions.sort(ActivateComparator.COMPARATOR);
}
List<T> loadedExtensions = new ArrayList<>();
// 对于一些明确想要的实现类,自然是直接放进出参的结果列表里
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
if (!name.startsWith(REMOVE_VALUE_PREFIX)
&& !names.contains(REMOVE_VALUE_PREFIX + name)) {
if (DEFAULT_KEY.equals(name)) {
if (!loadedExtensions.isEmpty()) {
activateExtensions.addAll(0, loadedExtensions);
loadedExtensions.clear();
}
} else {
loadedExtensions.add(getExtension(name));
}
}
}
if (!loadedExtensions.isEmpty()) {
activateExtensions.addAll(loadedExtensions);
}
return activateExtensions;
}
// 通过url 和 @Activate 里的value属性值来判断是否匹配
// 假设,现在入参的url 为 "test://localhost/test?myname=zhanfu&age=18"
// 而我们有一个扩展点实现类上注解了
// @Activate(
// value = {"myname:zhanfu"},
// group = {"test"},
// order = 1
// )
// 此时两者都指定了myname这个属性,且属性值都为zhanfu,这就算匹配上了
private boolean isActive(String[] keys, URL url) {
if (keys.length == 0) {
return true;
}
for (String key : keys) {
// @Active(value="key1:value1, key2:value2")
String keyValue = null;
if (key.contains(":")) {
String[] arr = key.split(":");
key = arr[0];
keyValue = arr[1];
}
for (Map.Entry<String, String> entry : url.getParameters().entrySet()) {
String k = entry.getKey();
String v = entry.getValue();
if ((k.equals(key) || k.endsWith("." + key))
&& ((keyValue != null && keyValue.equals(v)) || (keyValue == null && ConfigUtils.isNotEmpty(v)))) {
return true;
}
}
}
return false;
}
现在我们已经知道了,dubbo-spi 实现了比 java-spi 更详尽的功能,随之而来的,肯定也是更高的学习成本。总结一下:
如果你实现了某个扩展点,配置文件需放在这三个目录下
- META-INF/services/
- META-INF/dubbo/internal/
- META-INF/dubbo/
然后使用 ExtensionLoader.getExtensionLoader 得到扩展点加载器后,有三种获得实现类的方式
- extensionLoader.getExtension(name) : 获取指定实现类
- extensionLoader.getAdaptiveExtension : 获取自适应实现类
- extensionLoader.getActivateExtension(url, values, group):按筛选条件获取一批实现类
另外 dubbo-spi 还有两个重要特性,与Spring容器相似,就是dubbo-spi支持
- 功能增强,允许构建装饰器类(与代理类似)来执行额外功能,真正的功能调用其他类实现
- 控制反转,会在构建一个实现类时,自动帮你注入其他实现类
上文我们已经对Dubbo的SPI机制进行了详尽的介绍,它的概念和基础使用还是十分简单的。但关于几个注解理解起来有一定复杂度,不过万事开头难,倒也不必惧怕,作为Dubbo的基础能力,如果本章内容不能牢牢掌握,则难以灵活使用和配置Dubbo,因此强烈建议进行收藏,而后时常复习,相信很快就融会贯通了
- 点赞
- 收藏
- 关注作者
评论(0)