想要深入理解dubbo,SPI机制是绕不过去的,面试中也经常被提问到,dubbo大部分组件都是基于SPI机制实现的。
那么什么是SPI机制呢,要解决什么问题呢, 实际上要解决的问题就是,一个接口有多个实现类时,具体实现类不在程序中直接确定,而是由程序之外的配置掌控。这是设计模式中的策略模式。
dubbo SPI是基于java SPI机制实现的,进行一定的改造和优化,dubbo官网对java的spi也有一定的介绍。
那么dubbo 为什么不直接用java的SPI呢,主要概括是三点原因,
1.java可能一次加载所有扩展点,不用的话资源浪费,dubbo改成了按需加载实现类。
2.java spi以com.xxx.xxx com.xx.xxx格式配置,容易吃掉异常,dubbo改为xx=xxx.xx.xxx键值对配置,容易定位问题。
3.dubbo还增加了 IOC 和 AOP 等特性。
再具体一点,dubbo想要根据url去实现不同的注册中心,协议,负载均衡策略,序列化方式等等。正是由于spi的存在,我们才能在配置中 配置不同的注册中心,负载均衡策略等,我们知道dubbo是以总线模式来时刻传递和保存配置信息的,也就是配置信息都被放在URL上进行传递。然后根据url的参数,dubbo就能动态的去找对应的实现类。
那么比较核心的实现,就是com.alibaba.dubbo.common.extension包下的三个注解@SPI,@Adaptive,@Activate以及核心实现的类ExtensionLoader。
1.扩展点注解@SPI
@SPI注解的作用是标记一个接口为一个扩展点,可以有多个实现,有一个value属性,这个属性通过配置好的键值对匹配,即为默认实现类。该注解只是一个标记,需要跟其他两个注解配合使用。
用注册中心举例,我们知道dubbo支持很多注册中心,如nacos,Multicast,zookeeper,redis等等。官方推荐和我们日常使用的是zookeeper,但通过注解的value可知,其默认实现为"dubbo",根据配置文件可以找到,对应的实现类为DubboRegistryFactory,这是一个dubbo不依赖于其他中间件,自己写的一个注册中心,连dubbo官方文档都没有怎么提到。
2.扩展点自适应注解@Adaptive
@Adaptive这个注解在dubbo框架中一个注解干了2件事情,分别是标记在类级别和标记在方法上,功能并不相同。
当@Adaptive标记在类级别上时,表名该类是该接口的适配器。例如ExtensionFactory,有三个实现类AdaptiveExtensionFactory,SpiExtensionFactory,SpringExtensionFactory,其中AdaptiveExtensionFactory类标记了@Adaptive,该类则不提供具体实现逻辑,用来选择调用其他两个实现类具体哪种实现。在dubbo框架中在类级别标记的情况较少,@Adaptive主要标记在方法上。
@Adaptive在方法上标记,可以动态的通过url中参数选择具体实现类。用负载均衡举例子。
当我们配置负载均衡策略为roundrobin时,我们请求url中会含有“...&loadbalance=roundrobin&...”,@Adaptive中的“loadbalance”会匹配url中的参数,匹配到对应的参数值为roundrobin,根据配置文件可知对应负载均衡的实现类为RoundRobinLoadBalance,如果我们没有配置策略,则会根据@SPI中的value值RandomLoadBalance.NAME “random”,可知对应实现类为RandomLoadBalance。
再返回过来看@Adaptive的注释,就比较容易好理解,实际上注解的参数可以是一个数组,会依次匹配,如果没匹配到则会用驼峰规则去匹配,如果还没匹配到SPI也没有默认参数,则抛出IllegalStateException。
3.拓展点自动激活注解@Activate
@Activate一般是标记类级别上,主要是使用在多个扩展点实现,根据不同条件被激活的场景中,实际项目中比较实用的就是dubbo过滤器,@Activate有几个重要参数。
group,一般为provider或consumer,表示提供者或消费者调用时触发。
value,如果url中含有该key值,则被触发。
order,排序。
我写一个简单的demo。
接口Filter有@SPI注解,假如有多个filter实现类,可以根据@Activate注解表示,什么时候调用触发,排序等等,这个demo表示在服务端被调用时打印一句话。
4.核心实现类ExtensionLoader
ExtensionLoader是整个拓展机制的核心实现类,其中有三个最重要的方法,getExtension,getAdaptiveExtension,getActivateExtension。
getExtension
@SuppressWarnings("unchecked")
public T getExtension(String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
if ("true".equals(name)) {
return getDefaultExtension();
}
final Holder<Object> holder = getOrCreateHolder(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;
}
getExtension方法用于获取拓展类实例,基本思路就是检查缓存中是否有现成的数据,没有则调用createExtension去创建。
@SuppressWarnings("unchecked")
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);
}
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
initExtension(instance);
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}
createExtension的创建过程中,先调用getExtensionClasses()方法,也是先去缓存中检查,如果为空,则会加载META-INF/services,META-INF/dubbo,META-INF/dubbo/internal这几个配置的路径,将所有拓展类加载到jvm中,注意此时仅仅是加载class,并没有实例化,最后只初始化所需要的拓展类并放入缓存中,下次再调用时直接从缓存中获取即可。
getAdaptiveExtension
@Adaptive注解的主要功能是根据url的参数,以一定规则去匹配对应的拓展实现类,这里主要记录下实现的手段。
package com.alibaba.dubbo.registry;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class RegistryFactory$Adaptive implements com.alibaba.dubbo.registry.RegistryFactory {
public com.alibaba.dubbo.registry.Registry getRegistry(com.alibaba.dubbo.common.URL arg0) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg0;
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.registry.RegistryFactory) name from url(" + url.toString() + ") use keys([protocol])");
com.alibaba.dubbo.registry.RegistryFactory extension = (com.alibaba.dubbo.registry.RegistryFactory)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.registry.RegistryFactory.class).getExtension(extName);
return extension.getRegistry(arg0);
}
}
@SPI("dubbo")
public interface RegistryFactory {
@Adaptive({"protocol"})
Registry getRegistry(URL url);
}
以注册工厂为例子,在getAdaptiveExtension中,会动态拼接java代码字符串,生成类名+$Adpative这样的类,再编译成自适应类并初始化,这段java代码是根据RegistryFactory的注解为@SPI("dubbo"),方法注解为 @Adaptive({"protocol"}),拼接成了其中的String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );由此可知,url的protocol参数为空时会用dubbo,否则用url上的protocol参数,最终还是调用的getExtension方法去获取实例。这正是@Adaptive逻辑的实现手段,具体还有很多其他细节可以找对应的源码学习。
getActivateExtension
public List<T> getActivateExtension(URL url, String[] values, String group) {
List<T> activateExtensions = new ArrayList<>();
List<String> names = values == null ? new ArrayList<>(0) : Arrays.asList(values);
if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
getExtensionClasses();
for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Object activate = entry.getValue();
String[] activateGroup, activateValue;
if (activate instanceof Activate) {
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;
}
if (isMatchGroup(group, activateGroup)
&& !names.contains(name)
&& !names.contains(REMOVE_VALUE_PREFIX + name)
&& isActive(activateValue, url)) {
activateExtensions.add(getExtension(name));
}
}
activateExtensions.sort(ActivateComparator.COMPARATOR);
}
getActivateExtension基本思路也是差不多,通过调用getExtensionClasses()检查缓存,确保加载对应的class在缓存中,遍历拓展类,根据isMatchGroup方法判断满足条件的再进行实例化激活,排序等等。
最后,文章有哪些不对的地方,欢迎给我留言。