Dubbo SPI 机制源码分析(基于2.7.7)

Dubbo SPI 机制涉及到 @SPI@Adaptive@Activate 三个注解,ExtensionLoader 做为 Dubbo SPI 机制的核心负责加载和管理扩展点及其实现。本文以 ExtensionLoader 的源码做为分析主线,进而引出三个注解的做用和工做机制。java

ExtensionLoader 被设计为只能经过 getExtensionLoader(Class<T> type) 方法获取到实例,参数 type 表示拿到的这个实例要负责加载的扩展点类型。为了不在以后的源码分析中产生困惑,请先记住这个结论:每一个 ExtensionLoader 只能加载其绑定的扩展点类型(即 type 的类型)的具体实现。也就是说,若是 type 的值是 Protocol.class,那么这个 ExtensionLoader 的实例就只能加载 Protocol 接口的实现,不能去加载 Compiler 接口的实现。apache

怎么获取扩展实现

在 Dubbo 里,若是一个接口标注了 @SPI 注解,那么它就表示一个扩展点类型,这个接口的实现就是这个扩展点的实现。好比 Protocol 接口的声明:数组

@SPI("dubbo")
public interface Protocol {}
复制代码

一个扩展点可能存在多个实现,可使用 @SPI 注解的 value 属性指定要选择的默认实现。当用户没有明确指定要使用哪一个实现时,Dubbo 就会自动选择这个默认实现。缓存

getExtension(String name) 方法能够获取指定名称的扩展实现的实例,这个扩展实现的类型必须是当前 ExtensionLoader 绑定的扩展类型。这个方法会先查缓存里是否有这个扩展实现的实例,若是没有再经过 createExtension(String name) 方法建立实例。Dubbo 在这一块设置了多层缓存,进入 createExtension(String name) 方法后又会调用 getExtensionClasses() 方法拿到当前 ExtensionLoader 已加载的全部扩展实现。若是还拿不到,那就调用 loadExtensionClasses() 方法真的去加载了。markdown

private Map<String, Class<?>> loadExtensionClasses() {
  // 取 @SPI 注解上的值(只容许存在一个值)保存到 cachedDefaultName
  cacheDefaultExtensionName();
  Map<String, Class<?>> extensionClasses = new HashMap<>();
  // 不一样的策略表明不一样的目录,迭代进行加载
  for (LoadingStrategy strategy : strategies) {
    // loadDirectory(...)
    // 执行不一样策略
  }
  return extensionClasses;
}
复制代码

cacheDefaultExtensionName() 方法会从当前 ExtensionLoader 绑定的 type 上去获取 @SPI 注解,并将其 value 值保存到 ExtensionLoader 的 cachedDefaultName 字段用来表示扩展点的默认扩展实现的名称。app

SPI 配置的加载策略

接着迭代三种扩展实现加载策略。strategies 是经过 loadLoadingStrategies() 方法加载的,在这个方法里已经对三种策略进行了优先级排序,排序规则是低优先级的策略放在前面。简单看一下 LoadingStrategy 接口:框架

public interface LoadingStrategy extends Prioritized {
    String directory();
    default boolean preferExtensionClassLoader() {
        return false;
    }
    default String[] excludedPackages() {
        return null;
    }
    default boolean overridden() {
        return false;
    }
}
复制代码

overridden() 方法表示当前策略加载的扩展实现是否能够覆盖比其优先级低的策略加载的扩展实现,优先级由 Prioritized 接口控制。为了在加载扩展实现时可以方便的进行覆盖操做,对加载策略进行预先排序就很是重要。这也是 loadLoadingStrategies() 方法要排序的缘由。ide

查找和解析 SPI 配置文件

loadDirectory() 方法在当前策略指定的目录下查找 SPI 配置文件并加载为 java.net.URL 对象,接下来 loadResource() 方法对配置文件进行逐行解析。Dubbo SPI 的配置文件是 key=value 形式,key 表示扩展实现的名称,value 是扩展实现的具体类名,这里直接 split 后对扩展实现进行加载,最后交给 loadClass() 方法处理。工具

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name, boolean overridden) throws NoSuchMethodException {
  if (!type.isAssignableFrom(clazz)) {
    throw new IllegalStateException("...");
  }
  // 适配类
  if (clazz.isAnnotationPresent(Adaptive.class)) {
    cacheAdaptiveClass(clazz, overridden);
  } else if (isWrapperClass(clazz)) { // 包装类
    cacheWrapperClass(clazz);
  } else {
    clazz.getConstructor(); // 检查点:扩展类必需要有一个无参构造器
    // 兜底策略:若是配置文件没有按 key=value 这样写,就取类的简单名称做为 key,即 name
    if (StringUtils.isEmpty(name)) {
      name = findAnnotationName(clazz);
      if (name.length() == 0) {
        throw new IllegalStateException("..." + resourceURL);
      }
    }

    String[] names = NAME_SEPARATOR.split(name);
    if (ArrayUtils.isNotEmpty(names)) {
      // 若是当前实现类标注了 @Activate 则缓存
      cacheActivateClass(clazz, names[0]);
      // 扩展实现能够用逗号分隔取不少名字(a,b,c=com.xxx.Yyy),这里迭代全部名字作缓存
      for (String n : names) {
        // 缓存 扩展实现的实例 -> 名称
        cacheName(clazz, n);
        // 缓存 名称 -> 扩展实现的实例
        saveInExtensionClass(extensionClasses, clazz, n, overridden);
      }
    }
  }
}
复制代码

cacheAdaptiveClass() 方法是对 @Adaptive 的处理,这个稍后会介绍。源码分析

包装类

来看 isWrapperClass() 方法,这个方法用来判断当前实例化的扩展实现是否为包装类。判断条件很是简单,只要某个类具备一个只有一个参数的构造器,且这个参数的类型和当前 ExtensionLoader 绑定的扩展类型一致,这个类就是包装类

在 Dubbo 中包装类都是以 Wrapper 结尾,好比 QosProtocolWrapper:

public class QosProtocolWrapper implements Protocol {
  private Protocol protocol;
	// 包装类必要的构造器
  public QosProtocolWrapper(Protocol protocol) {
    if (protocol == null) {
      throw new IllegalArgumentException("protocol == null");
    }
    this.protocol = protocol;
  }
  
  @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        if (UrlUtils.isRegistry(invoker.getUrl())) { // 一些额外的逻辑
            startQosServer(invoker.getUrl());
            return protocol.export(invoker);
        }
        return protocol.export(invoker);
    }

    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        if (UrlUtils.isRegistry(url)) { // 一些额外的逻辑
            startQosServer(url);
            return protocol.refer(type, url);
        }
        return protocol.refer(type, url);
    }
}
复制代码

能够看到,Dubbo 中的包装类实际上就是 AOP 的一种实现,而且多个包装类能够不断嵌套,相似 Java I/O 类库的设计。回到 loadClass() 方法,若是当前是包装类,则放入 cachedWrapperClasses 集合中保存。

兜底不标准的 SPI 配置文件

loadClass() 方法的最后一个 else 分支中,首先去获取了一次当前扩展实现的无参构造器,由于以后实例化扩展实现的时候须要这个构造器,这里等因而提早作了一个检查。而后是作兜底操做,由于 SPI 配置文件可能没有按照 Dubbo 的要求写成 key=value 形式,那么就把扩展实现类的类名做为 keycacheActivateClass() 方法用于判断当前扩展实现是否携带了 @Activate 注解,若是有则缓存,这个注解的用处后文会详述。

扩展实现及其名称的多种缓存

最后把扩展实现的名称和扩展实现的 Class 对象进行双向缓存。cacheName() 方法作 Class 对象到扩展实现名称的映射,saveInExtensionClass() 是作扩展实现名称到 Class 对象的映射。

saveInExtensionClass() 方法的参数 overridden 实际就是来自于加载策略 LoadingStrategy 的 overridden() 方法。上文提到过三个加载策略是在迭代时是按照优先级从小到大顺序进行的,因此只要当前的 LoadingStrategy 容许覆盖以前策略建立的扩展实现,那么这里 overridden 就为 true

到了这里实际上就是 loadExtensionClasses() 方法的所有执行逻辑,当方法执行完成后当前 ExtensionLoader 所绑定的扩展类型的全部实现类就所有被加载成了 Class 对象并放入了 cachedClasses 中。

实例化扩展实现

再往上返回到 createExtension(String name) 中,若是在已加载的扩展实现类里找不到当前要获取扩展实现则抛出异常。接着尝试从缓存中获取一下对应的实例,若是没有则实例化并放入缓存。injectExtension() 方法就是经过反射将当前实例化出来的扩展实现所依赖的其余扩展实现也初始化并赋值。

这里用到一个 ExtensionFactory objectFactory,AdaptiveExtensionFactory 做为 ExtensionFactory 的适配实现,对 SpiExtensionFactory 和 SpringExtensionFactory 进行了适配。当要获取一个扩展实现时,都是调用 AdaptiveExtensionFactory 的 getExtension(Class<T> type, String name) 方法。

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;
}
复制代码

这个方法分别尝试调用两个具体实现的 getExtension() 方法来获取扩展实现。SpiExtensionFactory 是从 Dubbo 本身的容器里查找扩展实现,实际就是调用 ExtensionLoader 的方法来实现,算是一个门面。SpringExtensionFactory 顾名思义就是从 Spring 容器内查找扩展实现,毕竟不少时候 Dubbo 都是配合着 Spring 在使用。

回到 createExtension(String name) 方法继续往下看,接下来是迭代在加载扩展实现时保存的包装类,滚动将上一个包装完的实例做为下一个包装类的构造器参数进行包装,也就是说最终拿到的扩展实现的实例是最后一个包装类的实例。最后的最后,若是扩展实现有 Lifecycle 接口,则调用其 initialize() 方法初始化生命周期。至此,一个扩展实现就被建立出来了!

怎么选择要使用的扩展实现

loadClass() 方法中提到过,若是加载的扩展实现带有 @Adaptive 注解,cacheAdaptiveClass() 方法将会把这个扩展实现按照加载策略的覆盖(overridden)设置赋值给 cachedAdaptiveClass

@Adaptive 的做用

Dubbo 中的扩展点通常都具备不少个扩展实现,简单说就是一个接口存在不少个实现。但接口是不能被实例化的,因此要在运行时找一个具体的实现类来实例化。 @Adaptive 是用来在运行时决定选择哪一个实现的。若是标注在类上就表示这个类是适配类,加载扩展实现的时候直接赋值给 ExtensionLoader 的 cachedAdaptiveClass 字段便可,例如上文讲到的 AdaptiveExtensionFactory。

因此这里简单总结一下,所谓适配类就是在实际使用扩展点的时候用来选择具体的扩展实现的那个类

@Adaptive 也能够标注在接口方法上,表示这个方法要在运行时经过字节码生成工具动态生成方法体,在方法体内选择具体的实现来使用,好比 Protocol 接口:

@SPI("dubbo")
public interface Protocol {
  @Adaptive
  <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
 	@Adaptive
  <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
}
复制代码

很明显,Protocol 的每一个实现都有本身暴露服务和引用服务的逻辑,若是直接根据 URL 去解析要使用的协议并实例化显然不是一个好的选择。做为一个 Spring 应用工程师,应该马上想到 IoC 才是人间正道。Dubbo 的开发者(可能)也是这么想的,可是本身搞一套 IoC 出来又好像不是太合适,因而就经过了字节码加强的方式来实现。

动态适配类的建立

若是一个扩展点的全部实现类上都没有携带 @Adaptive 注解,可是扩展点的某些方法上带了 @Adaptive 注解,这就表示 Dubbo 须要在运行时使用字节码加强工具动态的建立一个扩展点的代理类,在代理类的同名方法里选择具体的扩展实现进行调用。

这么说有点抽象,咱们来看 ExtensionLoader 的 getAdaptiveExtension() 方法。这个方法获取当前 ExtensionLoader 绑定的扩展点的适配类,首先从 cachedAdaptiveInstance 上尝试获取,这个字段保存的是上文提到的 cachedAdaptiveClass 实例化的结果。若是获取不到,通过双重检查锁后调用 createAdaptiveExtension() 方法进行适配类的建立。

createAdaptiveExtension() 方法又调用 getAdaptiveExtensionClass() 方法拿到适配类的 Class 对象,即上文提到的 cachedAdaptiveClass,而后将 Class 实例化后调用 injectExtension() 方法进行注入。

getAdaptiveExtensionClass() 方法发现 cachedAdaptiveClass 没有值后转而调用 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);
}
复制代码

首先调用 AdaptiveClassCodeGenerator 类的 generate() 方法把适配类生成好,而后也是走 SPI 机制拿到须要的 Compiler 的适配类执行编译,最后把编译出来的适配类的 Class 对象返回。

Dubbo 使用 javassist 框架来动态生成适配类,AdaptiveClassCodeGenerator 类的 generate() 方法实际就是作的适配类文件的字符串拼接。具体的生成逻辑没有什么好讲的,都是些字符串操做,这里简单写个示例:

@SPI
interface TroubleMaker {
    @Adaptive
    Server bind(arg0, arg1);
  
    Result doSomething();
}
public class TroubleMaker$Adaptive implements TroubleMaker {
  
  	public Result doSomething() {
      throw new UnsupportedOperationException("The method doSomething of interface TroubleMaker is not adaptive method!");
    }
  
    public Server bind(arg0, arg1) {
        TroubleMaker extension =
            (TroubleMaker) ExtensionLoader
                .getExtensionLoader(TroubleMaker.class)
                .getExtension(extName);
        return extension.bind(arg0, arg1);
    }
}
复制代码

假设有个扩展点叫 TroubleMaker,那么动态生成的适配类就叫作 TroubleMaker$Adaptive,适配类对没有标注 @Adaptive 注解的方法会直接抛出异常,而使用了 @Adaptive 注解的方法内部实际是经过 ExtensionLoader 去找到要使用的具体的扩展实现,再调用这个扩展实现的同名方法。

扩展实现的选择遵循如下逻辑:

  • 读取 @Adaptive 注解的 value 属性,若是 value 没有值则把当前扩展点接口名转换为「点分隔」形式,好比 TroubleMaker 转换为 trouble.maker。而后用这个做为 key 从 URL 上去获取要使用的具体扩展实现。
  • 若是上一步没有获取到,则取扩展点接口上的 @SPI 注解的 value 值做为 key 再去 URL 上获取。

怎么启用扩展实现

有些扩展点的扩展实现是能够同时使用多个的,而且能够按照实际需求来启用,好比 Filter 扩展点的众多扩展实现。这就带来两个问题,一个是怎么启用扩展,另外一个是扩展是否能够启用。Dubbo 提供了 @Activate 注解来标注扩展的启用条件。

public @interface Activate {
  String[] group() default {};
  String[] value() default {};
}
复制代码

众所周知 Dubbo 分为客户端和服务端两侧,group 用来指定扩展能够在哪一端启用,取值只能是 consumerprovider,对应的常量位于 CommonConstants。value 用来指定扩展实现的开启条件,也就是说若是 URL 上能经过 getParameter(value) 方法获取一个不为空(即不为 false0nullN/A)的值,那么这个扩展实现就会被启用。

例如存在一个 Filter 扩展点的扩展实现 FilterX:

@Activate(group = {CommonConstants.PROVIDER}, value = "x")
public class FilterX implements Filter {}
复制代码

若是当前是服务端一侧在加载扩展实现,而且 url.getParameter("x") 能拿到一个不为空的值,那 FilterX 这个扩展实现就会被启用。须要注意的是,@Activate 的 value 属性的值不须要和 SPI 配置文件里的 key 保持一致,而且 value 能够是个数组

启用扩展实现的方式

第一种启用方式就是上文所讲的让 value 做为 url 的 key 而且值不为空,另外一种扩展实现的启用就要回到 ExtensionLoader 的 getActivateExtension(URL url, String key, String group) 方法。

参数 key 表示一个存在于 url 上的参数,这个参数的值指定了要启用的扩展实现,多个扩展实现之间用逗号分隔,参数 group 表示当前是服务端一侧仍是客户端一侧。这个方法把经过参数 key 获取到的值拆分后调用了重载方法 getActivateExtension(URL url, String[] values, String group),这个方法就是扩展实现启用的关键点所在。

首先是判断要开启的扩展实现名称列表里有没有 -default,这里的 - 是减号,是「去掉」的意思,default 表示默认开启的扩展实现,因此 -default 的意思就是要去掉默认开启的扩展实现。所谓默认开启的扩展实现,其实就是携带了 @Activate 注解可是注解的 value 没有值的那些扩展实现,好比 ConsumerContextFilter。以此推论,若是扩展实现的名称前带了 - 就表示这个扩展实现不开启

若是没有 -default 接着就是迭代 cachedActivates 去判断哪些扩展实现是须要使用的,关键方法是 isActive(String[] keys, URL url)。这个方法在源码里没有注释,理解起来可能有些困难。实际上就是判断传入的这些 keys 是否在 url 上存在。

这里有个骚操做,cachedActivates 保存的是「扩展实现名称」到「@Aactivate」注解的映射,也就是这个 mapvalue 不是扩展实现的 Class 对象或者实例。由于 cachedClassescachedInstances 已经分别保存了二者,只要有扩展实现的名字就能够获取到,没有必要多保存一份。

回到方法的另一个分支,若是有 -default,那就是只开启 url 上指定的扩展实现,同时处理一下携带了 - 的名称。方法最后把全部要开启的扩展实现放入 activateExtensions 集合返回。

启用扩展实现的示例

我的认为 Dubbo SPI 这一块适合采用视频的方式进行源码分析,由于这里面有不少逻辑是相互牵连的,依靠文字不太容易讲的明白。因此这里用一个示例来展现上文讲到的扩展实现启用逻辑。假设如今存在如下 5 个自定义 Filter:

public class FilterA implements Filter {}

@Activate(group = {CommonConstants.PROVIDER}, order = 2)
public class FilterB implements Filter {}

@Activate(group = {CommonConstants.CONSUMER}, order = 3)
public class FilterC implements Filter {}

@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER}, order = 4)
public class FilterD implements Filter {}

@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER}, order = 5, value = "e")
public class FilterE implements Filter {}
复制代码

配置文件 META-INF/dubbo/internal/org.apache.dubbo.rpc.Filter

fa=org.apache.dubbo.rpc.demo.FilterA
fb=org.apache.dubbo.rpc.demo.FilterB
fc=org.apache.dubbo.rpc.demo.FilterC
fd=org.apache.dubbo.rpc.demo.FilterD
fe=org.apache.dubbo.rpc.demo.FilterE
复制代码

首先直接查找消费者端(Consumer)可使用的 Filter 扩展点的扩展实现:

public static void main(String[] args) {
  ExtensionLoader<Filter> extensionLoader = ExtensionLoader.getExtensionLoader(Filter.class);
  URL url = new URL("", "", 10086);
  List<Filter> activate = extensionLoader.getActivateExtension(url, "", CommonConstants.CONSUMER);
  activate.forEach(a -> System.out.println(a.getClass().getName()));
}
// 输出
// org.apache.dubbo.rpc.filter.ConsumerContextFilter
// org.apache.dubbo.rpc.demo.FilterC
// org.apache.dubbo.rpc.demo.FilterD
复制代码

能够看到自定义扩展实现里的 C 和 D 被启用。A 因为没有 @Activate 注解不会默认启用,B 限制了只能在服务端(Provider)启用,E 的 @Activate 注解的 value 属性限制了 URL 上必须存在名叫 e 的参数能够被启用。

接下来添加参数尝试让 E 被启用:

URL url = new URL("", "", 10086).addParameter("e", (String) null);
// 输出
// org.apache.dubbo.rpc.filter.ConsumerContextFilter
// org.apache.dubbo.rpc.demo.FilterC
// org.apache.dubbo.rpc.demo.FilterD
复制代码

能够看到 E 仍是没被启用,这是由于虽然 URL 上存在了名为 e 的参数,可是值为空,不符合启用规则,这时候只要把值调整为任何不为空(即不为 false0nullN/A)的值就能够启用 E 了。

换另外一种方式启用 E:

URL url = new URL("", "", 3).addParameter("filterValue", "fe");
List<Filter> activate = extensionLoader.getActivateExtension(url, "filterValue", CommonConstants.CONSUMER);
// 输出
// org.apache.dubbo.rpc.filter.ConsumerContextFilter
// org.apache.dubbo.rpc.demo.FilterC
// org.apache.dubbo.rpc.demo.FilterD
// org.apache.dubbo.rpc.demo.FilterE
复制代码

添加参数 filterValue 并指定值为 fe,这里的值要和 SPI 配置文件里的 key 保持一致。调用 getActivateExtension() 方法时指定这个参数的名字,这时就能够看到 E 被启用了。

接下来试试去掉默认开启的扩展实现并指定 A 启用:

URL url = new URL("", "", 3).addParameter("filterValue", "fa,-default");
List<Filter> activate = extensionLoader.getActivateExtension(url, "filterValue", CommonConstants.CONSUMER);
// 输出
// org.apache.dubbo.rpc.demo.FilterA
复制代码

加上 -default 后 ConsumerContextFilter 和 C 、D 被禁用了,由于他们是默认开启的实现。再回忆一次,默认开启的扩展实现其实就是携带了 @Activate 注解可是注解的 value 没有值的那些扩展实现。尽管 A 没有携带 @Activate 注解,可是这里指定了须要启用,因此 A 被启用。

最后

好了,终于分析完了 Dubbo 的这一套 SPI 机制,其实也不算太复杂,只是逻辑绕了一点,有机会我会将本文录制为视频讲解,但愿能让你们有更好的理解。