Dubbo-SPI

前言

Dubbo 支持多种传输协议,可以有多种注册中心等等高扩展的功能,其原因就是 Dubbo SPI 提供的良好的扩展性,可以给开发者自己实现多样丰富的功能。Dubbo SPI在Java SPI的基础上发展而来,和Java SPI 一样,都是典型的策略模式实现,本文就主要介绍 Dubbo SPI 的使用方式和源码分析。

结合代码能够更深了解本文的内容,代码github地址:https://github.com/mikasaco/dubbo-study.git

Dubbo SPI 和 Java SPI 的比较

Java SPI 的使用方式可以参考博客

Dubbo SPI 相比于 Java SPI 有以下 3 个 优势:

  1. Java SPI 每次都会把所有实现类都加载并实例化(是在迭代器迭代的时候创建实例),而 Dubbo SPI 是分两段创建实例,先进行类加载,然后在使用到具体实现的时候才实例化,并且 Dubbo SPI 大量使用缓存,会把 Class 对象和实例对象都缓存起来,性能更好;

  2. Java SPI 在类加载失败的时候难以定位异常;

  3. Dubbo SPI 还支持 IOC 和 AOP 。

补充一下JVM的类加载机制

类加载大的上来分有 加载、连接、初始化 3 个步骤。经常会误把加载等同于类加载,把加载包含初始化。

  • 加载是把 class 文件读到方法区,并创建一个 Class 对象。 代码 Class.forName() 就是实现了加载;
  • 连接细分有包括了验证、准备、解析,这中间的步骤都是 JVM 实现,开发者不能干预;
  • 初始化阶段是执行类定义的构造方法,创建实例对象。代码 Class.newInstance() 就是实现初始化这一步。

在 Dubbo SPI 中把类加载就分成了两部分,先会加载 class 文件,但并不会立刻初始化,而是在使用的时候再调用 Class.newInstance() ,并且会将 Class.forName() 加载之后的 Class 对象和 Class.newInstance() 实例对象都缓存起来。

Dubbo SPI 的使用

我们以最简单的使用姿势为例,可以参考 github 中 basic 的实现。主要是以下几点:

  • 标注了 @SPI 注解的接口;
  • 实现了 @ SPI 注解的接口的实现类;
  • META-INF/services/ META-INF/dubbo/ META-INF/dubbo/internal/ 目录下创建接口全限定名的文件;
  • 文件中的内容是 key-value 形式, key 是实现类的别名, value 是实现类的全限定名。

Dubbo SPI 的三大注解

@SPI

用在接口上,标注这个接口是一个扩展点,可以使用 Dubbo SPI 功能。如果没加 @SPI 注解,会在 ExtensionLoader.getExtensionLoader 的时候就抛出异常。

如果 @SPI(“javassist”) 这种,也就是接口的默认实现的别名是 javassist ,通过 loader.getDefaultExtension() 可以获取默认实现。

@adaptive

@adaptive 也叫做自适应注解,顾名思义可以根据参数自己选择实现类。使用的方式有两种:注解在实现类上或注解在接口的方法上,Dubbo中只有两个类 AdaptiveCompiler 和 AdaptiveExtensionFactory 在类上使用该注解。

接口的方法

用在接口方法上时,会根据参数自动生成一个类,类名是 接口$Adaptive。然后在调用loader.getAdaptiveExtension()的时候就会执行这个生成的类,再根据参数去调用不同的实现类。

实现类

用在实现类上的话,标注这个实现类是接口的适配器实现,不会自动生成代码。调用 loader.getAdaptiveExtension()会返回这个实现类。但注意和loader.getDefaultExtension()的区别。

ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
Compiler adaptiveExtension = loader.getAdaptiveExtension();
adaptiveExtension.compile(); // AdaptiveCompiler 这个返回的只是配置中的key为adaptive的实现

Compiler defaultExtension = loader.getDefaultExtension();
defaultExtension.compile(); //JavassistCompiler 这个才是默认实现

@activate

通常获取扩展实现只能获取到一个实现类,但例如拦截器等场景通常需要我们可以扩展一系列的功能,而 @activate 注解就是为了这种场景而生的。@activate 可以注解在很多实现类上,然后通过 group 或 value 在URL参数中自动激活不同的多个实现类。

Dubbo SPI 的实现原理

Dubbo SPI 的实现原理我们探究这几种情况:

  1. 普通扩展原理
  2. 适配器扩展原理
  3. IOC 扩展原理
  4. AOP 扩展原理
  5. 自动激活扩展原理

普通扩展原理

使用注意

普通扩展的使用很简单,需要注意的是 @SPI(“javassist”) 注解中的 value 就是这个扩展点的默认实现,通过 ExtensionLoader.getDefaultExtension 可以直接获取到默认实现。

源码解析
ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
Compiler adaptiveExtension = loader.getDefaultExtension();

对于 ExtensionLoader.getExtensionLoader() 这行代码就是获取了接口的扩展类加载器,new ExtensionLoader<T>(type) type 就是 Compiler.class ,所以很简单,就是 new 了一个 ExtensionLoader。

而实现扩展的代码是在 ExtensionLoader#getExtension() 中,我们重点分析获取 ExtensionLoader#getExtension() 这行代码。

普通扩展原理

上图就是 Dubbo SPI 加载扩展类的方式,普通扩展是其他扩展的基础,从上面的部分我们也能看出 Dubbo SPI 相比于 Java SPI 的优势:

  1. Dubbo SPI 并不是程序一启动就会加载扩展实现,而是在调用 extensionLoader.getDefaultExtension() 等的时候才会去加载扩展类;
  2. Dubbo SPI 对 Class 对象、实例对象都会进行缓存,性能更好。

下面看下具体的源码。

//ExtensionLoader#getExtension
public T getExtension(String name) {
  ...
  Holder<Object> holder = cachedInstances.get(name); // 从实例缓存中获取
  if (holder == null) {
    cachedInstances.putIfAbsent(name, new Holder<Object>());
    holder = cachedInstances.get(name);
  }
  Object instance = holder.get();
  if (instance == null) {
    synchronized (holder) {
      instance = holder.get();
      if (instance == null) {
        instance = createExtension(name); // 去创建实例
        holder.set(instance);
      }
    }
  }
  return (T) instance;
}

//ExtensionLoader#createExtension
private T createExtension(String name) {
  Class<?> clazz = getExtensionClasses().get(name);// 获取Class对象
  ...
    if (instance == null) {
      EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());// 创建实例
    }
  ... // 普通扩展不用关注,在IOC和AOP部分需要注意
}

//ExtensionLoader#getExtensionClasses
private Map<String, Class<?>> getExtensionClasses() {
  Map<String, Class<?>> classes = cachedClasses.get();// 从Class缓存中取
  ...
    if (classes == null) {
      classes = loadExtensionClasses();// 没获取到就自己去加载
      cachedClasses.set(classes);
    }
}

//ExtensionLoader#loadExtensionClasses
private Map<String, Class<?>> loadExtensionClasses() {
  final SPI defaultAnnotation = type.getAnnotation(SPI.class);
  if (defaultAnnotation != null) {
    String value = defaultAnnotation.value();// 这里获取SPI的默认实现
    ...
  }
  Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
  loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);// META-INF/dubbo/internal/
  loadDirectory(extensionClasses, DUBBO_DIRECTORY); // META-INF/dubbo/
  loadDirectory(extensionClasses, SERVICES_DIRECTORY);// META-INF/services/
  return extensionClasses;
}

//ExtensionLoader#loadDirectory
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
  ...  
    urls = classLoader.getResources(fileName);  //获取配置文件(还未读入内存)
  ...
    while (urls.hasMoreElements()) {
      java.net.URL resourceURL = urls.nextElement();
      loadResource(extensionClasses, classLoader, resourceURL);// 解析配置文件的内容并加载
    }
}

//ExtensionLoader#loadResource
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
  BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8")); // IO读取配置文件
  ...
    loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name); // Class.forName 加载Class文件,创建Class对象
}

适配扩展原理

使用注意
区别点注解在接口方法注解在实现类
getAdaptiveExtension自动生成的类,但会根据参数动态选择别的实现注解了 @Adaptive 的实现类
配置文件不需要写需要,key 随便写什么
参数方法参数必须包含 URL 实例没要求

当注解在接口方法上时,如果是 @Adaptive({“type”,“subType”}) ,匹配规则:

  • 先匹配配置文件中 key 有没有等于 type 的 value的;
  • 再匹配配置文件中 key 有没有等于 subType 的 value的;
  • 最后只能用默认实现,接口上注解的 @SPI(“默认实现”)。
源码解析

适配扩展的原理和普通扩展有些类似,在适配扩展的代码中一部分和普通扩展是重合的。

适配扩展需要使用 @Adaptive 注解,上文提到该注解有两种使用方式:注解在实现类上和注解上接口方法上,两者的区别在 ExtensionLoader#loadClass() 开始体现。

适配扩展原理

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
  ...
    if (clazz.isAnnotationPresent(Adaptive.class)) { // 实现类上有注解@adaptive
      if (cachedAdaptiveClass == null) {
        cachedAdaptiveClass = clazz; // 把这个实现类放入cachedAdaptiveClass中
      }   
      ...
    }
}

private Class<?> getAdaptiveExtensionClass() {
  getExtensionClasses();
  //如果cachedAdaptiveClass缓存中有就返回Class对象了,也就是之前注解在实现类上的Class对象
  if (cachedAdaptiveClass != null) { 
    return cachedAdaptiveClass;
  }
  return cachedAdaptiveClass = createAdaptiveExtensionClass();//自动创建适配类
}

private Class<?> createAdaptiveExtensionClass() {
  String code = createAdaptiveExtensionClassCode();// 自动创建的类的字符串
  ClassLoader classLoader = findClassLoader();
  // 获取编译器Compiler
  com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(Compiler.class).getAdaptiveExtension();
  return compiler.compile(code, classLoader);// 编译
}         

注解在接口方法上需要自动生成一个扩展类,这个类本来是没有的,是通过字符串拼接出来的,而加载的方式是动态编译,有 JDK 编译器和 javassist 编译器两种方式(还有一个是注解了 @Adaptive 的 AdaptiveCompiler,这个就是根据参数选择用 JDK 还是 javassist) ,Dubbo 默认的是 javassist。javassist是一个动态编译的 Java 类库,相比于 JDK 的动态编译,效率更高。

Java 动态生成 Class 的方式(动态编译)有两种:基于字节码的操作、基于 API 的操作。javassist 属于后者,ASM 是前者,前者性能更高,但要操作字节码比较复杂。

IOC 扩展原理

使用注意
  • 使用 IOC-SPI 的扩展方式时,注入的扩展必须是适配扩展的方式,因为在 SpiExtensionFactory 获取的是适配扩展; SpiExtensionFactory

  • 使用 IOC-SPI 的扩展方式时, setXXX(Class c) 去找注入的扩展实现时,只会用到 Class ,也就是要注入的扩展接口, XXX 随便写什么;

  • 使用 IOC-spring 的扩展方式时,先根据 setXXX 中的 XXX 去 ApplicationContext 容器中匹配 bean name 获取扩展实现,没有找到再根据 Class 类型去匹配。

源码解析

Dubbo 的 IOC 最突出的体现就是在使用扩展点的时候,可以注入其他扩展点。具体方法在 ExtensionLoader#injectExtension ,对于普通扩展、适配扩展等都会在加载扩展实现类之后执行这个方法,如果有依赖(set 属性的方法)的话,就通过这个方法注入。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k0sa8FWp-1579508037687)(Dubbo-扩展点SPI/image-20191212204903256.png)]

private T injectExtension(T instance) {
  for (Method method : instance.getClass().getMethods()) { // 遍历所有的方法
    if (method.getName().startsWith("set") // 如果以set开头,且public修饰,参数一个
        && method.getParameterTypes().length == 1
        && Modifier.isPublic(method.getModifiers())) {
      if (method.getAnnotation(DisableInject.class) != null) {
        continue; // 方法注释DisableInject 跳过
      }
      Class<?> pt = method.getParameterTypes()[0];
      String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
      // spring和SPI的方式获取符合的扩展类
      Object object = objectFactory.getExtension(pt, property);
      if (object != null) {
        method.invoke(instance, object); //调用set方法进行注入
      }
    }
  }
}

// AdaptiveExtensionFactory#getExtension
public <T> T getExtension(Class<T> type, String name) {
  for (ExtensionFactory factory : factories) {
    T extension = factory.getExtension(type, name);
  }
}

// SpiExtensionFactory#getExtension
public <T> T getExtension(Class<T> type, String name) {
  if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
    ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
    if (!loader.getSupportedExtensions().isEmpty()) {
      // 这里获取的是适配扩展,所以SPI的方式一定要是适配扩展
      return loader.getAdaptiveExtension(); 
    }
  }
  return null;
}

// SpringExtensionFactory#getExtension
 public <T> T getExtension(Class<T> type, String name) {
   for (ApplicationContext context : contexts) {
     if (context.containsBean(name)) { 
       Object bean = context.getBean(name); // 根据 name 获取 bean
     }
   }
   for (ApplicationContext context : contexts) {
     return context.getBean(type); // 根据类型获取
     ...
   }
 } 
}

AOP 扩展原理

使用注意
  1. 对于扩展实现类和扩展接口,并不需要做额外的处理,这也体现了 AOP 的无侵入的特点;
  2. 装饰类继承扩展接口,需要声明构造方法,并且传入参数是扩展接口类型
  3. Dubbo 的 AOP 实现是典型的装饰者模式;
  4. 可以被多重装饰,最外层的装饰是配置文件最下面的装饰类。
源码解析

Dubbo SPI 支持 AOP 功能,是用装饰者模式实现的。当执行 extensionLoader.getDefaultExtension() 返回的其实是装饰对象(wrapper2)而不是原来的扩展实现(javassistCompiler),所以执行的是增强的方法。
AOP使用例子

需要注意的是,我们配置文件中装饰类的顺序会对装饰的顺序有影响的。例如 github 上的例子,如果把 wrapper1 和 wrapper2 换一下顺序,那么装饰的顺序也会变化。
装饰文件顺序

  1. 获取到的扩展实现其实是装饰对象,而非原来的扩展实现类;
  2. 配置文件中装饰类顺序从上往下依次是增强装饰的从内到外。

带着这两个结论,我们看下 Dubbo 是如何实现的。主要是在两个地方: loadClass 方法和 createExtension方法。


loadClass

Dubbo 在解析配置文件中的每行配置信息时,会判断这行配置对应类的构造方法的传入参数类型,是不是扩展接口,如果是的话就把这行配置对应的类放入 cachedWrapperClasses (缓存装饰类)。

这里可以解释上面的第 2 点结论:配置文件中装饰类顺序从上往下依次是增强装饰的从内到外。因为解析配置信息也是一行一行解析的,所以配置文件上面的装饰类(构造方法的传入参数类型是扩展接口),会先放入 cachedWrapperClasses 。

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
  ...
    else if (isWrapperClass(clazz)) {
      Set<Class<?>> wrappers = cachedWrapperClasses;
      if (wrappers == null) {
        cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
        wrappers = cachedWrapperClasses;
      }
      wrappers.add(clazz); // 放入装饰类缓存
    } 
}

// 判断类的构造方法的传入参数类型是不是扩展接口
private boolean isWrapperClass(Class<?> clazz) {
  try {
    clazz.getConstructor(type);
    return true;
  } catch (NoSuchMethodException e) {
    return false;
  }
}
createExtension

创建扩展类实例的时候,会遍历装饰类缓存,有的话,把扩展实现类当作构造方法的参数传入进去,创建装饰类实例。所以最后返回的是最外层的装饰类。

private T createExtension(String name) {
  ...
    Set<Class<?>> wrapperClasses = cachedWrapperClasses;
  if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
    // 从装饰类缓存中依次取出装饰类,并把实际的扩展类(github例子中的javassist)当作参数传入第一个装饰类(wrapper1,因为配置文件中wrapper1先解析),然后 instance会被覆盖,再次循环进行装饰(例子中装饰wrapper2,传入参数wrapper1)
    for (Class<?> wrapperClass : wrapperClasses) {
      instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
    }
  }
  return instance;
  ...
}

自动激活扩展原理

当调用 ExtensionLoader.getActivateExtension(URL url, String[] values, String group) 会根据 URL 中的参数和 value 以及 group 去匹配符合的扩展实现。@Activate 注解通常有三个参数:

  • group 激活同一组的扩展实现;
  • value 激活value名称符合的扩展实现;
  • order 激活的扩展实现排序。
public List<T> getActivateExtension(URL url, String[] values, String group) {
  List<T> exts = new ArrayList<T>();
  List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
  // 如果value -default 的参数不会激活
  if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
    // 这一步加载扩展类的Class对象,如果类上注解了Activate会放入缓存cachedActivates,给后面遍历
    getExtensionClasses();
    for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
      String name = entry.getKey();
      Activate activate = entry.getValue();
      if (isMatchGroup(group, activate.group())) {// group匹配实现类
        T ext = getExtension(name);
        if (!names.contains(name)// value匹配实现类
            && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
            && isActive(activate, url)) {
          exts.add(ext);
        }
      }
    }
    Collections.sort(exts, ActivateComparator.COMPARATOR);// order排序
  }
  ...
  return exts;
}

ExtensionFactory

ExtensionFactory 是创建整个 SPI 的核心,但 ExtensionFactory 本身又是通过 SPI-Adaptive 的方式创建的,在学习 SPI 这部分的时候,总有种鸡生蛋,蛋生鸡的疑惑,我们以 Dubbo 启动的入口 com.alibaba.dubbo.container.Mian 看下 ExtensionFactory 是怎么加载的。

  1. Main 入口类初始化的时候,创建静态成员变量 容器加载类 ExtensionLoader< Container > loader;

  2. 执行 ExtensionLoader 的构造方法,关键就在这个构造方法中;

    private ExtensionLoader(Class<?> type) {
        this.type = type;
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }
    
  3. 最后 getAdaptiveExtension() 返回的是 ExtensionFactory 的实现类 AdaptiveExtensionFactory ,因为有注解 @Adaptive。
    ExtensionFactory 的三个实现类

​ AdaptiveExtensionFactory。调用 getAdaptiveExtension() 方法会返回这个实现类,一般都是用的这个,但这个实现类最后还是要调用 SPI/spring 的实现类,也就是最后获取的具体实现还是通过 SPI/spring 的方式。

构造方法注入 SPI/spring 的 ExtensionFactory 工厂,getExtension() 获取实现的方法就是遍历这两个工厂,从中取扩展实现类。

public class AdaptiveExtensionFactory implements ExtensionFactory {

    private final List<ExtensionFactory> factories;// SPI/spring 工厂

    public AdaptiveExtensionFactory() {
      ...
        for (String name : loader.getSupportedExtensions()) {
            list.add(loader.getExtension(name)); //塞入 SPI/spring 工厂
        }
    }

    public <T> T getExtension(Class<T> type, String name) {
        for (ExtensionFactory factory : factories) { // 遍历 SPI/spring 工厂
            T extension = factory.getExtension(type, name); // 取扩展类
          ...
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值