cover_image

Dubbo SPI 扩展实现原理

风度玉门 拍码场
2023年01月13日 06:26

前言

Apache Dubbo 是一款 RPC 微服务框架,提供了包括 Java、Golang 等在内的多种语言 SDK 实现。可扩展性是一种设计理念,我们希望在现有的架构或设计基础上,以最小的改动来适应未来的变化。Dubbo SPI 的加载机制,让整个框架的接口和具体实现完全解耦,从而奠定了整个框架良好的可扩展性。

Dubbo SPI 没有直接使用 Java SPI,而是在其基础上做了相应的改进,形成了一套自己的规范和特性。下面我们从最基础的 Java SPI 介绍一下 SPI 的思想。

1 Java SPI

SPI (Service Provider Interface)最初是提供给厂商做插件开发的。Java SPI 使用了策略模式,一个接口会有多个实现。我们只声明接口,具体的实现并不在程序中直接确定,而是由程序之外的配置控制,用于最终的装配。一个简单的实现步骤如下:

  1. 定义一个接口及方法
  2. 编写该接口的一个实现
  3. META-INF/services/目录下创建一个接口全路径命名的文件,如 com.moguhu.spi.HelloService
  4. 文件内容为接口的具体实现类的全路径,每个实现类单独一行
  5. 在代码中通过 java.util.ServiceLoader 来加载具体的实现

下面我们看下具体的代码实现:

图片

获取实现类的工具方法 SpiServiceHelper 的代码实现:

/** * SPI类加载辅助工具 * * @author xuefeihu created on 2021/05/31. */public class SpiServiceHelper {
private final static Map<String, Object> singletonServices = new HashMap<>();
public synchronized static <S> S load(Class<S> service) { return StreamSupport.stream(ServiceLoader.load(service).spliterator(), false).map(SpiServiceHelper::singletonFilter) .findFirst().orElseThrow(ServiceLoadException::new); }
public synchronized static <S> Collection<S> loadAll(Class<S> service) { return StreamSupport.stream(ServiceLoader.load(service).spliterator(), false).map(SpiServiceHelper::singletonFilter) .collect(Collectors.toList()); }
@SuppressWarnings("unchecked") private static <S> S singletonFilter(S service) { if (service.getClass().isAnnotationPresent(MoSingleton.class)) { String className = service.getClass().getCanonicalName(); Object singletonInstance = singletonServices.putIfAbsent(className, service); return singletonInstance == null ? service : (S) singletonInstance; } else { return service; } }
}

2 Dubbo SPI 规范

Dubbo SPI 和 Java SPI 类似,需要在 META-INF/dubbo/ 目录下放置对应的 SPI 配置文件,文件名为接口的全路径名。如下所示:

图片

这里面需要注意的是,除了资源路径与 Java SPI 不同之外,文件内容中也多了个 key,如上图中的dubbo、hessian2、fastjson 等。这里与dubbo 最开始 2.0.x 版本中的 SPI 也有所不同(@SPI 原先叫做 @Extension),原先每个SPI 的实现的名称是写在每个实现类上的。接口和实现类都要有 @Extension 注解,实现类的注解上面会写这个SPI 实现的名字。而最新的扩展则是只需要在接口上加上@SPI,然后配置文件上带有对应 SPI 实现类的名称,从一定程度上做了简化。

图片

2.1 扩展点的分类与缓存

Dubbo SPI 是按需加载的,具体逻辑在ExtensionLoader类中实现,可以分为 Class 缓存实例缓存。这两种缓存又能根据扩展类的种类分为普通扩展类包装扩展类(Wrapper)、自适应扩展类(Adaptive)等。

Class 缓存:Dubbo SPI 获取扩展类时,首先会从缓存中获取。如果缓存中不存在,则会加载配置文件,将Class 缓存加载到内存中,类似于懒加载。

实例缓存ExtensionLoader 中不仅会缓存Class,也会缓存Class实例化后的对象。整体原则就是会从缓存中优先获取,缓存中没有的再使用 Class.forName() 将类加载到内存中。

被缓存的Class 和对象实例可以分为以下几类:

1. 普通扩展类,最基础的,配置在SPI 配置文件中的扩展类实现

2. 包装扩展类,包装类通常不会直接承载具体的实现,它是对ExtensionLoader加载的策略类的包装,通过这个包装可以实现Filter、Listener 等效果。如 Protocol 的包装类 ProtocolFilterWrapperProtocolListenerWrapper

3. 自适应扩展类,可以理解为对不同策略类的包装,这个类是由Dubbo 动态生成的,里面的内容大概可以理解为就是一个策略的组装,最终的业务调用还是由各个策略类实现。不同策略的选择通常是由 URL 参数决定的。

4. 其他缓存,如扩展类加载器缓存、扩展名缓存等。

图片

2.2 扩展点提供的功能

Dubbo SPI 一共包含 4 种特性,包含:自动包装(Wrapper)、自动装载(Autowire)、自适应(Adaptive)、自动激活(Active),下面分别介绍一下这4类特性。其中 Wrapper、Autowire、Adaptive 这3 个功能,在 Dubbo 开源的第一版(2.0.7)中就已经提供,后续只是简单的加了一个 Active 的功能。

自动包装

ExtensionLoader 加载扩展时,如果发现这个类有1个参数并且参数为ExtensionLoader 中的 T 类型时,就会自动认为是 wrappers(老版本命名叫做 autoproxies),也就是表示对真正的扩展包了一层。

public class ProtocolListenerWrapper implements Protocol {
public ProtocolListenerWrapper(Protocol protocol) { if (protocol == null) { throw new IllegalArgumentException("protocol == null"); } this.protocol = protocol; }
}

从上面可以看出,ProtocolListenerWrapper 有个1个参数且入参为 Protocol 的构造方法,此时ProtocolListenerWrapper 就是一个包装类,当获取一个Protocol的扩展时,如:DubboProtocol,此时就会为 DubboProtocol 自动作为入参,被包装为ProtocolListenerWrapper 的类型。因为 ProtocolListenerWrapper 也是一个Protocol 类型,此时ExtensionLoader 可以返回此包装类给上游使用。包装类主要是用于增强 扩展类的功能,比如 Protocol 的包装类有 2个:ProtocolListenerWrapper 和 ProtocolFilterWrapper ,分别为 Protocol 增加了过滤器和 监听器,以扩展其功能。

自动装载

自动装载类似于Spring 中的 Autowire,也就是自动装配。当一个扩展类是另外一个扩展类的成员变量时,并且有 setter 方法,此时ExtensionLoader 就会自动new 一个扩展类,自动注入。通常被注入的会是一个接口类型,如 Protocol,则会被注入为 Protocol$Adaptive,也就是一个自适应类型。

自适应

自适应的扩展可以认为是策略模式中,组装策略的那部分代码,而策略本身是由 URL 参数中的参数决定的。下面我们看下一个 Adaptive 的例子。

首先我们提供一个扩展接口 SimpleExt,其中有3个方法,echo()yell() 允许做自适应,而 bang() 不允许做自适应。

@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);}

经过ExtensionLoader 创建出来的自适应扩展代码,大概如下所示,也就是说具体通过哪个策略,是通过 URL 中对应的参数决定的:

package com.alibaba.dubbo.common.extensionloader.ext1;
import com.alibaba.dubbo.common.extension.ExtensionLoader;import com.alibaba.dubbo.common.extensionloader.ext1.SimpleExt;
public class SimpleExt$Adaptive implements SimpleExt {
public java.lang.String echo(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) { if (arg0 == null) throw new IllegalArgumentException("url == null"); com.alibaba.dubbo.common.URL url = arg0; String extName = url.getParameter("simple.ext", "impl1"); if(extName == null) throw new IllegalStateException("Fail to get extension(SimpleExt) name from url(" + url.toString() + ") use keys([simple.ext])"); SimpleExt extension = (SimpleExt)ExtensionLoader.getExtensionLoader(SimpleExt.class).getExtension(extName); return extension.echo(arg0, arg1); }
public java.lang.String yell(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) { if (arg0 == null) throw new IllegalArgumentException("url == null"); com.alibaba.dubbo.common.URL url = arg0; String extName = url.getParameter("key1", url.getParameter("key2", "impl1")); if(extName == null) throw new IllegalStateException("Fail to get extension(SimpleExt) name from url(" + url.toString() + ") use keys([key1, key2])"); SimpleExt extension = (SimpleExt)ExtensionLoader.getExtensionLoader(SimpleExt.class).getExtension(extName); return extension.yell(arg0, arg1); }
public java.lang.String bang(com.alibaba.dubbo.common.URL arg0, int arg1) { throw new UnsupportedOperationException("method bang is not adaptive method!"); }
}

自动激活

自动激活是ExtensionLoader 后续新增的功能,通过 @Adaptive 注解实现。可以标记对应的扩展点是否可以被激活使用,目前主要用于Filter 链构建前的,可用Filter列表获取。因为是后续新增的功能,笔者对比了Dubbo 2.6.02.0.7 两个版本里面Filter 列表获取的差异,老的里面是通过配置写死的 Filter 列表顺序,而新的顺序则是通过 @Adaptive 在各个扩展实现类中标注的,此时它可以标注分组(Filter 链可以用在服务端/客户端),也可以标注分组内的顺序。此时依赖 @Active 注解就可以构建出原来写死在代码里的 Filter 顺序列表了。

3 ExtensionLoader 实现

ExtensionLoader 是 Dubbo SPI 的核心类,里面提供了一系列的静态方法用于获取扩展类的对象。下面我们看下 getExtension()getAdaptiveExtension()getActiveExtension() 的实现流程。

3.1 getExtension() 流程

图片

从上图可以看出,getExtension() 首先会调用 createExtension()创建扩展,然后会读取该扩展的所有实现 Class 到内存中。当然这里面会判断之前是否已经加载过了该接口的扩展Class。之后会判断这个实现类的Class 在内存中是否已经有了实例,如果之前已经new 过了对象,则直接返回即可。如果是第一次获取,则会实例化该Class,然后对该对象中的 setter 方法做自动自动注入扩展(通常是 Adaptive类型)。然后该扩展接口如果有 Wrapper 类,则会依次包装,也就是在new 出来的对象上再包上一层,最后返回这个 Wrapper 类的实例。当然对于 Wrapper 的实例也会对其 setter 方法进行自动注入相应的扩展。

3.2 getAdaptiveExtension() 流程

图片

getAdaptiveExtension() 首先会判断缓存中是否已经初始化过Adaptive 对象,如果已经存在则直接返回。如果不存在,则调用 createAdaptiveExtension() 创建Adaptive扩展。此时和getExtension() 一样会去先加载接口的所有扩展Class到内存中,然后根据Adaptive注解生成Adaptive扩展类的代码,并编译为Class对象返回。之后会实例化该对象,并自动注入setter 属性,最后返回。

3.3 getActiveExtension() 流程

图片

getActiveExtension() 首先会加载所有的扩展Class 到内存中,然后根据@Active 中配置的 before、after、order 进行排序。最后对于用户自定义扩展类,根据URL入参进行排序,最后返回符合条件的有序扩展类实例列表。

3.4 ExtensionFactory 实现

对于 ExtensionFactory,是Dubbo 后续版本引入的。笔者对比了 Dubbo 2.6.02.0.7 两个版本,发现这个工厂仅仅是在做自动注入是被使用到的。下面我们看下 2个版本的自动注入地方的代码实现。

2.0.7版本

private void injectExtension(Object instance) {  try {    for (Method method : instance.getClass().getMethods()) {      if (method.getName().startsWith("set") && method.getParameterTypes().length == 1 && Modifier.isPublic(method.getModifiers())) {        Class<?> pt = method.getParameterTypes()[0];        if (pt.isInterface()) {          try {            // 对于自动装配的 bean, 直接获取 adaptive 类型的扩展            Object adaptive = getExtensionLoader(pt).getAdaptiveExtension();            method.invoke(instance, adaptive);          } catch (Exception e) {            logger.error("fail to inject via method " + method.getName() + " of interface " + type.getName() + ": " + e.getMessage(), e);          }        }      }    }  } catch (Exception e) {    logger.error(e.getMessage(), e);  }}

2.6.0版本

private T injectExtension(T instance) {  try {    if (objectFactory != null) {      for (Method method : instance.getClass().getMethods()) {        if (method.getName().startsWith("set")            && method.getParameterTypes().length == 1            && Modifier.isPublic(method.getModifiers())) {          Class<?> pt = method.getParameterTypes()[0];          try {            // 如 setName, 解析后 property = name            String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";            // 这里通过 ExtensionFactory 获取扩展类的实例            Object object = objectFactory.getExtension(pt, property);            if (object != null) {              method.invoke(instance, object);            }          } catch (Exception e) {            logger.error("fail to inject via method " + method.getName()                + " of interface " + type.getName() + ": " + e.getMessage(), e);          }        }      }    }  } catch (Exception e) {    logger.error(e.getMessage(), e);  }  return instance;}

通过上面的对比我们可以看出,ExtensionFactory 仅仅是在 setter 方法注入时,用于获取对应类型的扩展的工厂。对于Dubbo SPI 本身的扩展获取,和 ExtensionFactory 并没有什么直接关系,笔者认为其主要意义在于和 Spring Bean 的打通。下面我们看一下ExtensionFactory 的实现。

图片

ExtensionFactory的实现可以看出,有3个实现类。其中 SpiExtensionFactory 里面实现是和 2.0.7 版本中的实现是一致的,也就是直接获取被注入类型的 Adaptive 扩展。SpringExtensionFactory 表示从 Spring 上下文中获取实例注入,其主要代码如下:

public <T> T getExtension(Class<T> type, String name) {  for (ApplicationContext context : contexts) {    if (context.containsBean(name)) {      Object bean = context.getBean(name);      if (type.isInstance(bean)) {        return (T) bean;      }    }  }  return null;}

上面的代码很简单,就是直接从 Spring 上下文中获取,但是 context 是怎么注入到其中的呢。对于Bean的组装,主要实在 dubbo-config 层进行组装的,这里面会监听Spring 的初始化,然后将context注入。最后一个是 AdaptiveExtensionFactory,当然也是通常使用的一个,主要原因在于其可以自动选择使用 SPI 还是Spring 的Bean,大概代码如下:

@Adaptivepublic class AdaptiveExtensionFactory implements ExtensionFactory {
private final List<ExtensionFactory> factories;
public AdaptiveExtensionFactory() { ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class); List<ExtensionFactory> list = new ArrayList<ExtensionFactory>(); // 只有 SPI 和 Spring 2个 for (String name : loader.getSupportedExtensions()) { list.add(loader.getExtension(name)); } factories = Collections.unmodifiableList(list); }
public <T> T getExtension(Class<T> type, String name) { for (ExtensionFactory factory : factories) { T extension = factory.getExtension(type, name); if (extension != null) { return extension; } } return null; }
}

可以看出AdaptiveExtensionFactory 是@Adaptive 标注的,因此factories 中只有 2 个工厂,且用TreeSet 存储的顺序为 SPI、Spring。也就是说注入 Bean 会优先使用 Dubbo SPI 的扩展,然后再使用Spring 的Bean。

3.5 Compiler 动态编译

Compiler 类也是 Dubbo 后续版本中引入的,动态编译主要用于生成 Adaptive 类时使用的。在老的版本中,是直接在ExtensionLoader 中写死的用 javassist 进行编译的。在新的版本中,做了抽象并做了一定的改进。其类图大致如下:

图片

这里面和ExtensionFactory 的抽象比较类似,其中扩展主要有 Javassist 和 JDK 2种类型。这里面需要提一下的是,AbstractCompiler 这个类中的代码,首先他会拼接出全路径名,然后尝试Class.forName()加载这个类,这里是防止重复编译。如果已经加载过,此时就直接返回了。如果不存在,则调用子类方法去编译生成的code。

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, ClassHelper.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));    }  }}

这里可以发现,原先的代码里并没有做防止重复编译的限制,而提取后则限制了。这是因为原先代码只在 ExtensionLoader 中使用,而其本身就会判断实例是否存在,也就是相当于防止了重复编译。这里面提取出来之后,可能会提供给更多的入口使用,因此做了限制。但是笔者看了下代码,也就ExtensionLoader 在生成 Adaptive 扩展时使用了。这从一定程度上看,这个抽象是不是有点鸡肋了。

4 总结

Dubbo 中的扩展能力是从 JDK 标准的 SPI 扩展点发现机制加强而来,它改进了 JDK 标准的 SPI 的加载效率和稳定性。用户能够基于 Dubbo 提供的扩展能力,可以很方便的实现:按需加载、增加扩展类的 IOC 能力、增加扩展类的 AOP 能力、具备动态选择扩展实现的能力等。从 Dubbo 扩展的设计目标可以看出,它的设计思想可以延伸到类似其他的中间件上面,比如:Apache ShenYu 的 SPI 扩展机制,就参考了 Dubbo SPI 的设计。也就是说Dubbo SPI 是一种通用的代码可扩展设计,当系统增加新功能时,不需要对现有系统的结构和代码进行修改,仅仅新增一个扩展即可。

作者介绍

风度玉门,信也科技后端研发专家

招聘信息

Java、大数据、前端、测试等各种技术岗位热招中,欢迎扫码了解~

图片

微服务 · 目录
下一篇海外新业务微服务负载均衡治理
继续滑动看下一个
拍码场
向上滑动看下一个