Dubbo源码篇---SPI自适应扩展

607 阅读11分钟

Dubbo SPI自适应扩展机制是是Dubbo SPI扩展到补充,强化了Dubbo SPI扩展,实现了在扩展方法被调用时,根据运行时参数进行动态加载。该机制的核心是为扩展接口动态的生成具有代理功能的代码,然后通过javassist或jdk编译这段代码,得到class类,最后通过反射得到具体的代理类。

示例

package org.apache.dubbo.demo.provider.javaspi;
​
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;
​
@SPI("ali")
public interface Pay {
​
    @Adaptive({"payType"})
    void pay(URL url);
​
}
package org.apache.dubbo.demo.provider.javaspi;
​
import org.apache.dubbo.common.URL;
​
public class AliPay implements Pay{
​
    @Override
    public void pay(URL url) {
        System.out.println("支付宝支付");
    }
}
package org.apache.dubbo.demo.provider.javaspi;
​
import org.apache.dubbo.common.URL;
​
public class WechatPay implements Pay{
    @Override
    public void pay(URL url) {
        System.out.println("微信支付");
    }
}
# 此处为Dubbo SPI的配置文件,文件路径为:META-INF/dubbo/org.apache.dubbo.demo.provider.javaspi.Pay
ali=org.apache.dubbo.demo.provider.javaspi.AliPay
wechat=org.apache.dubbo.demo.provider.javaspi.WechatPay
wrapper=org.apache.dubbo.demo.provider.javaspi.PayWrapper1
package org.apache.dubbo.demo.provider.javaspi;
​
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.junit.jupiter.api.Test;
​
import java.util.ServiceLoader;
​
public class JavaSpiTest {
    @Test
    public void test() {
        ExtensionLoader<Pay> extensionLoader = ExtensionLoader.getExtensionLoader(Pay.class);
        // 获取自适应扩展
        Pay adaptiveExtension = extensionLoader.getAdaptiveExtension();
        // 使用支付宝支付
        adaptiveExtension.pay(URL.valueOf("dubbo://192.168.0.101:20880/xxxService"));                    
        // 使用微信支付
        adaptiveExtension.pay(URL.valueOf("dubbo://192.168.0.101:20880/xxxService?payType=wechat"));    
    }
}
​

以上为Dubbo SPI的自适应扩展机制的Demo,通过Pay adaptiveExtension = extensionLoader.getAdaptiveExtension();可以发现,adaptiveExtension实例是个代理类,在调用pay方法时,根据URL参数来决定调用具体的扩展类。

源码分析

在对自适应拓展生成过程进行深入分析之前,我们先来看一下与自适应拓展息息相关的一个注解,即 Adaptive 注解。该注解的定义如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
    String[] value() default {};
}

从上面的代码中可知,Adaptive 可注解在类或方法上。当 Adaptive 注解在类上时,Dubbo 不会为该类生成代理类。注解在方法(接口方法)上时,Dubbo 则会为该方法生成代理逻辑。Adaptive 注解在类上的情况很少,在 Dubbo 中,仅有两个类被 Adaptive 注解了,分别是 AdaptiveCompiler 和 AdaptiveExtensionFactory。此种情况,表示拓展的加载逻辑由人工编码完成。更多时候,Adaptive 是注解在接口方法上的,表示拓展的加载逻辑需由框架自动生成。Adaptive 注解的地方不同,相应的处理逻辑也是不同的。注解在类上时,处理逻辑比较简单,本文就不分析了。注解在接口方法上时,处理逻辑较为复杂,本章将会重点分析此块逻辑。

获取自适应扩展

/**
  * 加载自适应扩展,这里有双重缓存获取及双重检查,避免重复执行创建ExtensionFactory
  */
@SuppressWarnings("unchecked")
public T getAdaptiveExtension() {
  // 从缓存中获取自适应扩展实例
  Object instance = cachedAdaptiveInstance.get();
  if (instance == null) {
    if (createAdaptiveInstanceError != null) {
      throw new IllegalStateException("Failed to create adaptive instance: " +
                                      createAdaptiveInstanceError.toString(),
                                      createAdaptiveInstanceError);
    }
    // 加锁,避免重复创建自适应扩展
    synchronized (cachedAdaptiveInstance) {
      // 从缓存中获取ExtensionFactory
      instance = cachedAdaptiveInstance.get();
      if (instance == null) {
        try {
          // 创建自适应扩展
          instance = createAdaptiveExtension();
          // 设置缓存
          cachedAdaptiveInstance.set(instance);
        } catch (Throwable t) {
          createAdaptiveInstanceError = t;
          throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
        }
      }
    }
  }
​
  return (T) instance;
}

这里的方法很简单,就是先从缓存中获取自适应扩展实例,不存在的则通过createAdaptiveExtension()方法创建自适应扩展实例,并添加到缓存。

 /**
     * 创建自适应扩展
     */
    @SuppressWarnings("unchecked")
    private T createAdaptiveExtension() {
        try {
            // 获取自适应扩展且实例化后进行IOC注入
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
        }
    }

createAdaptiveExtension方法的代码非常少,但是包含了三个逻辑在里面:

1、获取自适应扩展类

2、实例化自适应扩展类

3、向自适应扩展类实例中注入依赖

第二步的逻辑最简单,就是将第一步获取到的扩展类实例化,第三步我们在Dubbo SPI中已做过分析,此处不再探讨。接下来我们重点分析下第一步

 /**
     * 获取自适应扩展类
     */
    private Class<?> getAdaptiveExtensionClass() {
        // 获取扩展类
        getExtensionClasses();
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        // 创建自适应扩展类
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

getAdaptiveExtensionClass方法的代码也很少,先是获取扩展类,再从缓存中获取自适应扩展类,如果缓存不存在,则创建自适应扩展类。其中getExtensionClasses()在Dubbo SPI中已经做过详细的分析,此处不再重复,接下来继续看createAdaptiveExtensionClass()方法。

 /**
     * 动态生成具有代理功能的自适应扩展类并编译
     */
    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);
    }

从上面的代码来看只是执行了四行代码,首先是通过new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate()完成自适应扩展类代码的动态生成,而后获取类加载器,然后获取Compile实例的编译器(Dubbo模式使用javassist作为编译器),最后通过编译器编译动态生成的自适应扩展类的代码并返回,接下来把重点放在动态代码生成的逻辑上。

自适应扩展类代码生成

我们顺着String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate()这行代码来分析,第一步是通过AdaptiveClassCodeGenerator的构造方法来指定扩展类以及扩展类的默认实现的名称,其中扩展类的默认实现的名称是通过作用在扩展类上的@SPI注解来实现的,在Dubbo SPI中已经分析过;第二步是代码生成的逻辑。

/**
  * 构造函数
  * @param type 扩展类
  * @param defaultExtName 默认实现的扩展类,在@SPI中声明
  */
public AdaptiveClassCodeGenerator(Class<?> type, String defaultExtName) {
  this.type = type;
  this.defaultExtName = defaultExtName;
}
​
/**
  * 生成代码
  */
public String generate() {
  // 不需要生成自适应类,因为没有找到自适应方法。
  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()方法的代码太多,为了方便阅读,我们将对该方法进行拆分

生成包信息
private String generatePackageInfo() {
    return String.format(CODE_PACKAGE, type.getPackage().getName());
}

包信息生成的逻辑非常简单,其中CODE_PACKAGE为指定的目标,通过String的format方法进行响应的替换。

生成包导入信息
private String generateImports() {
    return String.format(CODE_IMPORTS, ExtensionLoader.class.getName());
}

包导入信息生成的逻辑同样非常简单,与包信息生成一样。

生成类声明
private String generateClassDeclaration() {
    return String.format(CODE_CLASS_DECLARATION, type.getSimpleName(), type.getCanonicalName());
}

类声明生成的逻辑同样非常简单,与包信息生成一样。

生成方法
 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);
    }

此处显示分别获取到了该方法的返回值类型及方法名,然后依次生成方法内容、方法参数、异常抛出,最后通过String的format格式化生成的代码。其中方法参数、异常抛出的代码生成逻辑比较简单,这里不做分析,接下来主要分析方法内容的生成逻辑

private String generateMethodContent(Method method) {
        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
        StringBuilder code = new StringBuilder(512);
        // Adaptive注解检测,无此注解的方法在调用时将会抛出异常
        if (adaptiveAnnotation == null) {
            // 无Adaptive注解的方法,在方法内部直接抛出异常
            return generateUnsupported(method);
        } else {
            // 获取类型为URL的参数索引,如void pay(URL url)返回的为0,void pay(String name, URL url)返回的为1
            int urlTypeIndex = getUrlTypeIndex(method);
​
            // urlTypeIndex != -1,表示参数列表中存在 URL 参数
            if (urlTypeIndex != -1) {
                // 为 URL 类型参数生成判空代码,格式如下:
                // if (arg + urlTypeIndex == null) {throw new IllegalArgumentException("url == null");}
                code.append(generateUrlNullCheck(urlTypeIndex));
            } else {
                // 参数列表中不存在 URL 类型参数的校验
                code.append(generateUrlAssignmentIndirectly(method));
            }
​
            // 解析方法上@Adaptive注解的值
            String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
​
            // 查询该方法上是否有参数为org.apache.dubbo.rpc.Invocation
            boolean hasInvocation = hasInvocationArgument(method);
            
            code.append(generateInvocationArgumentNullCheck(method));
​
            // 解析URL参数,生成ExtensionName代码相关的逻辑
            code.append(generateExtNameAssignment(value, hasInvocation));
            // 校验ExtensionName是否为null
            code.append(generateExtNameNullCheck(value));
            // 生成获取Extension语句
            code.append(generateExtensionAssignment());
​
            // 生成返回语句
            code.append(generateReturnAndInvocation(method));
        }
​
        return code.toString();
    }

generateMethodContent方法分类了两部分,当该方法无@Adaptive注解时,逻辑非常简单,抛出异常;有该注解时则需要判断该方法是否包含URL参数,也分为了两种情况,当有URL参数时,逻辑也很简单,需要判断该URL参数是否为null,为null则抛出异常,第二种情况相对复杂,在该方法没有URL参数时,继续深入generateUrlAssignmentIndirectly方法

private String generateUrlAssignmentIndirectly(Method method) {
        Class<?>[] pts = method.getParameterTypes();
​
        Map<String, Integer> getterReturnUrl = new HashMap<>();
        // 遍历方法的参数类型列表
        for (int i = 0; i < pts.length; ++i) {
            // 遍历方法列表,寻找可返回 URL 的 getter 方法
            for (Method m : pts[i].getMethods()) {
                String name = m.getName();
                // 1. 方法名以 get 开头,或方法名大于3个字符
                // 2. 方法的访问权限为 public
                // 3. 非静态方法
                // 4. 方法参数数量为0
                // 5. 方法返回值类型为 URL
                if ((name.startsWith("get") || name.length() > 3)
                        && Modifier.isPublic(m.getModifiers())
                        && !Modifier.isStatic(m.getModifiers())
                        && m.getParameterTypes().length == 0
                        && m.getReturnType() == URL.class) {
                    getterReturnUrl.put(name, i);
                }
            }
        }
​
        if (getterReturnUrl.size() <= 0) {
            // 如果所有参数中均不包含可返回 URL 的 getter 方法,则抛出异常
            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());
        }
        
        // 默认获取方法名为getUrl
        Integer index = getterReturnUrl.get("getUrl");
        // 如果方法名为getUrl的方法存在,则通过索引获取
        if (index != null) {
            return generateGetUrlNullCheck(index, pts[index], "getUrl");
        } else {
            // 如果不存在方法名为getUrl的方法,则获取满足上述五个条件的首个getter方法
            Map.Entry<String, Integer> entry = getterReturnUrl.entrySet().iterator().next();
            // 生成对getter方法进行判断处理的代码
            return generateGetUrlNullCheck(entry.getValue(), pts[entry.getValue()], entry.getKey());
        }
    }
​
​
private String generateGetUrlNullCheck(int index, Class<?> type, String method) {
        // Null point check
        StringBuilder code = new StringBuilder();
​
        // 为 getter 方法返回的 URL 生成判空代码,格式如下:
        // if (argN.getter方法名() == null)
        //     throw new IllegalArgumentException(参数全限定名 + argument getUrl() == null);
        code.append(String.format("if (arg%d == null) throw new IllegalArgumentException("%s argument == null");\n",
                index, type.getName()));
        code.append(String.format("if (arg%d.%s() == null) throw new IllegalArgumentException("%s argument %s() == null");\n",
                index, method, type.getName(), method));
​
        // 生成赋值语句,格式如下:
        // URL全限定名 url = argN.getter方法名(),比如
        // com.alibaba.dubbo.common.URL url = invoker.getUrl();
        code.append(String.format("%s url = arg%d.%s();\n", URL.class.getName(), index, method));
        return code.toString();
    }

在generateUrlAssignmentIndirectly方法中有双重循环,先是遍历扩展方法的参数类型,再遍历各个参数类型的方法,找出满足方法访问权限为public、为非静态方法、方法参数为0,返回值类型为URL的getter方法,如果该方法不存在,则抛异常,否则根据不同情况生成上述存在的getter方法的判断代码。到这里,我们回到generateMethodContent方法,接着下面的流程来分析generateInvocationArgumentNullCheck方法

 private String generateInvocationArgumentNullCheck(Method method) {
        Class<?>[] pts = method.getParameterTypes();
        return IntStream.range(0, pts.length).filter(i -> CLASSNAME_INVOCATION.equals(pts[i].getName()))
                        .mapToObj(i -> String.format(CODE_INVOCATION_ARGUMENT_NULL_CHECK, i, i))
                        .findFirst().orElse("");
    }

generateInvocationArgumentNullCheck方法逻辑相对简单,如果存在Invocation,则为其生成判空方法,否则生成空字符串。到这里,我们回到generateMethodContent方法,接着下面的流程来分析generateExtNameAssignment方法

private String generateExtNameAssignment(String[] value, boolean hasInvocation) {
        // TODO: refactor it
        String getNameCode = null;
        // 遍历 value,这里的 value 是 Adaptive 的注解值
        // 此处循环目的是生成从URL中获取拓展名的代码,生成的代码会赋值给 getNameCode 变量。注意这
        // 个循环的遍历顺序是由后向前遍历的。
        for (int i = value.length - 1; i >= 0; --i) {
            // 当 i 为最后一个元素的坐标时
            if (i == value.length - 1) {
                // 默认拓展名非空
                if (null != defaultExtName) {
                    // protocol 是 url 的一部分,可通过 getProtocol 方法获取,其他的则是从
                    // URL 参数中获取。因为获取方式不同,所以这里要判断 value[i] 是否为 protocol
                    if (!"protocol".equals(value[i])) {
                        // hasInvocation 用于标识方法参数列表中是否有 Invocation 类型参数
                        if (hasInvocation) {
                            // 生成的代码功能等价于下面的代码:
                            // url.getMethodParameter(methodName, value[i], defaultExtName)
                            // 以 LoadBalance 接口的 select 方法为例,最终生成的代码如下:
                            //   url.getMethodParameter(methodName, "loadbalance", "random")
                            getNameCode = String.format("url.getMethodParameter(methodName, "%s", "%s")", value[i], defaultExtName);
                        } else {
                            // 生成的代码功能等价于下面的代码:
                            // url.getParameter(value[i], defaultExtName)
                            getNameCode = String.format("url.getParameter("%s", "%s")", value[i], defaultExtName);
                        }
                    } else {
                        // 生成的代码功能等价于下面的代码:
                        // ( url.getProtocol() == null ? defaultExtName : url.getProtocol() )
                        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 {
                            // 生成的代码功能等价于下面的代码:
                            // url.getParameter(value[i])
                            getNameCode = String.format("url.getParameter("%s")", value[i]);
                        }
                    } else {
                        // 生成从 url 中获取协议的代码,比如 "dubbo"
                        getNameCode = "url.getProtocol()";
                    }
                }
            } else {
                if (!"protocol".equals(value[i])) {
                    if (hasInvocation) {
                        // 生成代码格式同上
                        getNameCode = String.format("url.getMethodParameter(methodName, "%s", "%s")", value[i], defaultExtName);
                    } else {
                        // 生成的代码功能等价于下面的代码:
                        // url.getParameter(value[i], getNameCode)
                        // 以 Transporter 接口的 connect 方法为例,最终生成的代码如下:
                        // url.getParameter("client", url.getParameter("transporter", "netty"))
                        getNameCode = String.format("url.getParameter("%s", %s)", value[i], getNameCode);
                    }
                } else {
                    // 生成的代码功能等价于下面的代码:
                    // url.getProtocol() == null ? getNameCode : url.getProtocol()
                    // 以 Protocol 接口的 connect 方法为例,最终生成的代码如下:
                    // url.getProtocol() == null ? "dubbo" : url.getProtocol()
                    getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
                }
            }
        }
        // 格式化
        return String.format(CODE_EXT_NAME_ASSIGNMENT, getNameCode);
    }

generateExtNameAssignment方法用于根据 SPI 和 Adaptive 注解值生成“获取拓展名逻辑”,同时生成逻辑也受 Invocation 类型参数影响,综合因素导致本段逻辑相对复杂。本段逻辑可能会生成但不限于下面的代码:

// 第一种
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol()); 
// 第二种
String extName = url.getMethodParameter(methodName, "loadbalance", "random");
// 第三种
String extName = url.getParameter("client", url.getParameter("transporter", "netty"));

该方法的逻辑判断条件太多,阅读起来比较难受,可以通过多次调试去看看。到这里,我们回到generateMethodContent方法,接着下面的流程来分析generateReturnAndInvocation方法

private String generateReturnAndInvocation(Method method) {
        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);
    }

generateReturnAndInvocation方法为生成返回语句,逻辑不复杂,不做太多分析。

生成代码简单样式
public class Demo implements org.apache.dubbo.demo.provider.javaspi.Pay {
​
    public void say()  {
        throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.demo.provider.javaspi.Pay.say() of interface org.apache.dubbo.demo.provider.javaspi.Pay is not adaptive method!");
    }
​
    public void pay(org.apache.dubbo.common.URL arg0)  {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg0;
        String extName = url.getParameter("payType", "ali");
        if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.demo.provider.javaspi.Pay) name from url (" + url.toString() + ") use keys([payType])");
        org.apache.dubbo.demo.provider.javaspi.Pay extension = (org.apache.dubbo.demo.provider.javaspi.Pay)ExtensionLoader.getExtensionLoader(org.apache.dubbo.demo.provider.javaspi.Pay.class).getExtension(extName);
        extension.pay(arg0);
    }
}

总结

至此,Dubbo SPI自适应扩展的源码分析结束,在这里对其执行流程做个总结:

  1. 从缓存中获取自适应扩展实例,存在即返回,不存在即创建

  2. 创建自适应扩展实例

    1. 创建实例

      1. 获取自适应扩展类

        1. 获取扩展类(这里的流程与Dubbo SPI一样)

        2. 创建自适应扩展类

          1. 动态生成具有代理功能的自适应扩展类代码
          2. 获取Compiler实例
          3. 使用Compiler编译生成的自适应扩展类
      2. 实例化自适应扩展类(反射)

      3. 向自适应扩展实例注入依赖(这里的流程与Dubbo SPI一样)

    2. 将实例添加到缓存