在编写本文前,其实已经写了一篇关于Dubbo 自适应拓展特性的草稿,但是通篇写下来,所做的也不过是copy-write的过程,这样的文章对于我来说,又有什么意义呢?
尽管我多次想编写一篇详尽有效的文档,试图如同大神一样去理清Dubbo源码的脉络,但是深入其间才发现力有所不逮。因此便重新把那篇文章给删了,推倒重来,将阅读人群从别人转向自己。
这样一来,就变得清晰简单的许多。我只需要问自己以下几个问题,便能得到我想要的东西:
只要能解决这些问题,那么对于我来说,就是大有收获,至于别人的收获,又与我何干?
Dubbo的特性很多,如果一股脑的写成一篇文章,那么势必陷入僵局。围绕Dubbo一系列的特性,本文只针对Dubbo的自适应扩展与SPI部分进行解析。
那么这里顺带一提Dubbo的自适应扩展是什么?
一般情况下,Dubbo都通过SPI进行拓展,但对于有些拓展,希望在拓展方法被调用时,才灵活地根据运行时参数进行加载对应的拓展。这时候我们就称之为自适应拓展,其实就是动态拓展。
这里直接拿官网的例子来进行理解:
public interface WheelMaker {
Wheel makeWheel(URL url);
}
//WheelMaker 接口的自适应实现类如下:
public class AdaptiveWheelMaker implements WheelMaker {
public Wheel makeWheel(URL url) {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
// 1.从 URL 中获取 WheelMaker 名称
String wheelMakerName = url.getParameter("Wheel.maker");
if (wheelMakerName == null) {
throw new IllegalArgumentException("wheelMakerName == null");
}
// 2.通过 SPI 加载具体的 WheelMaker
WheelMaker wheelMaker = ExtensionLoader
.getExtensionLoader(WheelMaker.class).getExtension(wheelMakerName);
// 3.调用目标方法
return wheelMaker.makeWheel(URL url);
}
}
从上面可以看出,实际的服务实现是通过URL远程传过来的参数来动态获取的,这也就是自适应拓展。区别于直接采用SPI进行拓展加载,采用这样的方式会更加灵活。
那么解决完我想要学习什么特性的问题,第二个问题,我们姑且先不要急着去看Dubbo的实现原理,我们可以闭着眼睛,想假如是我们自己去实现,我们会怎么实现,以及遇到哪些问题?
当然我们并没有真正自己亲手去实现这个功能,我们不过是围绕这个特性或者这个问题,即如何动态地通过参数去适配到对应的实现方法去?(其实最low的方法就是if else),基于这个问题,我们展开了一系列的探讨。
ok,我们已经粗略地想了下这个实现过程了,乍一看,就两个点我们需要关注的:
(1)是否真的如我们所想采用了动态代理的方式?如果是,采用了哪些动态代理的技术?引申开来,当前有哪些具体的动态代理技术?他们的优缺点是什么?
(2)构建一个完整的代理方法的细节有哪些?
带着以下问题,我们开始去解剖代码。
当然我们不可能一下子扎入到源码当中,源码是汪洋大海啊,我们得找一个路标,那就是Dubbo的官网源码导读了,它是怎么说明这个问题的解决方案的呢?
自适应拓展机制的实现逻辑比较复杂,首先 Dubbo 会为拓展接口生成具有代理功能的代码。然后通过 javassist 或 jdk 编译这段代码,得到 Class 类。最后再通过反射创建代理类
有了这个路标,我们就可以比较快捷地把握住源码的方向了。Dubbo本身提供了一系列丰富的Demo,我们可以通过已经有的Demo进行源码解读。
Demo见dubbo-common ExtensionLoader_Adaptive_Test测试类
@Test
public void test_getAdaptiveExtension_defaultAdaptiveKey() throws Exception {
{
SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension();
Map map = new HashMap();
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 map = new HashMap();
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);
}
}
SimpleExt类
/**
* Simple extension, has no wrapper
*/
@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);
}
SimpleExtImpl1
public class SimpleExtImpl1 implements SimpleExt {
public String echo(URL url, String s) {
return "Ext1Impl1-echo";
}
public String yell(URL url, String s) {
return "Ext1Impl1-yell";
}
public String bang(URL url, int i) {
return "bang1";
}
}
SimpleExtImp2
public class SimpleExtImpl2 implements SimpleExt {
public String echo(URL url, String s) {
return "Ext1Impl2-echo";
}
public String yell(URL url, String s) {
return "Ext1Impl2-yell";
}
public String bang(URL url, int i) {
return "bang2";
}
}
以上是Dubbo简单的一个Demo。
我并不打算一点一点断点跟进去这个源码,然后一点一点的复制粘贴,上一篇的草稿就是这么写,但是写下来发现,值得我回味的源码并不是太多,况且官方文档的说明已经是十分详尽了,重复一次,意义不大。
我只想分享我在里面觉得有用的东西。
整个自适应拓展的实现其实就三步:
生成代理方法功能,很冗长的东西,但是其实就是根据参数自行构建代理代码的字符,本质上就是个苦力活,无非就是将构建过程拆解成几步:
/**
* generate and return class code
*/
public String generate() {
// no need to generate adaptive class since there's no adaptive method found.
if (!hasAdaptiveMethod()) {
throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
}
StringBuilder code = new StringBuilder();
code.append(generatePackageInfo());
code.append(generateImports());
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 method invocation statement and return it if necessary
*/
private String generateReturnAndInvocation(Method method) {
String returnStatement = method.getReturnType().equals(void.class) ? "" : "return ";
String args = Arrays.stream(method.getParameters()).map(Parameter::getName).collect(Collectors.joining(", "));
return returnStatement + String.format("extension.%s(%s);\n", method.getName(), args);
}
依然是那句话,构建代理方法,并不算是什么多么高大上的事情,就是一个苦力活,但是大神在写这些代码的时候总是尽量简洁,这一点的确是值得学习的。特别是最新版的dubbo源码,能一句话解决的,大神绝对不啰嗦。
很多地方都直接用类似一句代码形式就完成一个功能
/**
* generate package info
*/
private String generatePackageInfo() {
return String.format(CODE_PACKAGE, type.getPackage().getName());
}
构建完整个代码的模板大概就是这样:
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 yell(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
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])");
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) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
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])");
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);
}
public java.lang.String bang(org.apache.dubbo.common.URL arg0, int arg1) {
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!");
}
}
当整个构建完代理代码后,就可以采用javassist或者jdk去编译了
private Class> createAdaptiveExtensionClass() {
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
ClassLoader classLoader = findClassLoader();
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}
看这一块的源码,依然秉持着带着问题去理解源码,同时我们要时刻把握住我们究竟不懂什么,要去学习什么。
在这里能学到的东西无非两样:
我们心里揣着这些问题,姑且往下继续看看,咱们还是拿dubbo的demo代码来看,简单快捷,经济实惠。
JavassistCompileTest
@Test
public void testCompileJavaClass() throws Exception {
JavassistCompiler compiler = new JavassistCompiler();
Class> clazz = compiler.compile(getSimpleCode(), JavassistCompiler.class.getClassLoader());
// Because javassist compiles using the caller class loader, we should't use HelloService directly
Object instance = clazz.newInstance();
Method sayHello = instance.getClass().getMethod("sayHello");
Assertions.assertEquals("Hello world!", sayHello.invoke(instance));
}
无论是javassistCompile和jdkCompile继承抽象类AbstractCompile类
里面具体的步骤如下:
package org.apache.dubbo.common.compiler.support;
import org.apache.dubbo.common.compiler.Compiler;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Abstract compiler. (SPI, Prototype, ThreadSafe)
*/
public abstract class AbstractCompiler implements Compiler {
private static final Pattern PACKAGE_PATTERN = Pattern.compile("package\\s+([$_a-zA-Z][$_a-zA-Z0-9\\.]*);");
private static final Pattern CLASS_PATTERN = Pattern.compile("class\\s+([$_a-zA-Z][$_a-zA-Z0-9]*)\\s+");
@Override
public Class> compile(String code, ClassLoader classLoader) {
code = code.trim();
Matcher matcher = PACKAGE_PATTERN.matcher(code);
String pkg;
if (matcher.find()) {
pkg = matcher.group(1);
} else {
pkg = "";
}
matcher = CLASS_PATTERN.matcher(code);
String cls;
if (matcher.find()) {
cls = matcher.group(1);
} else {
throw new IllegalArgumentException("No such class name in " + code);
}
String className = pkg != null && pkg.length() > 0 ? pkg + "." + cls : cls;
try {
return Class.forName(className, true, org.apache.dubbo.common.utils.ClassUtils.getCallerClassLoader(getClass()));
} catch (ClassNotFoundException e) {
if (!code.endsWith("}")) {
throw new IllegalStateException("The java code not endsWith \"}\", code: \n" + code + "\n");
}
try {
return doCompile(className, code);
} catch (RuntimeException t) {
throw t;
} catch (Throwable t) {
throw new IllegalStateException("Failed to compile class, cause: " + t.getMessage() + ", class: " + className + ", code: \n" + code + "\n, stack: " + ClassUtils.toString(t));
}
}
}
protected abstract Class> doCompile(String name, String source) throws Throwable;
}
javassist和jdkcompile具体实现doCompile的方法。
JavassistCompile.java
private static final Pattern IMPORT_PATTERN = Pattern.compile("import\\s+([\\w\\.\\*]+);\n");
private static final Pattern EXTENDS_PATTERN = Pattern.compile("\\s+extends\\s+([\\w\\.]+)[^\\{]*\\{\n");
private static final Pattern IMPLEMENTS_PATTERN = Pattern.compile("\\s+implements\\s+([\\w\\.]+)\\s*\\{\n");
private static final Pattern METHODS_PATTERN = Pattern.compile("\n(private|public|protected)\\s+");
private static final Pattern FIELD_PATTERN = Pattern.compile("[^\n]+=[^\n]+;");
@Override
public Class> doCompile(String name, String source) throws Throwable {
CtClassBuilder builder = new CtClassBuilder();
builder.setClassName(name);
// process imported classes
Matcher matcher = IMPORT_PATTERN.matcher(source);
while (matcher.find()) {
builder.addImports(matcher.group(1).trim());
}
// process extended super class
matcher = EXTENDS_PATTERN.matcher(source);
if (matcher.find()) {
builder.setSuperClassName(matcher.group(1).trim());
}
// process implemented interfaces
matcher = IMPLEMENTS_PATTERN.matcher(source);
if (matcher.find()) {
String[] ifaces = matcher.group(1).trim().split("\\,");
Arrays.stream(ifaces).forEach(i -> builder.addInterface(i.trim()));
}
// process constructors, fields, methods
String body = source.substring(source.indexOf('{') + 1, source.length() - 1);
String[] methods = METHODS_PATTERN.split(body);
String className = ClassUtils.getSimpleClassName(name);
Arrays.stream(methods).map(String::trim).filter(m -> !m.isEmpty()).forEach(method -> {
if (method.startsWith(className)) {
builder.addConstructor("public " + method);
} else if (FIELD_PATTERN.matcher(method).matches()) {
builder.addField("private " + method);
} else {
builder.addMethod("public " + method);
}
});
// compile
ClassLoader classLoader = org.apache.dubbo.common.utils.ClassUtils.getCallerClassLoader(getClass());
CtClass cls = builder.build(classLoader);
return cls.toClass(classLoader, JavassistCompiler.class.getProtectionDomain());
}
JdkCompile.java
@Override
public Class> doCompile(String name, String sourceCode) throws Throwable {
int i = name.lastIndexOf('.');
String packageName = i < 0 ? "" : name.substring(0, i);
String className = i < 0 ? name : name.substring(i + 1);
JavaFileObjectImpl javaFileObject = new JavaFileObjectImpl(className, sourceCode);
javaFileManager.putFileForInput(StandardLocation.SOURCE_PATH, packageName,
className + ClassUtils.JAVA_EXTENSION, javaFileObject);
Boolean result = compiler.getTask(null, javaFileManager, diagnosticCollector, options,
null, Arrays.asList(javaFileObject)).call();
if (result == null || !result) {
throw new IllegalStateException("Compilation failed. class: " + name + ", diagnostics: " + diagnosticCollector);
}
return classLoader.loadClass(name);
}
以上则是javassist在dubbo自适应拓展的简单应用。
javassist是什么?javassist的官网有给出定义:javassist官网
Javassist (Java Programming Assistant) makes Java bytecode manipulation simple. It is a class library for editing bytecodes in Java; it enables Java programs to define a new class at runtime and to modify a class file when the JVM loads it. Unlike other similar bytecode editors, Javassist provides two levels of API: source level and bytecode level.
简单地说,javassist就是一款用java语言操作字节码的类库。它可以:
相比较其他的字节码操作类库,它提供了两种方式的API:
具体的Javassist API操作可以参考Javassist帮助手册中文版