Dubbo SPI的自适应扩展原理
很多人在学习 SPI 的时候将@SPI
和@Adaptive
注解混在一起学习,最后学得晕晕乎乎看完之后似懂非懂,如果你也有这种困扰,请继续阅读。
并不是说不该将这两个内容一起学习,而是要有个先后顺序再加上自己的推理。是先有 SPI 机制,然后才有的自适应扩展,自适应扩展是基于 SPI 机制的高级特性。
# 为什么需要自适应扩展点
在 Dubbo 中,很多拓展都是通过 SPI 机制进行加载的,比如 Protocol、Cluster、LoadBalance 等。有时,有些拓展并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。这听起来有些矛盾。拓展未被加载,那么拓展方法就无法被调用(静态方法除外)。拓展方法未被调用,拓展就无法被加载。对于这个矛盾的问题,Dubbo 通过自适应拓展机制很好的解决了。
对于这个问题,以之前 demo 为例进行我们进行推演:
public interface MySPI {
void say();
}
public class HelloMySPI implements MySPI{
@Override
public void say() {
System.out.println("HelloMySPI say:hello");
}
}
public class GoodbyeMySPI implements MySPI {
@Override
public void say() {
System.out.println("GoodbyeMySPI say:Goodbye");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
现在要增加一个接口 Person,他可以和人打招呼。他有一个实现类是Man
,他可以动态的跟人说 hello 或者 goodbye
public interface Person {
void greeting();
}
public class Man implements Person{
private MySPI adaptive;
public void setAdaptive(MySPI adaptive) {
this.adaptive = adaptive;
}
@Override
public void greeting(URL url) {
adaptive.say(url);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
但是adaptive
成员要么是HelloMySPI
的实例化对象,要么是GoodbyeMySPI
的实例化对象,怎么实现动态的去根据需要获取呢?解决这个问题就可以增加一个代理,作为自适应类。所以增加自适应扩展实现如下:
public class AdaptiveMySPI implements MySPI {
@Override
public void say() {
// 1.通过某种方式获得拓展实现的名称
String mySpiName;
// 2.通过 SPI 加载具体的 mySpi
MySPI myspi = ExtensionLoader.getExtensionLoader(MySPI.class).getExtension(mySpiName);
// 3.调用目标方法
myspi.say();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
将代理类AdaptiveMySPI
作为Man
的成员对象,这样就可以实现按需调用了。按需加载如何实现呢?之前我们在getExtension()
方法中提到过,只要在根据名字查找的时候,才会按照需要懒加载,所以这个问题天然被 Dubbo SPI 解决了。那么剩下的关键就是如何按需调用,也就是如何获得名字。
- 可以在当前线程的上下文中获得,比如通过 ThreadLocal 保存。
- 可以通过接口参数传递,但是这样就需要实现自适应扩展的接口按照约定去定义参数,否则就无法拿到名字,这样对于被代理的接口是有一定限制的。
Dubbo 用的是第二种方式,也就是他总有办法从参数中动态拿到扩展类名。
# 2.模拟原理 demo
再具体一些 Dubbo 是怎么实现的呢?
自适应拓展机制的实现逻辑比较复杂,首先 Dubbo 会为拓展接口生成具有代理功能的代码。然后通过 javassist 或 jdk 编译这段代码,得到 Class 类。最后再通过反射创建代理类,整个过程比较复杂。为了让大家对自适应拓展有一个感性的认识,按照之前的知识,下面我们继续对之前 demo 为例进行改造:
@SPI
public interface MySPI {
void say(URL url);
}
public class HelloMySPI implements MySPI{
@Override
public void say(URL url) {
System.out.println("HelloMySPI say:hello");
}
}
public class GoodbyeMySPI implements MySPI {
@Override
public void say(URL url) {
System.out.println("GoodbyeMySPI say:Goodbye");
}
}
public class AdaptiveMySPI implements MySPI {
@Override
public void say(URL url) {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
// 1.从 URL 中获取 mySpi 名称
String mySpiName = url.getParameter("myspi.type");
if (mySpiName == null) {
throw new IllegalArgumentException("MySPI == null");
}
// 2.通过 SPI 加载具体的 mySpi
MySPI myspi = ExtensionLoader.getExtensionLoader(MySPI.class).getExtension(mySpiName);
// 3.调用目标方法
myspi.say(url);
}
}
@SPI("man")
public interface Person {
void greeting(URL url);
}
public class Man implements Person {
private MySPI adaptive = = ExtensionLoader.getExtensionLoader(MySPI.class).getExtension(“adaptive”);
public void setAdaptive(MySPI adaptive) {
this.adaptive = adaptive;
}
@Override
public void greeting(URL url) {
adaptive.say(url);
}
}
public static void main(String[] args) {
ExtensionLoader<Person> extensionLoader = ExtensionLoader.getExtensionLoader(Person.class);
Person hello = extensionLoader.getExtension("man");
hello.greeting(URL.valueOf("dubbo://192.168.0.101:100?myspi.type=hello"));
hello.greeting(URL.valueOf("dubbo://192.168.0.101:100?myspi.type=goodbye"));
}
//输出
HelloMySPI say:hello
GoodbyeMySPI say:Goodbye
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
大家与之前的代码对比就可以发现区别,MySPI 的方法增加了 URL 参数,因为 dubbo 中 url 就是作为一个配置总线贯穿整个调用链路的。这样便可以拿到扩展名,动态调用和加载了。
比如 demo 中的一个 URL
dubbo://192.168.0.101:100?myspi.type=hello
AdaptiveMySPI 动态的人从 url 中拿到了myspi.type=hello
,然后根据 name 拿到了扩展实现,以此完成动态调用。
上面的示例展示了自适应拓展类的核心实现 —- 在拓展接口的方法被调用时,通过 SPI 加载具体的拓展实现类,并调用拓展对象的同名方法。所以接下来的关键就在于,自适应拓展类是如何生成的,Dubbo 是怎么做的。
# 3.@Adaptive 注解
关于 Dubbo 的自适应扩展,一定避不开这个关键注解@Adaptive
。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
String[] value() default {};
}
2
3
4
5
6
从上面的代码中可知,Adaptive 可注解在类或方法上。
- 当 Adaptive 注解在类上时,Dubbo 不会为该类生成代理类。Adaptive 注解在类上的情况很少,在 Dubbo 中,仅有两个类被 Adaptive 注解了,分别是 AdaptiveCompiler 和 AdaptiveExtensionFactory。此种情况,表示拓展的加载逻辑由人工编码完成。
- 注解在方法(接口方法)上时,Dubbo 则会为该方法生成代理逻辑。更多时候,Adaptive 是注解在接口方法上的,表示拓展的加载逻辑需由框架自动生成。
Adaptive 注解的地方不同,相应的处理逻辑也是不同的。注解在类上时,处理逻辑比较简单,本文就不分析了。注解在接口方法上时,处理逻辑较为复杂,本章将会重点分析此块逻辑。
# 4.源码解读
在 Dubbo 中如何获得一个自适应扩展类呢?只需要一行代码。
MySPI adaptive = ExtensionLoader.getExtensionLoader(MySPI.class).getAdaptiveExtension();
@SPI
public interface MySPI {
@Adaptive(value = {"myspi.type"})
void say(URL url);
}
2
3
4
5
6
7
getExtensionLoader()
之前我们依旧分析过了,这里直接从getAdaptiveExtension()
开始。
getAdaptiveExtension()
方法是获取自适应拓展的入口方法,首先会检查缓存cachedAdaptiveInstance
,缓存未命中,则会执行双重检查,调用 createAdaptiveExtension()
方法创建自适应拓展。
public T getAdaptiveExtension() {
// 从缓存中查找自适应扩展类实例,命中直接返回
Object instance = cachedAdaptiveInstance.get();
//缓存没有命中
if (instance == null) {
…… 异常处理
//双重检查
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
// 创建自适应实例
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
…… 异常处理
return (T) instance;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
createAdaptiveExtension()
会首先查找只适应扩展类,然后通过反射进行实例化,再调用injectExtension
对扩展实例中注入依赖。
你可能会问直接返回依赖不就行了?为什么还需要注入?
这是因为任何利用 Dubbo SPI 机制加载的用户创建类都是有可能有成员依赖于其他拓展类的,用户实现的自适应扩展类也不例外。而另一种 Dubbo 自己生成的自适应扩展类则不可能出现依赖其他类的情况。
这里只关注重点方法getAdaptiveExtensionClass()
。
- 首先
getExtensionClasses()
会获取该接口所有的拓展类, - 然后会检查缓存是否为空,
cachedAdaptiveClass
缓存着自适应扩展类的类型。 - 如果缓存中不存在则调用
createAdaptiveExtensionClass
开始创建自适应扩展类。
// 用于缓存自适应扩展类的类型
private volatile Class<?> cachedAdaptiveClass = null;
private Class<?> getAdaptiveExtensionClass() {
// 加载所有的拓展类配置
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
// 创建自适应拓展类
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
2
3
4
5
6
7
8
9
10
11
getExtensionClasses
之前讲过,你肯定会奇怪为什么还需要加载所有扩展类。在这里有个关键逻辑,在调用 loadResource 方法时候会解析@Adaptive 注解,如果被标注了,就表示这个类是一个自适应扩展类实现,会被设置到缓存cacheAdaptiveClass
中。
所以有两个原因:
1、是自定义自适应扩展类需要 SPI 机制加载
2、是设置缓存
# 5. 生成自适应扩展类
非自己实现的自适应扩展类,都要走createAdaptiveExtensionClass
逻辑。
主要逻辑如下:
- 动态生成自适应扩展类代码
- 获取类加载器和编译器类(Dubbo 默认使用 javassist 作为编译器)
- 编译、加载动态生成的类
private Class<?> createAdaptiveExtensionClass() {
// 动态生成自适应扩展类代码
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
// 获取类加载器
ClassLoader classLoader = findClassLoader();
// 获取编译器类 ⚠️ AdaptiveCompiler 也是自己定义的
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
//编译、加载、生产Class
return compiler.compile(code, classLoader);
}
2
3
4
5
6
7
8
9
10
生成自适应扩展类的代码都在AdaptiveClassCodeGenerator
中,generate()
方法会生成和返回一个自适应扩展类。之前的版本代码其实比较复杂,逻辑都写在了一起,并没有 generate
方法,进入 Apache 孵化之后对代码结构进行了调整,结构清晰了许多。
主要逻辑如下:
- 检查接口是否有方法被@Adaptive 修饰。
- 生产 class 头部的 package 信息。
- 生成依赖类的 import 信息。
- 生成方法声明信息。
- 遍历接口方法依次生成实现方法。
- 类结束用
}
收尾,类信息转换为字符串返回。
public String generate() {
// no need to generate adaptive class since there's no adaptive method found.
// 检查接口是否有注解了Adaptive的方法,至少需要有一个
if (!hasAdaptiveMethod()) {
throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
}
StringBuilder code = new StringBuilder();
// 生成package信息
code.append(generatePackageInfo());
// 生成依赖类的import信息
code.append(generateImports());
// 生成类的声明信息 public class 接口名$Adaptive implements 接口名
code.append(generateClassDeclaration());
// 遍历接口方法 按需生产实现类
Method[] methods = type.getMethods();
for (Method method : methods) {
code.append(generateMethod(method));
}
// 类结尾
code.append("}");
if (logger.isDebugEnabled()) {
logger.debug(code.toString());
}
// 转换为字符串返回
return code.toString();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
下面依次介绍上述步骤的主要逻辑。
# 检查@Adaptive 注解
遍历接口方法依次检查是否被@Adaptive 标注,至少需要有一个方法被注解,否则抛出异常。
private boolean hasAdaptiveMethod() {
return Arrays.stream(type.getMethods()).anyMatch(m -> m.isAnnotationPresent(Adaptive.class));
}
2
3
# 生成 package 和 import 信息
按照接口的路径名生成对应的 package 信息,并且生成导入信息,目前只是 import 了ExtensionLoader
这个类。
private static final String CODE_PACKAGE = "package %s;\n";
private String generatePackageInfo() {
return String.format(CODE_PACKAGE, type.getPackage().getName());
}
private static final String CODE_IMPORTS = "import %s;\n";
private String generateImports() {
return String.format(CODE_IMPORTS, ExtensionLoader.class.getName());
}
// import org.apache.dubbo.common.extension.ExtensionLoader;
2
3
4
5
6
7
8
9
# 生成类的声明信息
生成的类名 = 拓展接口名+$Adaptive,实现的接口就是拓展接口的全限定名。比如 public class MySPI$Adaptive implements org.daley.spi.demo.MySPI
private String generateClassDeclaration() {
return String.format(CODE_CLASS_DECLARATION, type.getSimpleName(), type.getCanonicalName());
}
private static final String CODE_CLASS_DECLARATION = "public class %s$Adaptive implements %s {\n";
2
3
4
# 生成方法体
generateMethod
方法是自适应拓展类生成代理类的核心逻辑所在。它主要会分别拿到方法返回类型、方法名、生成方法体、生成方法参数、生成方法异常,然后按照方法的模板的占位符生成代理方法。很明显,重中之重是生成方法体内容。
private String generateMethod(Method method) {
// 分别拿到方法返回类型、方法名、方法体、方法参数、方法异常
String methodReturnType = method.getReturnType().getCanonicalName();
String methodName = method.getName();
// 生成方法体
String methodContent = generateMethodContent(method);
String methodArgs = generateMethodArguments(method);
String methodThrows = generateMethodThrows(method);
// 按照方法模板替换占位符,生成方法内容
return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
}
2
3
4
5
6
7
8
9
10
11
generateMethodContent
主要做了如下几件事:
- 检查是否被@Adaptive 注解,如果没有被注解则生产一段抛出异常的代码。如果被注解,则继续后面逻辑。
- 找到 URL 类型参数的 index,并且生成检查 URL 参数是否为空的逻辑。
- 如果没有 URL 参数,则检查是否方法参数有 public 类型无参 get 方法可以直接拿到 URL。
- 拿到@Adaptive 注解配置的 value,如果没有配置就用接口名默认。
- 检查是否有 Invocation 类型参数。
- 根据不同的情况拿到拓展名。
- 根据扩展名从
getExtension
中拿到真正的扩展类。 - 执行扩展类目标方法,按需返回结果。
这些步骤逻辑都不算复杂,需要格外注意的是第 6 点,这里详细再说明下。
private String generateMethodContent(Method method) {
Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
StringBuilder code = new StringBuilder(512);
// 检查是否被@Adaptive注解
if (adaptiveAnnotation == null) {
// 不是自适应方法生成的代码是一段抛出异常的代码。
return generateUnsupported(method);
} else {
// 找到 URL.class 类型参数位置
int urlTypeIndex = getUrlTypeIndex(method);
// found parameter in URL type
if (urlTypeIndex != -1) {
// Null Point check
// 找到 URL 生成代码逻辑
code.append(generateUrlNullCheck(urlTypeIndex));
} else {
// did not find parameter in URL type
// 未找到 URL 生成代码逻辑
// 再找找是否有方法参数有get方法可以返回URL.class的,并且还是不需要入参的public方法
code.append(generateUrlAssignmentIndirectly(method));
}
// 拿到 Adaptive注解的value
String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
// 有 Invocation 类型参数
boolean hasInvocation = hasInvocationArgument(method);
// 检查参数不为null
code.append(generateInvocationArgumentNullCheck(method));
code.append(generateExtNameAssignment(value, hasInvocation));
// check extName == null?
code.append(generateExtNameNullCheck(value));
// getExtension 根据name拿到扩展类
code.append(generateExtensionAssignment());
// return statement 生成方法调用语句并在必要时返回
code.append(generateReturnAndInvocation(method));
}
return code.toString();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
在generateExtNameAssignment
中会有如下几种不同的情况:是否最后一个参数,是否有 Invocation 类型参数、是否配置了名字为protocol
的注解 value。
将上面的三种条件组合,生成对应不同的代码,核心其实都是如何正确的从 URL 参数中拿到动态扩展名,具体已做注释。
private String generateExtNameAssignment(String[] value, boolean hasInvocation) {
// TODO: refactor it
String getNameCode = null;
// 从最后一个开始遍历
for (int i = value.length - 1; i >= 0; --i) {
if (i == value.length - 1) {
// 如果是最后一个参数,设置了默认拓展名
if (null != defaultExtName) {
// 配置的value不等于"protocol"
if (!"protocol".equals(value[i])) {
if (hasInvocation) {
// 有invocation 根据配置名字从url获取getMethodParameter
getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
} else {
// 没有invocation,getParameter获取参数
getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
}
} else {
// 直接取协议名
getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName);
}
} else {
//没有设置默认拓展名,和上面的区别就是 没有默认值处理的逻辑。上面获取不到可以直接用默认值。
if (!"protocol".equals(value[i])) {
if (hasInvocation) {
getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
} else {
getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
}
} else {
getNameCode = "url.getProtocol()";
}
}
} else {
// 如果不是最后一个参数
if (!"protocol".equals(value[i])) {
if (hasInvocation) {
getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
} else {
getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
}
} else {
getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
}
}
}
return String.format(CODE_EXT_NAME_ASSIGNMENT, getNameCode);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
对于上述条件,可以生成如下几种情况的代码。
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
String extName = url.getMethodParameter(methodName, "loadbalance", "random");
String extName = url.getParameter("client", url.getParameter("transporter", "netty"));
2
3
还有一个疑问点是如果有多个参数怎么办?按照代码逻辑,最终的表现效果就是如果有多个参数,且非 Invocation,会生成多层嵌套代码,并且以最外层也就是最左边的参数为准,右边的参数作为默认值。
举个例子:
@Adaptive(value = {"protocol","param2","myspi.type"})
void say(URL url);
2
生成代码如下:
url.getProtocol() == null ? (url.getParameter("param2", url.getParameter("myspi.type"))) : url.getProtocol()
当上述一切代码执行完成后,就生成了最终的代理类,并且经过编译和加载最终完成实例化,可以被程序所调用,实现动态按需调用。
最终生成的代理类如下:
package org.daley.spi.demo;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class MySPI$Adaptive implements org.daley.spi.demo.MySPI {
public void say(org.apache.dubbo.common.URL arg0) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
String extName = url.getProtocol() == null ?(url.getParameter("param2",url.getParameter("myspi.type"))) : url.getProtocol();
if(extName == null) throw new IllegalStateException("Failed to get extension (org.daley.spi.demo.MySPI) name from url (" + url.toString() + ") use keys([protocol, param2, myspi.type])");
org.daley.spi.demo.MySPI extension = (org.daley.spi.demo.MySPI)ExtensionLoader.getExtensionLoader(org.daley.spi.demo.MySPI.class).getExtension(extName);
extension.say(arg0);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
到这里,Dubbo 自适应扩展的原理就讲解结束了。
- 01
- idea 热部署插件 JRebel 安装及破解,不生效问题解决04-10
- 02
- spark中代码的执行位置(Driver or Executer)12-12
- 03
- 大数据技术之 SparkStreaming12-12