dubbo其他

Dubbo Adaptive机制详解实战

2019-10-12  本文已影响0人  晴天哥_王志

开篇

 本文尝试通过一个示例来讲解Adaptive机制的用法,然后会从源码的角度对其实现原理进行讲解

Adaptive机制

 对应于Adaptive机制,Dubbo提供了一个注解@Adaptive,该注解可以用于接口的某个实现类上,也可以用于接口方法上。如果用在接口的子类上,则表示Adaptive机制的实现会按照该子类的方式进行自定义实现;如果用在方法上,则表示Dubbo会为该接口自动生成一个子类,并且按照一定的格式重写该方法,而其余没有标注@Adaptive注解的方法将会默认抛出异常。

 对于第一种Adaptive的使用方式,Dubbo里只有ExtensionFactory接口使用了,其有一个子类AdaptiveExtensionFactory就使用了@Adaptive注解进行了标注,主要作用就是在获取目标对象时,分别通过ExtensionLoader和Spring容器两种方式获取,主要用在SPI的IOC实现中用于获取上下文的Bean对象。

 对于第二种使用@Adaptive注解标注在接口方法上以实现Adaptive机制的使用原理,通过一个下面的例子进行说明。

用法示例

例子说明

----定义基础应用类

public interface Fruit {}
public class Apple implements Fruit {}
public class Banana implements Fruit{}



----定义SPI类

@SPI("banana")
public interface PlantsWater {

    Fruit grant();

    @Adaptive
    String watering(URL url);
}


public class AppleWater implements PlantsWater {
    public Fruit grant() {
        return new Apple();
    }

    public String watering(URL url) {
        System.out.println("watering apple");
        return "watering finished";
    }
}


public class BananaWater implements PlantsWater {

    public Fruit grant() {
        return new Banana();
    }

    public String watering(URL url) {
        System.out.println("watering banana");
        return "watering success";
    }
}



----resources文件 org.apache.dubbo.spi.example.PlantsWater

apple=org.apache.dubbo.spi.example.AppleWater
banana=org.apache.dubbo.spi.example.BananaWater


------测试代码内容

public class ExtensionLoaderDemo {

    public static void main(String[] args) {
        // 首先创建一个模拟用的URL对象
        URL url = URL.valueOf("dubbo://192.168.0.101:20880?plants.water=apple");
        // 通过ExtensionLoader获取一个PlantsWater对象,getAdaptiveExtension已经加载了所有SPI类
        PlantsWater plantsWater = ExtensionLoader.getExtensionLoader(PlantsWater.class)
                .getAdaptiveExtension();
        // 使用该PlantsWater调用其"自适应标注的"方法,获取调用结果
        String result = plantsWater.watering(url);
        System.out.println(result);
    }
}


-----实际输出内容

十月 11, 2019 7:48:51 下午 org.apache.dubbo.common.logger.LoggerFactory info
信息: using logger: org.apache.dubbo.common.logger.jcl.JclLoggerAdapter
watering apple
watering finished

Process finished with exit code 0

原理补充

PlantsWater$Adaptive

说明:

package org.apache.dubbo.spi.example;
import org.apache.dubbo.common.extension.ExtensionLoader;

public class PlantsWater$Adaptive implements org.apache.dubbo.spi.example.PlantsWater {

    public java.lang.String watering(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("plants.water", "banana");
        
        if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.spi.example.PlantsWater) name from url (" + url.toString() + ") use keys([plants.water])");
        
        org.apache.dubbo.spi.example.PlantsWater extension = (org.apache.dubbo.spi.example.PlantsWater)ExtensionLoader
                                             .getExtensionLoader(org.apache.dubbo.spi.example.PlantsWater.class)
                                             .getExtension(extName);
        
        return extension.watering(arg0);
    }

    public org.apache.dubbo.spi.example.Fruit grant()  {
        throw new UnsupportedOperationException("The method public abstract org.apache.dubbo.spi.example.Fruit org.apache.dubbo.spi.example.PlantsWater.grant() of interface org.apache.dubbo.spi.example.PlantsWater is not adaptive method!");
    }
}

实现原理

Dubbo Adaptive的实现机制根据上面的讲解其实步骤已经比较清晰了,主要分为如下三个步骤:

可以看到,通过这种方式,Dubbo就实现了一种通过配置参数动态选择所使用的服务的目的,而实现这种机制的入口主要在ExtensionLoader.getAdaptiveExtension()方法

public class ExtensionLoaderDemo {

    public static void main(String[] args) {
        // 首先创建一个模拟用的URL对象
        URL url = URL.valueOf("dubbo://192.168.0.101:20880?plants.water=apple");
        // 通过ExtensionLoader获取一个FruitGranter对象
        PlantsWater plantsWater = ExtensionLoader.getExtensionLoader(PlantsWater.class)
                .getAdaptiveExtension();
        // 使用该FruitGranter调用其"自适应标注的"方法,获取调用结果
        String result = plantsWater.watering(url);
        System.out.println(result);
    }
}

说明:

public T getAdaptiveExtension() {
  Object instance = cachedAdaptiveInstance.get();
  if (instance == null) {
    if (createAdaptiveInstanceError == null) {
      synchronized (cachedAdaptiveInstance) {
        instance = cachedAdaptiveInstance.get();
        if (instance == null) {
          try {
            // 创建Adaptive实例
            instance = createAdaptiveExtension();
            cachedAdaptiveInstance.set(instance);
          } catch (Throwable t) {
            createAdaptiveInstanceError = t;
            throw new IllegalStateException("Failed to create adaptive " 
                + "instance: " + t.toString(), t);
          }
        }
      }
    } else {
      throw new IllegalStateException("Failed to create adaptive instance: " 
          + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
    }
  }

  return (T) instance;
}

说明:
上面的代码首先通过双检查法来从缓存中获取Adaptive实例,如果没获取到,则创建一个。我们这里继续看createAdaptiveExtension()方法的实现。

    private T createAdaptiveExtension() {
        try {
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
        }
    }

说明:
上面代码是创建Adaptive实例的方法是一个主干方法,从这里调用方法的顺序就可以看出其主要作用:

    private Class<?> getAdaptiveExtensionClass() {

  // 通过读取Dubbo的配置文件,获取其中的SPI类,其主要处理了四部分的类:
  // 1. 标注了@Activate注解的类,该注解的主要作用是将某个实现子类标注为自动激活,也就是在加载
  //    实例的时候也会加载该类的对象;

  // 2. 记录目标接口是否标注了@Adaptive注解,如果标注了该注解,则表示需要为该接口动态生成子类,或者说
  //    目标接口是否存在标注了@Adaptive注解的子类,如果存在,则直接使用该子类作为Adaptive类;
  
  // 3. 检查加载到的类是否包含有传入目标接口参数的构造方法,如果是,则表示该类是一个代理类,也可以
  //    将其理解为最终会被作为责任链进行调用的类,这些类最终会在目标类被调用的时候以类似于AOP的方式,
  //    将目标类包裹起来,然后将包裹之后的类对外提供服务;

  // 4. 剩余的一般类就是实现了目标接口,并且作为基础服务提供的类。

        getExtensionClasses();



  // 经过上面的类加载过程,如果目标接口某个子类存在@Adaptive注解,就会将其class对象缓存到
  // cachedAdaptiveClass对象中。这里我们就可以看到@Adaptive注解的两种使用方式的分界点,也就是说,
  // 如果某个子类标注了@Adaptive注解,那么就会使用该子类所自定义的Adaptive机制,如果没有子类标注了
  // 该注解,那么就会使用下面的createAdaptiveExtensionClass()方式来创建一个目标类class对象
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }

  // 创建一个目标接口的子类class对象
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }


    private Class<?> createAdaptiveExtensionClass() {
  // 为目标接口生成子类代码,以字符串形式表示
        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();

  // 获取classloader
        ClassLoader classLoader = findClassLoader();

  // 通过jdk或者javassist的方式编译生成的子类字符串(默认是javassist),从而得到一个class对象
        org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        return compiler.compile(code, classLoader);
    }

说明:
上面的代码中主要是一个骨架代码,首先通过getExtensionClasses()获取配置文件中配置的各个类对象,其加载的原理可阅读文章Dubbo之SPI原理详解;加载完成后,会通过AdaptiveClassCodeGenerator来为目标类生成子类代码,并以字符串的形式返回,最后通过javassist或jdk的方式进行编译然后返回class对象。这里我们主要阅读AdaptiveClassCodeGenerator.generate()方法是如何生成目标接口的子类的。

   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();
        code.append(generatePackageInfo()); // 生成package信息
        code.append(generateImports()); // 生成import信息,这里只导入了ExtensionLoader类,其余的类都通过全限定名的方式来使用
        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(); // 返回生成的class代码
    }

说明:
上面代码的generate()方法是生成目标类的主干方法,其主要分为如下几个步骤:

    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); // 对方法进行格式化返回
    }

说明:
上面代码的方法的生成,也拆分成了几个子步骤,主要包括:

    private String generateMethodContent(Method method) {

        // 获取方法上标注的@Adaptive注解,前面讲到,Dubbo会使用该注解的值作为动态参数的key值
        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
        StringBuilder code = new StringBuilder(512);

        // 如果当前方法没有标注@Adaptive注解,该方法的实现就会默认抛出异常
        if (adaptiveAnnotation == null) {
            return generateUnsupported(method);
        } else {

            // 获取参数中类型为URL的参数所在的参数索引位置,因为我们的参数都是通过arg[i]的形式编排的,因而
            // 获取其索引就可以得到该参数的引用。这里URL参数的主要作用是获取目标参数对应的参数值
            int urlTypeIndex = getUrlTypeIndex(method);
            
            if (urlTypeIndex != -1) {
                // 如果参数中存在URL类型的参数,那么就为该参数进行空值检查,如果为空,则抛出异常
                code.append(generateUrlNullCheck(urlTypeIndex));
            } else {
                // 如果参数中不存在URL类型的参数,那么就会检查每个参数,判断其是否有某个方法的返回值是URL类型,
                // 如果存在该方法,则首先对该参数进行空指针检查,如果为空则抛出异常。然后调用该对象的目标方法,
                // 以获取到一个URL对象,然后对获取到的URL对象进行空值检查,为空也会抛出异常。
                code.append(generateUrlAssignmentIndirectly(method));
            }

            // 这里主要是获取@Adaptive注解的参数,如果没有配置,就会使用目标接口的类型由驼峰形式转换为点分形式
            // 的名称作为将要获取的参数值的key名称,比如前面的PlantsWater转换后为plants.water。
            // 这里需要注意的是,返回值是一个数组类型,这是因为Dubbo会通过嵌套获取的方式来的到目标参数,
            // 比如我们使用了@Adaptive({"client", "transporter"})的形式,那么最终就会在URL对象中获取两次
            // 参数,如String extName = url.getParameter("client", url.getParameter("transporter"))
            String[] value = getMethodAdaptiveValue(adaptiveAnnotation);

            // 判断是否存在Invocation类型的参数
            boolean hasInvocation = hasInvocationArgument(method);
            
            // 为Invocation类型的参数添加空值检查的逻辑
            code.append(generateInvocationArgumentNullCheck(method));
            
            // 生成获取extName的逻辑,也即前面通过String[] value生成的通过url.getParameter()的
            // 逻辑代码,最终会得到用户配置的扩展的名称,从而对应某个基础服务类
            code.append(generateExtNameAssignment(value, hasInvocation));

            // 为extName添加空值检查代码
            code.append(generateExtNameNullCheck(value));
            
            // 通过extName在ExtensionLoader中获取其对应的基础服务类,比如前面的PlantsWater,在这里就是
            // PlantsWater extension = ExtensionLoader.getExtensionLoader(ExtensionLoader.class)
            // .getExtension(extName),这样就得到了一个PlantsWater的实例对象
            code.append(generateExtensionAssignment());

            // 生成目标实例的当前方法的调用逻辑,然后将结果返回。比如PlantsWater就是
            // return extension.watering(arg0);
            // 这里方法名就是当前实现的方法的名称,而参数就是当前方法传入的参数,
            // 就是目标接口中的同一方法,而方法参数前面已经讲到,都是使用arg[i]的形式命名的,因而这里直接
            // 将其依次罗列出来即可
            code.append(generateReturnAndInvocation(method));
        }
        
        // 将生成的代码返回
        return code.toString();
    }



    private String[] getMethodAdaptiveValue(Adaptive adaptiveAnnotation) {
        // 获取@Adaptive注解的配置获取目标参数的key值
        String[] value = adaptiveAnnotation.value();
        // @Adaptive没有注解,就通过Interface定义的名字如PlantsWater按字母分割生成plantswater 
        if (value.length == 0) {
            String splitName = StringUtils.camelToSplitName(type.getSimpleName(), ".");
            value = new String[]{splitName};
        }
        return value;
    }

说明:
上面的逻辑主要分为了如下几个步骤:

可以看到,这里实现的自适应机制逻辑结构是非常清晰的,读者通过阅读这里的源码也就比较好的理解了Dubbo所提供的自适应机制的原理,也能够比较好的通过自适应机制来完成某些定制化的工作。

补充

在Adaptive扩展机制实现当中,有几个变量有必要梳理一下。

extName的获取优先级:@SPI("xxx")和URL.getParameter("yyy","xxx"),如果URL中带有变量标识则优先取URL,否则取@SPI中的变量。

AdaptiveValue的获取,@Adaptive("zzz") 和 interface PlantsWater当中的PlantsWater接口名,优先以@Adaptive标识的名字,其次取接口名分割。

参考

Dubbo Adaptive机制详解

上一篇下一篇

猜你喜欢

热点阅读