阅读 203

Dubbo SPI 学习笔记

Java 原生 SPI 机制会加载所有的实现类,而往往我们只需要其中一个,因此造成资源浪费。 Dubbo SPI 不仅解决了这种资源浪费,而且还进行了扩展和修改。

Dubbo 按照 SPI 配置文件的用途,将其分成了三类目录:

  1. META-INF/services/ 目录:该目录下的 SPI 配置文件用来兼容 JDK SPI 。

  2. META-INF/dubbo/ 目录:该目录用于存放用户自定义 SPI 配置文件。

  3. META-INF/dubbo/internal/ 目录:该目录用于存放 Dubbo 内部使用的 SPI 配置文件。

    Dubbo 将 SPI 配置文件改成了 KV 格式,key 为扩展名,value 为具体实现类,这样我们可以直接通过扩展名来获取指定实现。以 org.apache.dubbo.rpc.Protocol为例:

registry=org.apache.dubbo.registry.integration.InterfaceCompatibleRegistryProtocol
service-discovery-registry=org.apache.dubbo.registry.integration.RegistryProtocol
复制代码

1. 核心实现

Dubbo SPI 的核心逻辑在类 org.apache.dubbo.common.extension.ExtensionLoader 里。

1.1 @SPI 注解

@SPI 用于修饰接口,代表该接口是扩展接口,注解的 value 为默认扩展名。

/**
 * Protocol. (API/SPI, Singleton, ThreadSafe)
 */
@SPI("dubbo")
public interface Protocol {
    // 省略方法
}
复制代码

具体实现获取方式:

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("dubbo");
复制代码

1.2 LoadingStrategy

扩展类加载策略,对应上面说的三类 SPI 配置文件

  • org.apache.dubbo.common.extension.ServicesLoadingStrategy
  • org.apache.dubbo.common.extension.DubboLoadingStrategy
  • org.apache.dubbo.common.extension.DubboInternalLoadingStrategy

1.3 创建扩展类实例

org.apache.dubbo.common.extension.ExtensionLoader#createExtension 方法核心流程如下:

  1. 根据扩展名获取具体实现类
  2. 根据类从缓存获取实例,如获取成功马上返回,否则利用反射创建实例
  3. 自动注入实例对象中的属性(injectExtension),调用实例的 setter 方法进行注入
  4. 自动包装,类似装饰器模式
  5. 对实现了 org.apache.dubbo.common.context.Lifecycle 的实例进行初始化(initExtension)

1.4 @Adaptive 注解与适配器

@Adaptive 注解用来实现 Dubbo 的适配器功能。@Adaptive 注解可以修饰接口或方法。

  • 修饰接口。Dubbo 中的 ExtensionFactory 接口有三个实现类,如下图所示,ExtensionFactory 接口上有 @SPI 注解,AdaptiveExtensionFactory 实现类上有 @Adaptive 注解。AdaptiveExtensionFactory 不包含任何具体实现,只是负责选择具体哪一个 ExtensionFactory 接口实现来创建扩展实例。

image.png

  • 修饰接口方法,Dubbo 会动态生成适配器类。例如, Transporter 接口有两个被 @Adaptive 注解修饰的方法:
@SPI("netty") 
public interface Transporter { 
    @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY}) 
    RemotingServer bind(URL url, ChannelHandler handler) throws RemotingException; 
    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY}) 
    Client connect(URL url, ChannelHandler handler) throws RemotingException; 
}
复制代码

Dubbo 会生成一个 Transporter$Adaptive 适配器类,该类继承了 Transporter 接口:

public class Transporter$Adaptive implements Transporter { 
    public org.apache.dubbo.remoting.Client connect(URL arg0, ChannelHandler arg1) throws RemotingException { 
        if (arg0 == null) throw new IllegalArgumentException("url == null"); 
        URL url = arg0; 
        // 确定扩展名,优先从URL中的client参数获取,其次是transporter参数 
        // 这两个参数名称由@Adaptive注解指定,最后是@SPI注解中的默认值 
        String extName = url.getParameter("client",
            url.getParameter("transporter", "netty")); 
        if (extName == null) 
            throw new IllegalStateException("..."); 
        // 通过ExtensionLoader加载Transporter接口的指定扩展实现 
        Transporter extension = (Transporter) ExtensionLoader 
              .getExtensionLoader(Transporter.class) 
                    .getExtension(extName); 
        return extension.connect(arg0, arg1); 
    } 
    ... // 省略bind()方法 
}
复制代码

动态生成适配器类的逻辑在 org.apache.dubbo.common.extension.ExtensionLoader#createAdaptiveExtensionClass 

适配器类及其实例保存在 ExtensionLoader 类的 cachedAdaptiveClass 和 cachedAdaptiveInstance 属性里。

1.5 自动注入 injectExtension

实例创建以后 Dubbo 会对实例进行自动注入。关键逻辑在 org.apache.dubbo.common.extension.ExtensionLoader#injectExtension

满足以下条件的方法会被自动注入:

  1. 方法被 public 修饰
  2. set 开头
  3. 只有一个参数且不是基础类型
  4. 方法没有被 @DisableInject 注解修饰

Dubbo 依赖 ExtensionFactory 接口来实现注入,目前有三个实现类:

  • SpringExtensionFactory:根据属性名注入 Spring Bean
  • SpiExtensionFactory:根据扩展接口获取适配器实现
  • AdaptiveExtensionFactory:ExtensionFactory 的适配器实现

1.6 自动包装

Dubbo 将多个扩展实现类的公共逻辑,抽象到“包装类”中,“包装类”与普通的扩展实现类一样,也实现了扩展接口,在获取真正的扩展实现对象时,在其外面包装一层“包装类”对象,可以理解成一层装饰器。

判断是“包装类”的逻辑:包含的构造函数只有一个参数且为扩展接口类型

private boolean isWrapperClass(Class<?> clazz) {
    try {
        clazz.getConstructor(type);
        return true;
    } catch (NoSuchMethodException e) {
        return false;
    }
}
复制代码

包装的关键逻辑:

List<Class<?>> wrapperClassesList = new ArrayList<>();
if (cachedWrapperClasses != null) {
    wrapperClassesList.addAll(cachedWrapperClasses);
    wrapperClassesList.sort(WrapperComparator.COMPARATOR);
    Collections.reverse(wrapperClassesList);
}

if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
    for (Class<?> wrapperClass : wrapperClassesList) {
        Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
        if (wrapper == null
            || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
            instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
        }
    }
}
复制代码

1.7 @Activate注解与自动激活特性

利用这个特性可以根据特定的URL请求情况返回特定的扩展。

扫描类时 Dubbo 把被 @Activate 注解修饰的实现类缓存到集合 cachedActivates 里。 @Activate 注解标注在扩展实现类上,有 group、value 以及 order 三个属性。

  • group 属性:修饰的实现类是在 Provider 端被激活还是在 Consumer 端被激活。

  • value 属性:修饰的实现类只在 URL 参数中出现指定的 key 时才会被激活。

  • order 属性:用来确定扩展实现类的排序。

关键逻辑在 org.apache.dubbo.common.extension.ExtensionLoader#getActivateExtension(org.apache.dubbo.common.URL, java.lang.String[], java.lang.String) 

public List<T> getActivateExtension(URL url, String[] values, String group) {
    ...
}
复制代码

获取扩展流程:

  1. 获取默认激活的扩展集合。遍历 cachedActivates ,获取出满足以下条件的扩展,添加到 activateExtensions 集合里。
    1. 扩展上的 @Activate 注解指定的 group 属性与当前 group 匹配
    2. 扩展名没有出现在入参 values 中(既未在入参 values 中明确指定,也未在入参 values 中明确指定删除,删除使用 “-扩展名” 来表示)
    3. 扩展上的 @Activate 注解指定的 value 属性与 URL 中的出现的请求参数匹配,具体判断逻辑在方法 ExtensionLoader#isActive
  2. 根据指定入参 values 获取扩展
    1. 如果 values 包含 default,那么以 default 作为分界线,把位于 default 前的扩展名对应的扩展放在 activateExtensions 集合的前面,其余放到后面。
    2. 如果 values 不包含 default,那么把扩展都放在 activateExtensions 集合的后面。

最后举个简单的例子说明上述处理流程,假设 cachedActivates 集合缓存的扩展实现如下表所示: 在 Provider 端调用 getActivateExtension() 方法时传入的 values 配置为 "demoFilter3、-demoFilter2、default、demoFilter1",那么根据上面的逻辑:

  1. 得到默认激活的扩展实实现集合中有 [ demoFilter4, demoFilter6 ];

  2. 排序后为 [ demoFilter6, demoFilter4 ];

  3. 按序添加自定义扩展实例之后得到 [ demoFilter3, demoFilter6, demoFilter4, demoFilter1 ]。

参考

Dubbo SPI 精析,接口实现两极反转(下)

Dubbo 2.7 源码

文章分类
后端
文章标签