博客中代码地址:https://github.com/farliu/farpc.git
dubbo架构

本章讲解自适应扩展机制,单独将这一块拿出来,是因为这段代码逻辑复杂,处理分支较多。如果不是从上一章看过来的,建议先看看上一章讲的IOC部分。基础不牢地动山摇的情况下无法分析。

自适应扩展机制解决了一个什么问题呢?下面取自dubbo官方的一段话:

有些拓展并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。

这句话意思表达还是很明显,就是在方法被调用的才去选择调用哪个扩展点。但是这时问题来了,“选择”这个动作怎么来做?谁来做?对于这个两个问题,怎么做?是自适应扩展机制的核心。而谁来做?是自适应扩展机制的解决方案。弄清楚这两点,基本差不多了。我们先看一个demo。

自适应扩展机制示例

取自dubbo的单测(dubbo-common模块)。

org.apache.dubbo.common.extension.ext1.SimpleExt

1
2
3
4
# Comment 1
impl1=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl1#Hello World
impl2=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl2 # Comment 2
impl3=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl3 # with head space

ExtensionLoader_Adaptive_Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void test_getAdaptiveExtension_defaultAdaptiveKey() throws Exception {
{
SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension();

Map<String, String> map = new HashMap<String, String>();
URL url = new URL("p1", "1.2.3.4", 1010, "path1", map);

String echo = ext.echo(url, "haha");
assertEquals("Ext1Impl1-echo", echo);
}

{
SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension();

Map<String, String> map = new HashMap<String, String>();
map.put("simple.ext", "impl2");
URL url = new URL("p1", "1.2.3.4", 1010, "path1", map);

String echo = ext.echo(url, "haha");
assertEquals("Ext1Impl2-echo", echo);
}
}

test_getAdaptiveExtension_defaultAdaptiveKey中的两个代码块唯一的区别就是,第二个代码块传入的map中保存了simple.ext->impl2的键值对,就拿到了SimpleExtImpl2的对象。这也正是自适应扩展机制解决的问题。

原理概括

为了更好的理解,先把原理交个底。我总觉得一步步验证比一步步发掘要更能理解一件事物。至于自适应机制的原理,dubbo会给需要自适应的方法生成一个代理类,通过javassist或jdk编译这段代码,得到Class。而代理类里面的逻辑,就是根据传入的Url对象中的变量取得扩展对象并调用。

SimpleExt接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
@SPI("impl1")
public interface SimpleExt {
// @Adaptive example, do not specify a explicit key.
@Adaptive
String echo(URL url, String s);

@Adaptive({"key1", "key2"})
String yell(URL url, String s);

// no @Adaptive
String bang(URL url, int i);
}

dubbo为其生成自适应代理类如下:

1
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
package org.apache.dubbo.common.extension.ext1;

import org.apache.dubbo.common.extension.ExtensionLoader;

public class SimpleExt$Adaptive implements org.apache.dubbo.common.extension.ext1.SimpleExt {
public java.lang.String bang(org.apache.dubbo.common.URL arg0, int arg1) {
// 没有标注@Adaptive,自适应调用直接抛异常
throw new UnsupportedOperationException("The method public abstract java.lang.String org.apache.dubbo.common.extension.ext1.SimpleExt.bang(org.apache.dubbo.common.URL,int) of interface org.apache.dubbo.common.extension.ext1.SimpleExt is not adaptive method!");
}

public java.lang.String yell(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
// 判断URL是否空
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
// key1和key2,是@Adaptive中所获得的值。根据这两个key从URL中获取值,默认值为impl1,从类上的SPI注解中获取
String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));
if (extName == null)
throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([key1, key2])");
// 普通的SPI扩展对象生成
org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt) ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);
// 调用目标方法
return extension.yell(arg0, arg1);
}

public java.lang.String echo(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
// 判断URL是否空
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
// 从URL中取出simple.ext的值,默认值为impl1,从类上的SPI注解中获取
String extName = url.getParameter("simple.ext", "impl1");
if (extName == null)
throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([simple.ext])");
// 普通的SPI扩展对象生成
org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt) ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);
// 调用目标方法
return extension.echo(arg0, arg1);
}
}

这就是dubbo自适应扩展的原理,看到这里是不是觉得自适应扩展也就那么回事。原本看起来如此神奇的功能,原理竟然如此简单。果然早在一百年前,周树人同志就告诫我们:“悲剧,是把美好的东西撕碎了给人看。”

话说回来,这里还有一点值得说。串通整个过程的@Adaptive注解,用起来有讲究。这一点很多博客都直接复制官网内容,一带而过。我想说的是,该注解确实可以标注在类和方法上,标注在方法上,用于自适应扩展机制,也就是本章的重点。而标注在类上,约定这个自适应扩展机制由程序员手动实现,不用dubbo生成扩展类。这里值得注意,dubbo为了这种扩展方式在很多地方都做了兼容。比如:

  1. 加载配置文件时,loadClass()中对标注了@Adaptive的类做缓存
  2. createAdaptiveExtension()中为标注了@Adaptive的类再做了一次注入
  3. getAdaptiveExtensionClass()在执行loadClass()还不存在已缓存的自适应扩展,也就是不存在标注了@Adaptive的类,才会创建。

还有需要注意一点,可以看到,上述所说的原理完全依赖于入参中是否存在URL,那么当入参中不存在URL对象,dubbo会怎么处理呢?直接抛异常?还是有妥善处理方式?

源码验证

我们以getAdaptiveExtension()为入口,该方法中常规的DCL校验缓存,然后调用createAdaptiveExtension()方法。

1
2
3
4
5
6
7
8
9
10
11
12
private T createAdaptiveExtension() {
try {
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
cachedAdaptiveClass = createAdaptiveExtensionClass();
return injectExtension((T) cachedAdaptiveClass.newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
}
}

createAdaptiveExtension()是整个自适应扩展机制的全景。主要包含了三个逻辑:

  1. 调用getExtensionClasses()获取所有扩展实现类
  2. createAdaptiveExtensionClass()自动生成自适应实现类
  3. injectExtension()为标注@Adaptive的类再做了一次注入

加载自适应代理类

getExtensionClasses()在上一章详细讲了,该方法用来加载配置文件中所有的扩展实现类。而需要再次提一下的是,在它调用的loadClass()中,对标注@Adaptive的类进行单独缓存,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
...
// 检测类是否标注Adaptive注解,使用一个变量保存起来
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.getName()
+ ", " + clazz.getName());
}
}
...
}

getExtensionClasses()在读取配置文件后,一个个加载其中的实现类,会检查该类是否标注@Adaptive,如果标注了则会将其保存在cachedAdaptiveClass变量中。这里也就是加载程序员人工编写自适应扩展类,这里有一个要求,一个接口只允许存在一个自适应扩展类。否则,抛异常。

自动生成自适应实现类

经过上述加载后,如果不存在人工编写的自适应扩展类,也还没自己创建自适应扩展类,那么开始由dubbo生成。

1
2
3
4
5
6
7
8
9
10
private Class<?> createAdaptiveExtensionClass() {
// 生成自适应扩展类的代码
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
// 这里就想尽办法获得一个非空的ClassLoader
ClassLoader classLoader = findClassLoader();
// 获取Compiler对象,默认使用javassist
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
// 编译,生成class
return compiler.compile(code, classLoader);
}

createAdaptiveExtensionClass()总得来说就是完成两件事,一是生成自适应扩展类的代码,二是编译,生成class。于第二点,不做详细解释,我们主要查看代码生成规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public String generate() {
// 校验是否存在有@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 org.apache.dubbo.common.extension.ext1;
code.append(generatePackageInfo());
// 生成import,如:import com.alibaba.dubbo.common.extension.ExtensionLoader;
code.append(generateImports());
// 生成类定义,如:public class SimpleExt$Adaptive implements org.apache.dubbo.common.extension.ext1.SimpleExt {
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();
}

generate()中生成包、import、类定义,这三块代码实现都是通过String.format(),逻辑简单,这里一笔带过。generateMethod()逻辑分为生成方法主体、生成方法参数、生成方法定义异常,然后拼接在一起。而生成方法主体的逻辑分支较多,我先捋出一条思路,再一点点看。

  1. 分别处理是否有@Adaptive修饰的方法
  2. 定位URL对象的值
  3. 获取该接口自适应路由的key,用户获取URL中实现类的名字,并非空判断
  4. 通过实现类的名字,调用普通的SPI,生成扩展对象
  5. 调用目标方法
检测@Adaptive修饰

对于没有Adaptive修饰的方法,以SimpleExt.bang()方法为例。dubbo则不会为该方法生成具体逻辑,而是直接抛出异常,生成逻辑如下:

1
2
3
4
5
6
7
8
9
private static final String CODE_UNSUPPORTED = "throw new UnsupportedOperationException(\"The method %s of interface %s is not adaptive method!\");\n";

private String generateMethodContent(Method method) {
Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
if (adaptiveAnnotation == null) {
return String.format(CODE_UNSUPPORTED, method, type.getName());
}
...
}
定位URL对象的值

上文说到,自适应扩展机制,完全依赖URL对象,当不存在URL对象时,无法实现自适应扩展。而不是所有方法都需要URL做为入参的,那么dubbo是怎么处理的呢?

  1. 对于入参中存在URL对象,获取方式就是直接遍历获得。
  2. 对不入参不存在URL对象的方法,dubbo会遍历入参,通过反射调用入参中是否存在以get开头、返回值为URL的方法,并调用。
1
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
private static final String CODE_URL_NULL_CHECK = "if (arg%d == null) throw new IllegalArgumentException(\"url == null\");\n%s url = arg%d;\n";

private String generateMethodContent(Method method) {
Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
StringBuilder code = new StringBuilder(512);
if (adaptiveAnnotation == null) {
...
} else {
// 获取URL对象在入参的位置
int urlTypeIndex = getUrlTypeIndex(method);

if (urlTypeIndex != -1) {
// 存在URL对象,生成代码:判断该对象是否为空并赋值。如:if (arg0 == null) throw new IllegalArgumentException("url == null");org.apache.dubbo.common.URL url = arg0;
code.append(String.format(CODE_URL_NULL_CHECK, urlTypeIndex, URL.class.getName(), index));
} else {
Class<?>[] pts = method.getParameterTypes();
// 遍历所有入参
for (int i = 0; i < pts.length; ++i) {
// 遍历每一个入参中的所有方法
for (Method m : pts[i].getMethods()) {
String name = m.getName();
if ((name.startsWith("get") || name.length() > 3)
// 是否为public方法
&& Modifier.isPublic(m.getModifiers())
// 不是static方法
&& !Modifier.isStatic(m.getModifiers())
// 没有入参
&& m.getParameterTypes().length == 0
// 返回值是URL
&& m.getReturnType() == URL.class) {
// 生成代码:判断该入参是否为空、判断入参调用get方法返回值是否为空并赋值。
return generateGetUrlNullCheck(i, pts[i], name);
}
}
}

// getter method not found, throw
throw new IllegalStateException("Failed to create adaptive class for interface " + type.getName()
+ ": not found url parameter or url attribute in parameters of method " + method.getName());
}
}
...
}
获取URL中实现类的名字

获取实现类的名字,需要先获取URL对象中key。获取这个key,dubbo会先从Adaptive注解中取得,倘若注解中没有设置该值,则根据类名生成一个简单的名字当做key。比如SimpleExt,处理后会生成simple.ext当做key。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private String generateMethodContent(Method method) {
Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
StringBuilder code = new StringBuilder(512);
if (adaptiveAnnotation == null) {
...
} else {
...
String[] value = adaptiveAnnotation.value();
if (value.length == 0) {
// 注解中没有指定key。
String splitName = StringUtils.camelToSplitName(type.getSimpleName(), ".");
value = new String[]{splitName};
}
return value;
...
}
}

接下来,是由上述生成key值,调用generateExtNameAssignment()从URL取得扩展名字。这段代码根据各类情况,分别处理,if判断很长,需要慢慢捋清楚。最终会生成一下代码

1
2
3
4
5
6
7
String extName = (url.getProtocol() == null ? "impl1" : url.getProtocol());

或者:
String extName = url.getMethodParameter(methodName, "loadbalance", "random");

或者:
String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));
生成扩展对象、调用目标方法
1
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
private static final String CODE_EXTENSION_ASSIGNMENT = "%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);\n";

private String generateMethodContent(Method method) {
...
else {
// 生成调用SPI生成扩展对象的代码
code.append(String.format(CODE_EXTENSION_ASSIGNMENT, type.getName(), ExtensionLoader.class.getSimpleName(), type.getName()));

// 调用目标方法,并返回值
code.append(generateReturnAndInvocation(method));
}

return code.toString();
}

private String generateReturnAndInvocation(Method method) {
// 判断是否void返回类型的方法
String returnStatement = method.getReturnType().equals(void.class) ? "" : "return ";

// 拼接入参
String args = IntStream.range(0, method.getParameters().length)
.mapToObj(i -> String.format(CODE_EXTENSION_METHOD_INVOKE_ARGUMENT, i))
.collect(Collectors.joining(", "));

// 调用目标方法
return returnStatement + String.format("extension.%s(%s);\n", method.getName(), args);
}

到这里,生成代理类代码的逻辑都在上述过程中,后续就是dubbo调用Compiler生成class,然后使用了。SPI到本文,源码讲解就结束了,后面就是我们自己手动实现SPI了。