前言
Dubbo 支持多种传输协议,可以有多种注册中心等等高扩展的功能,其原因就是 Dubbo SPI 提供的良好的扩展性,可以给开发者自己实现多样丰富的功能。Dubbo SPI在Java SPI的基础上发展而来,和Java SPI 一样,都是典型的策略模式实现,本文就主要介绍 Dubbo SPI 的使用方式和源码分析。
结合代码能够更深了解本文的内容,代码github地址:https://github.com/mikasaco/dubbo-study.git
Dubbo SPI 和 Java SPI 的比较
Java SPI 的使用方式可以参考博客
Dubbo SPI 相比于 Java SPI 有以下 3 个 优势:
-
Java SPI 每次都会把所有实现类都加载并实例化(是在迭代器迭代的时候创建实例),而 Dubbo SPI 是分两段创建实例,先进行类加载,然后在使用到具体实现的时候才实例化,并且 Dubbo SPI 大量使用缓存,会把 Class 对象和实例对象都缓存起来,性能更好;
-
Java SPI 在类加载失败的时候难以定位异常;
-
Dubbo SPI 还支持 IOC 和 AOP 。
补充一下JVM的类加载机制
类加载大的上来分有 加载、连接、初始化 3 个步骤。经常会误把加载等同于类加载,把加载包含初始化。
- 加载是把 class 文件读到方法区,并创建一个 Class 对象。 代码
Class.forName()
就是实现了加载; - 连接细分有包括了验证、准备、解析,这中间的步骤都是 JVM 实现,开发者不能干预;
- 初始化阶段是执行类定义的构造方法,创建实例对象。代码
Class.newInstance()
就是实现初始化这一步。
在 Dubbo SPI 中把类加载就分成了两部分,先会加载 class 文件,但并不会立刻初始化,而是在使用的时候再调用 Class.newInstance()
,并且会将 Class.forName()
加载之后的 Class 对象和 Class.newInstance()
实例对象都缓存起来。
Dubbo SPI 的使用
我们以最简单的使用姿势为例,可以参考 github 中 basic 的实现。主要是以下几点:
- 标注了 @SPI 注解的接口;
- 实现了 @ SPI 注解的接口的实现类;
- 在
META-INF/services/
META-INF/dubbo/
META-INF/dubbo/internal/
目录下创建接口全限定名的文件; - 文件中的内容是 key-value 形式, key 是实现类的别名, value 是实现类的全限定名。
Dubbo SPI 的三大注解
@SPI
用在接口上,标注这个接口是一个扩展点,可以使用 Dubbo SPI 功能。如果没加 @SPI 注解,会在 ExtensionLoader.getExtensionLoader
的时候就抛出异常。
如果 @SPI(“javassist”) 这种,也就是接口的默认实现的别名是 javassist ,通过 loader.getDefaultExtension()
可以获取默认实现。
@adaptive
@adaptive 也叫做自适应注解,顾名思义可以根据参数自己选择实现类。使用的方式有两种:注解在实现类上或注解在接口的方法上,Dubbo中只有两个类 AdaptiveCompiler 和 AdaptiveExtensionFactory 在类上使用该注解。
接口的方法
用在接口方法上时,会根据参数自动生成一个类,类名是 接口$Adaptive
。然后在调用loader.getAdaptiveExtension()
的时候就会执行这个生成的类,再根据参数去调用不同的实现类。
实现类
用在实现类上的话,标注这个实现类是接口的适配器实现,不会自动生成代码。调用 loader.getAdaptiveExtension()
会返回这个实现类。但注意和loader.getDefaultExtension()
的区别。
ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
Compiler adaptiveExtension = loader.getAdaptiveExtension();
adaptiveExtension.compile(); // AdaptiveCompiler 这个返回的只是配置中的key为adaptive的实现
Compiler defaultExtension = loader.getDefaultExtension();
defaultExtension.compile(); //JavassistCompiler 这个才是默认实现
@activate
通常获取扩展实现只能获取到一个实现类,但例如拦截器等场景通常需要我们可以扩展一系列的功能,而 @activate 注解就是为了这种场景而生的。@activate 可以注解在很多实现类上,然后通过 group 或 value 在URL参数中自动激活不同的多个实现类。
Dubbo SPI 的实现原理
Dubbo SPI 的实现原理我们探究这几种情况:
- 普通扩展原理
- 适配器扩展原理
- IOC 扩展原理
- AOP 扩展原理
- 自动激活扩展原理
普通扩展原理
使用注意
普通扩展的使用很简单,需要注意的是 @SPI(“javassist”) 注解中的 value 就是这个扩展点的默认实现,通过 ExtensionLoader.getDefaultExtension
可以直接获取到默认实现。
源码解析
ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
Compiler adaptiveExtension = loader.getDefaultExtension();
对于 ExtensionLoader.getExtensionLoader()
这行代码就是获取了接口的扩展类加载器,new ExtensionLoader<T>(type)
type 就是 Compiler.class ,所以很简单,就是 new 了一个 ExtensionLoader。
而实现扩展的代码是在 ExtensionLoader#getExtension()
中,我们重点分析获取 ExtensionLoader#getExtension()
这行代码。
上图就是 Dubbo SPI 加载扩展类的方式,普通扩展是其他扩展的基础,从上面的部分我们也能看出 Dubbo SPI 相比于 Java SPI 的优势:
- Dubbo SPI 并不是程序一启动就会加载扩展实现,而是在调用
extensionLoader.getDefaultExtension()
等的时候才会去加载扩展类; - Dubbo SPI 对 Class 对象、实例对象都会进行缓存,性能更好。
下面看下具体的源码。
//ExtensionLoader#getExtension
public T getExtension(String name) {
...
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;
}
//ExtensionLoader#createExtension
private T createExtension(String name) {
Class<?> clazz = getExtensionClasses().get(name);// 获取Class对象
...
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());// 创建实例
}
... // 普通扩展不用关注,在IOC和AOP部分需要注意
}
//ExtensionLoader#getExtensionClasses
private Map<String, Class<?>> getExtensionClasses() {
Map<String, Class<?>> classes = cachedClasses.get();// 从Class缓存中取
...
if (classes == null) {
classes = loadExtensionClasses();// 没获取到就自己去加载
cachedClasses.set(classes);
}
}
//ExtensionLoader#loadExtensionClasses
private Map<String, Class<?>> loadExtensionClasses() {
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
String value = defaultAnnotation.value();// 这里获取SPI的默认实现
...
}
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);// META-INF/dubbo/internal/
loadDirectory(extensionClasses, DUBBO_DIRECTORY); // META-INF/dubbo/
loadDirectory(extensionClasses, SERVICES_DIRECTORY);// META-INF/services/
return extensionClasses;
}
//ExtensionLoader#loadDirectory
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
...
urls = classLoader.getResources(fileName); //获取配置文件(还未读入内存)
...
while (urls.hasMoreElements()) {
java.net.URL resourceURL = urls.nextElement();
loadResource(extensionClasses, classLoader, resourceURL);// 解析配置文件的内容并加载
}
}
//ExtensionLoader#loadResource
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8")); // IO读取配置文件
...
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name); // Class.forName 加载Class文件,创建Class对象
}
适配扩展原理
使用注意
区别点 | 注解在接口方法 | 注解在实现类 |
---|---|---|
getAdaptiveExtension | 自动生成的类,但会根据参数动态选择别的实现 | 注解了 @Adaptive 的实现类 |
配置文件 | 不需要写 | 需要,key 随便写什么 |
参数 | 方法参数必须包含 URL 实例 | 没要求 |
当注解在接口方法上时,如果是 @Adaptive({“type”,“subType”}) ,匹配规则:
- 先匹配配置文件中 key 有没有等于 type 的 value的;
- 再匹配配置文件中 key 有没有等于 subType 的 value的;
- 最后只能用默认实现,接口上注解的 @SPI(“默认实现”)。
源码解析
适配扩展的原理和普通扩展有些类似,在适配扩展的代码中一部分和普通扩展是重合的。
适配扩展需要使用 @Adaptive 注解,上文提到该注解有两种使用方式:注解在实现类上和注解上接口方法上,两者的区别在 ExtensionLoader#loadClass()
开始体现。
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
...
if (clazz.isAnnotationPresent(Adaptive.class)) { // 实现类上有注解@adaptive
if (cachedAdaptiveClass == null) {
cachedAdaptiveClass = clazz; // 把这个实现类放入cachedAdaptiveClass中
}
...
}
}
private Class<?> getAdaptiveExtensionClass() {
getExtensionClasses();
//如果cachedAdaptiveClass缓存中有就返回Class对象了,也就是之前注解在实现类上的Class对象
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
return cachedAdaptiveClass = createAdaptiveExtensionClass();//自动创建适配类
}
private Class<?> createAdaptiveExtensionClass() {
String code = createAdaptiveExtensionClassCode();// 自动创建的类的字符串
ClassLoader classLoader = findClassLoader();
// 获取编译器Compiler
com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);// 编译
}
注解在接口方法上需要自动生成一个扩展类,这个类本来是没有的,是通过字符串拼接出来的,而加载的方式是动态编译,有 JDK 编译器和 javassist 编译器两种方式(还有一个是注解了 @Adaptive 的 AdaptiveCompiler,这个就是根据参数选择用 JDK 还是 javassist) ,Dubbo 默认的是 javassist。javassist是一个动态编译的 Java 类库,相比于 JDK 的动态编译,效率更高。
Java 动态生成 Class 的方式(动态编译)有两种:基于字节码的操作、基于 API 的操作。javassist 属于后者,ASM 是前者,前者性能更高,但要操作字节码比较复杂。
IOC 扩展原理
使用注意
-
使用 IOC-SPI 的扩展方式时,注入的扩展必须是适配扩展的方式,因为在 SpiExtensionFactory 获取的是适配扩展;
-
使用 IOC-SPI 的扩展方式时, setXXX(Class c) 去找注入的扩展实现时,只会用到 Class ,也就是要注入的扩展接口, XXX 随便写什么;
-
使用 IOC-spring 的扩展方式时,先根据 setXXX 中的 XXX 去 ApplicationContext 容器中匹配 bean name 获取扩展实现,没有找到再根据 Class 类型去匹配。
源码解析
Dubbo 的 IOC 最突出的体现就是在使用扩展点的时候,可以注入其他扩展点。具体方法在 ExtensionLoader#injectExtension
,对于普通扩展、适配扩展等都会在加载扩展实现类之后执行这个方法,如果有依赖(set 属性的方法)的话,就通过这个方法注入。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k0sa8FWp-1579508037687)(Dubbo-扩展点SPI/image-20191212204903256.png)]
private T injectExtension(T instance) {
for (Method method : instance.getClass().getMethods()) { // 遍历所有的方法
if (method.getName().startsWith("set") // 如果以set开头,且public修饰,参数一个
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
if (method.getAnnotation(DisableInject.class) != null) {
continue; // 方法注释DisableInject 跳过
}
Class<?> pt = method.getParameterTypes()[0];
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
// spring和SPI的方式获取符合的扩展类
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object); //调用set方法进行注入
}
}
}
}
// AdaptiveExtensionFactory#getExtension
public <T> T getExtension(Class<T> type, String name) {
for (ExtensionFactory factory : factories) {
T extension = factory.getExtension(type, name);
}
}
// SpiExtensionFactory#getExtension
public <T> T getExtension(Class<T> type, String name) {
if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
if (!loader.getSupportedExtensions().isEmpty()) {
// 这里获取的是适配扩展,所以SPI的方式一定要是适配扩展
return loader.getAdaptiveExtension();
}
}
return null;
}
// SpringExtensionFactory#getExtension
public <T> T getExtension(Class<T> type, String name) {
for (ApplicationContext context : contexts) {
if (context.containsBean(name)) {
Object bean = context.getBean(name); // 根据 name 获取 bean
}
}
for (ApplicationContext context : contexts) {
return context.getBean(type); // 根据类型获取
...
}
}
}
AOP 扩展原理
使用注意
- 对于扩展实现类和扩展接口,并不需要做额外的处理,这也体现了 AOP 的无侵入的特点;
- 装饰类继承扩展接口,需要声明构造方法,并且传入参数是扩展接口类型;
- Dubbo 的 AOP 实现是典型的装饰者模式;
- 可以被多重装饰,最外层的装饰是配置文件最下面的装饰类。
源码解析
Dubbo SPI 支持 AOP 功能,是用装饰者模式实现的。当执行 extensionLoader.getDefaultExtension()
返回的其实是装饰对象(wrapper2)而不是原来的扩展实现(javassistCompiler),所以执行的是增强的方法。
需要注意的是,我们配置文件中装饰类的顺序会对装饰的顺序有影响的。例如 github 上的例子,如果把 wrapper1 和 wrapper2 换一下顺序,那么装饰的顺序也会变化。
- 获取到的扩展实现其实是装饰对象,而非原来的扩展实现类;
- 配置文件中装饰类顺序从上往下依次是增强装饰的从内到外。
带着这两个结论,我们看下 Dubbo 是如何实现的。主要是在两个地方: loadClass 方法和 createExtension方法。
loadClass
Dubbo 在解析配置文件中的每行配置信息时,会判断这行配置对应类的构造方法的传入参数类型,是不是扩展接口,如果是的话就把这行配置对应的类放入 cachedWrapperClasses (缓存装饰类)。
这里可以解释上面的第 2 点结论:配置文件中装饰类顺序从上往下依次是增强装饰的从内到外。因为解析配置信息也是一行一行解析的,所以配置文件上面的装饰类(构造方法的传入参数类型是扩展接口),会先放入 cachedWrapperClasses 。
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
...
else if (isWrapperClass(clazz)) {
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz); // 放入装饰类缓存
}
}
// 判断类的构造方法的传入参数类型是不是扩展接口
private boolean isWrapperClass(Class<?> clazz) {
try {
clazz.getConstructor(type);
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
createExtension
创建扩展类实例的时候,会遍历装饰类缓存,有的话,把扩展实现类当作构造方法的参数传入进去,创建装饰类实例。所以最后返回的是最外层的装饰类。
private T createExtension(String name) {
...
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
// 从装饰类缓存中依次取出装饰类,并把实际的扩展类(github例子中的javassist)当作参数传入第一个装饰类(wrapper1,因为配置文件中wrapper1先解析),然后 instance会被覆盖,再次循环进行装饰(例子中装饰wrapper2,传入参数wrapper1)
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
...
}
自动激活扩展原理
当调用 ExtensionLoader.getActivateExtension(URL url, String[] values, String group)
会根据 URL 中的参数和 value 以及 group 去匹配符合的扩展实现。@Activate 注解通常有三个参数:
- group 激活同一组的扩展实现;
- value 激活value名称符合的扩展实现;
- order 激活的扩展实现排序。
public List<T> getActivateExtension(URL url, String[] values, String group) {
List<T> exts = new ArrayList<T>();
List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
// 如果value -default 的参数不会激活
if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
// 这一步加载扩展类的Class对象,如果类上注解了Activate会放入缓存cachedActivates,给后面遍历
getExtensionClasses();
for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Activate activate = entry.getValue();
if (isMatchGroup(group, activate.group())) {// group匹配实现类
T ext = getExtension(name);
if (!names.contains(name)// value匹配实现类
&& !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
&& isActive(activate, url)) {
exts.add(ext);
}
}
}
Collections.sort(exts, ActivateComparator.COMPARATOR);// order排序
}
...
return exts;
}
ExtensionFactory
ExtensionFactory 是创建整个 SPI 的核心,但 ExtensionFactory 本身又是通过 SPI-Adaptive 的方式创建的,在学习 SPI 这部分的时候,总有种鸡生蛋,蛋生鸡的疑惑,我们以 Dubbo 启动的入口 com.alibaba.dubbo.container.Mian
看下 ExtensionFactory 是怎么加载的。
-
Main 入口类初始化的时候,创建静态成员变量 容器加载类 ExtensionLoader< Container > loader;
-
执行 ExtensionLoader 的构造方法,关键就在这个构造方法中;
private ExtensionLoader(Class<?> type) { this.type = type; objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); }
-
最后
getAdaptiveExtension()
返回的是 ExtensionFactory 的实现类 AdaptiveExtensionFactory ,因为有注解 @Adaptive。
AdaptiveExtensionFactory。调用 getAdaptiveExtension() 方法会返回这个实现类,一般都是用的这个,但这个实现类最后还是要调用 SPI/spring 的实现类,也就是最后获取的具体实现还是通过 SPI/spring 的方式。
构造方法注入 SPI/spring 的 ExtensionFactory 工厂,getExtension() 获取实现的方法就是遍历这两个工厂,从中取扩展实现类。
public class AdaptiveExtensionFactory implements ExtensionFactory {
private final List<ExtensionFactory> factories;// SPI/spring 工厂
public AdaptiveExtensionFactory() {
...
for (String name : loader.getSupportedExtensions()) {
list.add(loader.getExtension(name)); //塞入 SPI/spring 工厂
}
}
public <T> T getExtension(Class<T> type, String name) {
for (ExtensionFactory factory : factories) { // 遍历 SPI/spring 工厂
T extension = factory.getExtension(type, name); // 取扩展类
...
}
}
}