Dubbo自适应拓展学习笔记

关于阅读开源代码的一些想法

在编写本文前,其实已经写了一篇关于Dubbo 自适应拓展特性的草稿,但是通篇写下来,所做的也不过是copy-write的过程,这样的文章对于我来说,又有什么意义呢?
尽管我多次想编写一篇详尽有效的文档,试图如同大神一样去理清Dubbo源码的脉络,但是深入其间才发现力有所不逮。因此便重新把那篇文章给删了,推倒重来,将阅读人群从别人转向自己。
这样一来,就变得清晰简单的许多。我只需要问自己以下几个问题,便能得到我想要的东西:

  • 你最想了解的时Dubbo的哪个特性?
  • 基于这个特性如果让你来实现,你会怎么实现?遇到哪些问题?
  • 你无法解决的问题别人的解决思路是什么?
  • 里面最有价值的算法或者思路是什么?

只要能解决这些问题,那么对于我来说,就是大有收获,至于别人的收获,又与我何干?

我最想了解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的实现原理,我们可以闭着眼睛,想假如是我们自己去实现,我们会怎么实现,以及遇到哪些问题?

  • 1.采用什么方式去标记需要动态拓展的方法?
  • 2.由于框架的开发者是不应该去关注业务以及具体业务的实现,但是又一定程度的需要将调用拦截下来,做一定的路由引导,这时候就应该要有一层简单的代理。但是由于服务的多样性以及灵活性,只能采用动态代理的方式去实现包装。那么会采用什么技术去实现动态代理?
  • 假设我们已经选定了动态代理了,那么我们需要构建代理方法的相关代码,如何去构建一个完整的代理方法,分几部分,其中需要注意些什么?

当然我们并没有真正自己亲手去实现这个功能,我们不过是围绕这个特性或者这个问题,即如何动态地通过参数去适配到对应的实现方法去?(其实最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。
我并不打算一点一点断点跟进去这个源码,然后一点一点的复制粘贴,上一篇的草稿就是这么写,但是写下来发现,值得我回味的源码并不是太多,况且官方文档的说明已经是十分详尽了,重复一次,意义不大。

我只想分享我在里面觉得有用的东西。
整个自适应拓展的实现其实就三步:

  • 生成代理功能代码
  • 通过javassit或者jdk编译代码,得到Class类
  • 反射创建代理类

1.如何生成代理方法

生成代理方法功能,很冗长的东西,但是其实就是根据参数自行构建代理代码的字符,本质上就是个苦力活,无非就是将构建过程拆解成几步:

  • 创建package信息
  • 创建import信息
  • 创建类声明信息
  • 构建自适应代理方法
/**
     * 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);
    }

2.如何采用javassist或者jdk编译代码

看这一块的源码,依然秉持着带着问题去理解源码,同时我们要时刻把握住我们究竟不懂什么,要去学习什么。
在这里能学到的东西无非两样:

  • 什么是javassist?
  • 怎么去用它或者他们?
  • 使用它的其他场景是什么?

我们心里揣着这些问题,姑且往下继续看看,咱们还是拿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类
里面具体的步骤如下:

  • 正则匹配拿到当前的包名
  • 正则匹配拿到类名
  • 通过报名和类名拼装成全限定类名
  • 用Class.forName的方式尝试获取对应的类
  • 如果没有,则采用javassist和jdk去编译代码

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官网

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语言操作字节码的类库。它可以:

  • 在运行时定义以及加载一个新的类
  • 在JVM加载时修改class文件

相比较其他的字节码操作类库,它提供了两种方式的API:

  • 源码级别
  • 字节码级别

具体的Javassist API操作可以参考Javassist帮助手册中文版

你可能感兴趣的:(Dubbo,JVM)