手写dubbo框架8-SPI 自适应扩展机制

2020-10-21 10:23:00 浏览数 (1)

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

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

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

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

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

自适应扩展机制示例

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

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

代码语言:javascript复制
# 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

ExtensionLoaderAdaptiveTest

代码语言:javascript复制
@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);
    }
}

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

原理概括

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

SimpleExt接口定义如下:

代码语言:javascript复制
@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为其生成自适应代理类如下:

代码语言:javascript复制
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()方法。

代码语言:javascript复制
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的类进行单独缓存,如下:

代码语言:javascript复制
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生成。

代码语言:javascript复制
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。于第二点,不做详细解释,我们主要查看代码生成规则。

代码语言:javascript复制
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则不会为该方法生成具体逻辑,而是直接抛出异常,生成逻辑如下:

代码语言:javascript复制
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的方法,并调用。
代码语言:javascript复制
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。

代码语言:javascript复制
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判断很长,需要慢慢捋清楚。最终会生成一下代码

代码语言:javascript复制
String extName = (url.getProtocol() == null ? "impl1" : url.getProtocol());
或者:
String extName = url.getMethodParameter(methodName, "loadbalance", "random");
或者:
String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));
生成扩展对象、调用目标方法
代码语言:javascript复制
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了。

0 人点赞