Dubbo—SPI及自适应扩展原理

mock=com.alibaba.dubbo.rpc.support.MockProtocol
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
memcached=memcom.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol

看到这么多扩展类(每一个配置文件中都有很多),我们首先应该思考一个问题:Dubbo一启动,就加载所有的扩展类么?作为一个优秀的RPC框架,肯定不会耗时耗力做这样的无用功,所以肯定会通过一种方式拿到指定的扩展才对。我们可以看到大多是以键值对方式(表示为extName-value)配置的扩展,那么不难猜测,这里的extName就是用来实现上面所说的功能的。 那到底是不是呢?以上纯属猜测,下面就到源码中去验证。

SPI源码

Dubbo中实现SPI的核心类是ExtensionLoader,该类并未提供公共的构造方法来初始化,而是通过getExtensionLoader方法获取一个loader对象:

// loader缓存
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
public static ExtensionLoader getExtensionLoader(Class type) {
if (type == null)
throw new IllegalArgumentException(“Extension type == null”);
if(!type.isInterface()) {
throw new IllegalArgumentException(“Extension type(” + type + “) is not interface!”);
}
if(!withExtensionAnnotation(type)) {
throw new IllegalArgumentException(“Extension type(” + type +
“) is not extension, because WITHOUT @” + SPI.class.getSimpleName() + " Annotation!");
}

ExtensionLoader loader = (ExtensionLoader) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));
loader = (ExtensionLoader) EXTENSION_LOADERS.get(type);
}
return loader;
}

private final ExtensionFactory objectFactory;
private ExtensionLoader(Class<?> type) {
this.type = type;
objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

这里的class参数就是扩展点的接口类型,每一个loader都需要绑定一个扩展点类型。然后首先从缓存中获取loader,未获取到就初始化一个loader并放入缓存。而在私有构造器初始化的时候我们需要注意objectFactory这个变量,先大概有个映像,后面会用到。 拿到loader之后,就可以调用getExtension方法去获取指定的扩展点了,该方法传入了一个name参数,不难猜测这个就是配置文件中的键,可以debugger验证一下:

private final ConcurrentMap<String, Holder> cachedInstances = new ConcurrentHashMap<String, Holder>();
public T getExtension(String name) {
if (name == null || name.length() == 0)
throw new IllegalArgumentException(“Extension name == null”);
if (“true”.equals(name)) {
return getDefaultExtension();
}
// 从缓存中获取Holder对象,该对象的值就是扩展对象
Holder holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder());
holder = cachedInstances.get(name);
}
// 缓存中没有该扩展,说明还没有加载扩展类,就去配置文件中加载并创建对应的扩展对象
// 这里通过双重校验锁的方式保证线程安全,Dubbo中大量运用了该技巧
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}

同样的也是先从缓存拿,缓存没有就创建并添加到缓存,因此主要看createExtension方法:

// 扩展类实例缓存对象
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap

关键点代码就在getExtensionClasses方法中,怎么从配置文件中加载扩展类的。而该方法主要是调用了loadExtensionClasses方法:

private Map<String, Class<?>> loadExtensionClasses() {
// 判断接口上是否标注有 @SPI注解,该注解的值就是默认使用的扩展类,
// 赋值给cachedDefaultName变量缓存起来
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if(defaultAnnotation != null) {
String value = defaultAnnotation.value();
if(value != null && (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];
    }
    }

// 真正读取配置文件的方法
Map<String, Class<?>> extensionClasses = new HashMap

该方法主要是缓存当前扩展接口指定的默认扩展实现类(@SPI注解指定),并调用loadFile读取配置文件,从这里我们可以看到Dubbo默认是读取以下3个文件夹中的配置文件:

private static final String SERVICES_DIRECTORY = “META-INF/services/”;
private static final String DUBBO_DIRECTORY = “META-INF/dubbo/”;
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + “internal/”;

然后是loadFile,该方法很长,不全部放上来了,这里提取关键的代码:

String fileName = dir + type.getName();

首先通过文件全路径找到对应的文件,并用BufferedReader一行行读取文件内容:

String name = null;
int i = line.indexOf(‘=’);
if (i > 0) {
// 配置文件中的键
name = line.substring(0, i).trim();
// 扩展点全类名
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
// 加载class,如果有些类的依赖jar包未导入,这里就会抛出异常(比如WebserviceProtocol)
Class<?> clazz = Class.forName(line, true, classLoader);
// 验证当前类型是否是扩展类的父类型
if (! type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error when load extension class(interface: " +
type + ", class line: " + clazz.getName() + "), class "

  • clazz.getName() + “is not subtype of interface.”);
    }
    // 扩展类是否标注了 @Adaptive 注解,表示为一个自定义的自适应扩展类
    // 如果是将其缓存到cachedAdaptiveClass
    if (clazz.isAnnotationPresent(Adaptive.class)) {
    if(cachedAdaptiveClass == null) {
    cachedAdaptiveClass = clazz;
    } else if (! cachedAdaptiveClass.equals(clazz)) {
    // 超过一个自定义的自适应扩展类就抛出异常
    throw new IllegalStateException("More than 1 adaptive class found: "
  • cachedAdaptiveClass.getClass().getName()
  • ", " + clazz.getClass().getName());
    }
    } else {
    try {
    // 进入到该分支表示为Wrapper装饰扩展类,该类都有一个特征:包含
    // 一个有参的构造器,如果没有,就抛出异常进入到另一个分支,
    // Wrapper类的作用我们后面再分析
    clazz.getConstructor(type);
    // 缓存Wrapper到cachedWrapperClasses中
    Set<Class<?>> wrappers = cachedWrapperClasses; if (wrappers == null) { cachedWrapperClasses = new ConcurrentHashSet

至此,我们就看到了Dubbo SPI的实现全过程,我们也了解了Dubbo强大的扩展性是如何实现的,但是这么多扩展,Dubbo在运行中是如何决定调用哪一个扩展点的方法呢?这就是Dubbo另一强大的机制:自适应扩展。(PS:这里需要留意cachedAdaptiveClass和cachedWrapperClasses两个变量的赋值,后面会用到。)

二、自适应扩展机制

什么是自适应扩展?上文刚刚也说了,Dubbo中存在很多的扩展类,这些扩展类不可能一开始就全部初始化,那样非常的耗费资源,所以我们应该在使用到该类的时候再进行初始化,也就是懒加载。但是这是比较矛盾的,拓展未被加载,那么拓展方法就无法被调用(静态方法除外)。拓展方法未被调用,拓展就无法被加载(官网原话)。所以也就有了自适应扩展机制,那么这个原理是怎样的呢? 首先需要了解@Adaptive注解,该注解可以标注在类和方法上:

  • 标注在类上,表明该类为自定义的适配类

  • 标注在方法上,表明需要动态的为该方法创建适配类

当有地方调用扩展类的方法时,首先会调用适配类的方法,然后适配类再根据扩展名称调用getExtension方法拿到对应的扩展类对象,最后调用该对象的方法即可。流程就这么简单,下面看看代码怎么实现的。 首先我们回到ExtensionLoader的构造方法中:

objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());

其中调用了getAdaptiveExtension方法,从方法名不难看出就是去获取一个适配类对象:

private final Holder cachedAdaptiveInstance = new Holder();
public T getAdaptiveExtension() {
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
if(createAdaptiveInstanceError == null) {
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
}
}
}
}
else {
throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
}
}

return (T) instance;
}

该方法很简单,就是从缓存中获取适配类对象,未获取到就调用createAdaptiveExtension方法加载适配类并通过反射创建对象:

private T createAdaptiveExtension() {
try {
// 这里又注入了些东西,先略过
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can not create adaptive extenstion " + type + ", cause: " + e.getMessage(), e);
}
}

调用getAdaptiveExtensionClass加载适配类:

private Class<?> getAdaptiveExtensionClass() {
// 这里刚刚分析过了,从配置文件中加载配置类
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

cachedAdaptiveClass这个变量应该还没忘,在loadFile里赋值的,即我们自定义的适配扩展类,若没有则调用createAdaptiveExtensionClass动态创建:

private Class<?> createAdaptiveExtensionClass() {
// 生成适配类的Java代码,主要实现标注了@Adaptive的方法逻辑
String code = createAdaptiveExtensionClassCode();
ClassLoader classLoader = findClassLoader();
// 调用compiler编译
com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}

该方法就是生成适配类的字节码,你一定好奇适配类的代码是怎样的,只需要打断点就可以看到了,这里我们以Protocol类的适配类为例:

import com.alibaba.dubbo.common.extension.ExtensionLoader;

public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
public void destroy() {
throw new UnsupportedOperationException(“method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!”);
}

public int getDefaultPort() {
throw new UnsupportedOperationException(“method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!”);
}

public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) {
if (arg1 == null) throw new IllegalArgumentException(“url == null”);
com.alibaba.dubbo.common.URL url = arg1;
String extName = (url.getProtocol() == null ? “dubbo” : url.getProtocol());
if (extName == null)
throw new IllegalStateException(“Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(” + url.toString() + “) use keys([protocol])”);
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.refer(arg0, arg1);
}

public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) {
if (arg0 == null) throw new IllegalArgumentException(“com.alibaba.dubbo.rpc.Invoker argument == null”);
if (arg0.getUrl() == null)
throw new IllegalArgumentException(“com.alibaba.dubbo.rpc.Invoker argument getUrl() == null”);
com.alibaba.dubbo.common.URL url = arg0.getUrl();
String extName = (url.getProtocol() == null ? “dubbo” : url.getProtocol());
if (extName == null)
throw new IllegalStateException(“Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(” + url.toString() + “) use keys([protocol])”);
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.export(arg0);
}
}

后面会讲到Protocol扩展类都是通过export方法暴露服务,refer方法引用服务,而这两个方法在接口中都标注了@Adaptive注解,所以Dubbo为其生成了动态的适配类(这个和Java的动态代理的原理有点像。同时我们看到这两个方法中都通过getExtension方法去获取指定的扩展类的实例(这个扩展类名称来自于Invoker(后面会讲)中的url,因为dubbo是基于url驱动的,所有的配置都在url中)。 这就是Dubbo强大的自适应扩展机制的实现原理,我们可以将其运用到我们的项目中去,这就是看源码的好处。不过还有个问题,刚刚在createAdaptiveExtensionClass方法中你一定疑惑compiler是什么,它也是调用的getAdaptiveExtension获取适配类,这不就进入了死循环么? 当然不会,首先我们可以去配置文件看看Compiler的扩展类都有哪些:

adaptive=com.alibaba.dubbo.common.compiler.support.AdaptiveCompiler
jdk=com.alibaba.dubbo.common.compiler.support.JdkCompiler
javassist=com.alibaba.dubbo.common.compiler.support.JavassistCompiler

有一个AdaptiveCompiler类,从名字上我们就能猜到它是一个自定义的适配类了,然后在其类上可以看到@Adaptive注解验证我们的猜想,那么上文也说了在loadFile方法中会将该类赋值给cachedAdaptiveClass变量缓存,然后在createAdaptiveExtension -> getAdaptiveExtensionClass方法中获取并实例化对象,所以并不会死循环,那么在该类中做了什么呢?

public class AdaptiveCompiler implements Compiler {

// 这个是在哪赋值的?
private static volatile String DEFAULT_COMPILER;

public static void setDefaultCompiler(String compiler) {
DEFAULT_COMPILER = compiler;
}

public Class<?> compile(String code, ClassLoader classLoader) {
Compiler compiler;
ExtensionLoader loader = ExtensionLoader.getExtensionLoader(Compiler.class);
String name = DEFAULT_COMPILER; // copy reference
if (name != null && name.length() > 0) {
// 根据 DEFAULT_COMPILER 名称获取
compiler = loader.getExtension(name);
} else {
// 获取@SPI注解值指定的默认扩展
compiler = loader.getDefaultExtension();
}
return compiler.compile(code, classLoader);
}

}

该适配类会从两个地方获取扩展类,先来看看getDefaultExtension:

public T getDefaultExtension() {
getExtensionClasses();
if(null == cachedDefaultName || cachedDefaultName.length() == 0
|| “true”.equals(cachedDefaultName)) {
return null;
}
return getExtension(cachedDefaultName);
}

cachedDefaultName 这个不陌生吧,在loadExtensionClasses方法中赋值的,其值为@SPI的值,这里就是javassist。再看另外一条分支,是通过DEFAULT_COMPILER的值去获取的,这个变量提供了一个setter方法,点过去我们可以看到是在ApplicationConfig类中的setCompiler方法调用的,因为该类是配置类实例,也就是说可以通过dubbo:application的compiler参数来配置编译器类型,查看文档,也确实有这个配置参数。所以看源码能让我们了解平时项目中配置各个参数的意义,从而有针对的选择和配置适当的参数,而不是一味的照搬文档就完事。

三、Dubbo IOC

在上文中我们看到injectExtension这样一个方法,它是做什么的呢?接下来就详细分析它的作用和实现。

private T injectExtension(T instance) {
try {
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
if (method.getName().startsWith(“set”)
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
Class<?> pt = method.getParameterTypes()[0];
try {
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : “”;
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("fail to inject via method " + method.getName()

  • " of interface " + type.getName() + ": " + e.getMessage(), e);
    }
    }
    }
    }
    } catch (Exception e) {
    logger.error(e.getMessage(), e);
    }
    return instance;
    }

这个方法就是Dubbo依赖注入的实现,从上面代码中我们可以看出该方法是通过setter方法注入依赖扩展的(因为有些扩展点是需要依赖其它扩展点的,所以单单初始化当前扩展点还不行,还需要注入依赖的扩展):首先通过反射拿到参数的类型,然后从setter方法名中获取到扩展点的名称,最后从objectFactory中获取依赖的扩展实例并通过反射注入。objectFactory这个参数还记得是什么,怎么初始化赋值的么?这里具体的实例对象是(不清楚怎么来的忘记了就往上翻翻)AdaptiveExtensionFactory适配类类的对象,首先看看该类的初始化:

private final List factories;
public AdaptiveExtensionFactory() {
ExtensionLoader loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
List list = new ArrayList();
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

毕竟工作也这么久了 ,除了途虎一轮,也七七八八面试了不少大厂,像阿里、饿了么、美团、滴滴这些面试过程就不一一写在这篇文章上了。我会整理一份详细的面试过程及大家想知道的一些问题细节

美团面试经验

美团面试
字节面试经验
字节面试
菜鸟面试经验
菜鸟面试
蚂蚁金服面试经验
蚂蚁金服
唯品会面试经验
唯品会

因篇幅有限,图文无法详细发出
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
yle=“zoom: 33%;” />

最后

毕竟工作也这么久了 ,除了途虎一轮,也七七八八面试了不少大厂,像阿里、饿了么、美团、滴滴这些面试过程就不一一写在这篇文章上了。我会整理一份详细的面试过程及大家想知道的一些问题细节

美团面试经验

[外链图片转存中…(img-WNdWG2OU-1713080939700)]
字节面试经验
[外链图片转存中…(img-qcmOYns6-1713080939700)]
菜鸟面试经验
[外链图片转存中…(img-RzF2G7ny-1713080939700)]
蚂蚁金服面试经验
[外链图片转存中…(img-pN3JnQqE-1713080939700)]
唯品会面试经验
[外链图片转存中…(img-3hE0ILFD-1713080939701)]

因篇幅有限,图文无法详细发出
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 11
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值