:::warning 针对多实例特性,需要进行二次更新….. :::

从 Dubbo 2.0 到 3.0 的演变的过程中,提出了轻核心,重拓展的理念,大量原本集成在框架内的模块被移到了拓展之中。

Dubbo 的拓展由 org.apache.dubbo.common.extension.ExtensionLoader 泛型类负责,该类不提供对外的构造函数,获取示例需要 getExtensionLoader 静态工厂方法获取,且内部通过缓存机制来保证相同类型的实现类只会存在一份。

ExtensionLoader 类上,有两个核心方法:

  • getAdaptiveExtension - 针对同类型拓展(例如 Protocol),提供一个访问适配器。
  • getExtension - 加载真实的拓展类实例

getAdaptiveExtension - 拓展动态适配器

其中 getAdaptiveExtension 最难理解,接下来逐个分析: 「Dubbo」3.0 Dubbo SPI 拓展管理 - 图1

SPI 注解

cacheDefaultExtensionName 函数会读取接口类上的 @SPI 注解值,来确定该拓展的默认实现是什么,例如:

基类接口 说明
@SPI("dubbo")
org.apache.dubbo.rpc.Protocol
默认协议是 Dubbo
@SPI("netty")
org.apache.dubbo.remoting.Transporter
默认网络通道是 Netty
@SPI("javassist")
org.apache.dubbo.common.compiler.Compiler
默认编译器是 javassist

@SPI 注解还有另外一个作用,标记该类型拓展是否可以通过 Dubob SPI 机制拓展:

  1. // => org.apache.dubbo.common.extension.ExtensionLoader#getExtensionLoader
  2. // ExtensionLoader 只加载 @SPI 标记的拓展实现
  3. if (!withExtensionAnnotation(type)) {
  4. throw new IllegalArgumentException("Extension type (" + type +
  5. ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
  6. }
  7. // => org.apache.dubbo.common.extension.ExtensionLoader#withExtensionAnnotation
  8. private static <T> boolean withExtensionAnnotation(Class<T> type) {
  9. return type.isAnnotationPresent(SPI.class);
  10. }

创建拓展动态适配器

回到 loadExtensionClasses 方法,这里相当于提前把实现类加载到 JVM 之中了,供后续的 getExtension 方法使用,下节再做展开。

进入 createAdaptiveExtensionClass 方法:

private Class<?> createAdaptiveExtensionClass() {
    ClassLoader classLoader = findClassLoader();

    // 为了支持 GraalVM 的 native 编译,所以此处把动态代理类最后生成的代码直接放进了代码库
    // 但同时也导致了灵活性问题。
    if (ApplicationModel.getEnvironment().getConfiguration().getBoolean(NATIVE, false)) {
        try {
            return classLoader.loadClass(type.getName() + "$Adaptive");
        } catch (ClassNotFoundException e) {
            // 此处不应该放行,自动生成代理类逻辑在 Native 环境下无法正常执行
            // 已经反馈给社区
            //ignore
            e.printStackTrace();
        }
    }

    String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
    //=> 这里有鸡和蛋的问题
    org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    return compiler.compile(code, classLoader);
}

最后一段,假如 compiler 需要 compiler 自己的适配器呢?那是先有鸡还是先有蛋呢?
Dubbo 做法是,当 @Adaptive 注解标记在类上的时候,这么这个适配器是不需要动态编译的.
具体代码为在 loadDirectory 的子过程 loadClass 中:

// => org.apache.dubbo.common.extension.ExtensionLoader#loadClass
// 如果发现 Adaptive 标记的代理类,则直接加载并缓存
if (clazz.isAnnotationPresent(Adaptive.class)) {
    cacheAdaptiveClass(clazz, overridden);
}

// => org.apache.dubbo.common.extension.ExtensionLoader#getAdaptiveExtensionClass
// 执行完 getExtensionClasses 方法后,cachedAdaptiveClass 已经被设置值了,不会再进入动态
// 编译代码的阶段了。
private Class<?> getAdaptiveExtensionClass() {
    getExtensionClasses(); 
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

回到编译器,Dubbo 在编译器拓展里面提供了三个实现,定义文件在 dubbo-common/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.common.compiler.Compiler

类实现 说明
AdaptiveCompiler� 编译器路由,会选择调用 JDK 或者 Javassist 实现功能,默认是 Javassist
JdkCompiler Dubbo 官方基于 JDK 自带的 JavaCompiler 封装的字节码注入工具类
JavassistCompiler 在运行阶段,通过 Javassist 框架进行字节码注入

此处先不深究字节码注入细节,我们需要注意的是 Dubbo SPI 注入的类,是被自定义 ClassLoader 加载的,通过 Launcher$AppClassLoader 类加载器,是获取不到的(假如该类文件在 AppClassLoader 的 classpath 里面存在,那获取到的也不是同一个类,使用 == 判断是 false,也不可混用 instance of 判定)。
相对的,默认情况下,通过 Java SPI 中的注入类是被 Launcher$AppClassLoader 加载的,是可以获取到的。

适配器简化模型

最后生成的拓展动态适配器可归结为如下伪代码:

// 在接口定义上标记 @SPI 注解,代表通过 Dubbo SPI 加载
// 并指定默认实现类
@SPI("DefaultExtensionName")
public interface YourInterface {
    Result methodA(Param);
}

// 其中一个实现类
public YourInterfaceImpl implements {
    @Override
    Result methodA(Param) {
        // Do somethings
        return Result;
    }
}


// 主角登场,动态拓展适配器,在 Dubbo 中是由
public YourInterface$Adaptive implements YourInterface {
    @Override
    Result methodA(Param) {
        // 从入参里面提取需要加载具体实现类的名字
        String extName = ExtractImplClassNameFrom(param);

        // 通过类名,获取最终实现类的实例
        YourInterface actualExtension = getExtension(extName);

        return actualExtension.methodA(Param);
    }
}

其中 getExtension 的具体实现,就是我们下一小节要讲的。

getExtension - 获取拓展实例

在 Dubbo 框架中,为了灵活性会通过 AdaptiveExtension 动态路由到 getExtension 来获取最终的实现类实例。

至此,我们会有一个疑问,Dubbo 为什么不直接使用 JDK SPI 而非得自定义一套增强的 Dubbo SPI?官方给出的理由如下,接下来我们将依此进行验证:

  1. 可以支持 K/V 形式的实现类定义,加快后续检索
  2. 可以通过 Dubbo IoC 进行依赖注入增强

前文提到,创建拓展动态适配器的时候,会提前加载实现类,其中核心方法是 loadExtensionClasses,在 Dubbo SPI 中,该方法在功能目标上对标 JDK SPI 提供的 ServiceLoader.load 方法。 「Dubbo」3.0 Dubbo SPI 拓展管理 - 图2

实现类加载策略

在该方法中,会依据 strategies 制定的加载策略来获取特定实现

// => org.apache.dubbo.common.extension.ExtensionLoader#loadExtensionClasses
private Map<String, Class<?>> loadExtensionClasses() {
    cacheDefaultExtensionName();

    Map<String, Class<?>> extensionClasses = new HashMap<>();

    for (LoadingStrategy strategy : strategies) {
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        // 当初 Dubbo 捐献给 Apache 基金会的时候,包域名限定改成了 org.apache
        // 此处是为了兼容历史代码
        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
    }

    return extensionClasses;
}

而有意思的是,strategies 是通过 JAVA 标准的 SPI 机制加载的(非 Dubbo SPI),加载的配置文件为 dubbo-common 子模块下的 org.apache.dubbo.common.extension.LoadingStrategy 文件,默认定义了三个策略,如果不同目录的类实现定义存在冲突,则优先使用高优先级目录的类实现定义。优先级的实现可简单概括为:抢占式加载,禁止覆盖,高优先级策略优先加载。

策略类 策略配置:优先级/目录 备注
DubboInternalLoadingStrategy 高优先级
META-INF/dubbo/internal/
Dubbo 内部核心拓展
DubboLoadingStrategy 中优先级
META-INF/dubbo/
用户定义的 Dubbo 拓展
ServicesLoadingStrategy 低优先级
META-INF/services/
标准 JAVA SPI 目录

执行实现类加载(非实例化)

走到 loadDirectory 方法,该方法支持解析 2 种格式的配置文件
1)KV 形式
Dubbo SPI 特有,示例如下,最终会映射为实现类的 Map 集合

// 示例如下
// => dubbo-rpc/dubbo-rpc-api/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol

listener=org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=org.apache.dubbo.rpc.support.MockProtocol

2)兼容 JAVA SPI 配置

// 示例文件(实际应用中,Dubbo 是通过 JAVA SPI 加载的该策略文件,但 Dubbo SPI 也能处理)
// => dubbo-common/src/main/resources/META-INF/services/org.apache.dubbo.common.extension.LoadingStrategy

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

兼容 JAVA SPI 明显需要解决一个问题,如何获取类的名字,作为 Map 容器的 KEY 值,这个答案在这 findAnnotationName 可以找到。如果获取名字失败,将会抛出异常,终止加载。

// => org.apache.dubbo.common.extension.ExtensionLoader#findAnnotationName
// 该方法其实也有历史包袱,@Extension 这个注解其实已经废弃
private String findAnnotationName(Class<?> clazz) {
    // 一开始是尝试从 @Extension 注解获取类名的代码,已经标记 @Deprecated,省略相关代码
       // ......

    String name = clazz.getSimpleName();
    if (name.endsWith(type.getSimpleName())) {
        // 实现类名移除基类类名尾缀,为该实现类的名字
        // 例如 DubboInternalLoadingStrategy 实现了 LoadingStrategy,则移除现在尾缀后
        // 最终 name 值为 DubboInternal
        name = name.substring(0, name.length() - type.getSimpleName().length());
    }
    return name.toLowerCase();
}

接下来聊聊配置解析成功后,实现类是如何加载的,核心关注 LoadClass 方法:

// => org.apache.dubbo.common.extension.ExtensionLoader#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("Error occurred when loading extension class (interface: " +
                                        type + ", class line: " + clazz.getName() + "), class "
                                        + clazz.getName() + " is not subtype of interface.");
    }

    if (clazz.isAnnotationPresent(Adaptive.class)) {
        // 前文已经讲到,鸡和蛋的问题,不再重复
        cacheAdaptiveClass(clazz, overridden);
    } else if (isWrapperClass(clazz)) {
        // 包装类
        cacheWrapperClass(clazz);
    } else {
        // 普通实现类,需要带有一个公开的无参构造器
        clazz.getConstructor();
        // 前文提到的解析 JAVA SPI 配置文件时候的 name 补全策略
        if (StringUtils.isEmpty(name)) {
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
            }
        }

        // 逗号分隔的拓展实现类名,所以运行存在这样的配置:name1,name2=com.foo.XxxProtocol
        String[] names = NAME_SEPARATOR.split(name);
        if (ArrayUtils.isNotEmpty(names)) {
            // 带生效条件的实现类,由 @Activate 注解标记生效条件
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                cacheName(clazz, n);
                // 保持到拓展类缓存容器中,注意此处 overridden 是 false
                // 不然前文提到加载策略优先级会失效(破坏了抢占式加载)
                saveInExtensionClass(extensionClasses, clazz, n, overridden);
            }
        }
    }
}

最终的,拓展实现类可以总结为三大类型:

类型 判定条件
AdaptiveClass @Adaptive 注解标记类
在 Dubbo 3.0 内,目前依旧只有两个类标记
- AdaptiveCompiler
- AdaptiveExtensionFactory


对应的缓存字段:
Class<?> ExtensionLoader#cachedAdaptiveClass
WrapperClass 包装类:拥有一个公开的构造函数,有且只有一个类型为 type 接口类的参数。
例如 ProtocolListenerWrapper 的构造函数
public ProtocolListenerWrapper(Protocol protocol) 就符合这个约定

对应的缓存字段:
Set<Class<?>> ExtensionLoader#cachedWrapperClasses
ExtensionClass 普通实现类,需要带有一个公开的无参构造器,内部细分为
- 附带生效条件的实现类,由 @Activate 注解标记生效条件
- 真真普通的实现类


对应的缓存字段:
Holder<Map<String, Class<?>>> ExtensionLoader#cachedClasses

Dubbo IOC 注入(实例化类)

注意到此为此,我们完成的是“实现类”的加载和缓存构建,此时还不存在实现类实例。接下来,我们开始探讨“实现类实例”是如何创建的,此处重点关注 Dubbo IoC 注入过程。 「Dubbo」3.0 Dubbo SPI 拓展管理 - 图3 进入 createExtension 核心方法:

// => org.apache.dubbo.common.extension.ExtensionLoader#createExtension
private T createExtension(String name, boolean wrap) {
    // ......
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            // 前文提到的普通拓展实现类,必须拥有一个无参构造器
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }

        // Dubbo IOC
        injectExtension(instance);


        if (wrap) {
            // 典型的职责链模式的应用,稍后会有详细分析
            // ......
        }

        // 如果实例的超类是 org.apache.dubbo.common.context.Lifecycle
        // 则触发生命周期的 initialize 钩子
        initExtension(instance);

        // 实例创建一次后,将会被缓存起来,所以通过 Dubbo IOC 获取的实例本质上都是单例
        return instance;
    } // ......
}

进入 injectExtension,我们惊讶的发现,Dubbo IOC 的依赖注入代码相当清晰:

private T injectExtension(T instance) {
    // 理解 objectFactory 是难点重点,后文单独说明
    if (objectFactory == null) {
        return instance;
    }

    try {
        for (Method method : instance.getClass().getMethods()) {
            // 只针对 Setter 方法进行依赖注入
            if (!isSetter(method)) {
                continue;
            }
            /**
             * Check {@link DisableInject} to see if we need auto injection for this property
             */
            // 可以手动标记忽略某个 Setter 注入方法
            if (method.getAnnotation(DisableInject.class) != null) {
                continue;
            }

            Class<?> pt = method.getParameterTypes()[0];
            // 基础数据类型无法注入,如 int, long 等
            if (ReflectUtils.isPrimitives(pt)) {
                continue;
            }

            try {
                String property = getSetterProperty(method);
                // 通过工厂方法获取需要注入的依赖
                Object object = objectFactory.getExtension(pt, property);
                // 非空则执行注入
                if (object != null) {
                    method.invoke(instance, object);
                }
            } catch (Exception e) {
                logger.error("Failed to inject via method " + method.getName()
                             + " of interface " + type.getName() + ": " + e.getMessage(), e);
            }

        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}

接下来我们回答 objectFactory 是什么,我们先可以将其概括为:依赖工厂
ExtensionLoader 初始化的时候:

// => org.apache.dubbo.common.extension.ExtensionLoader#ExtensionLoader
// 私有构造函数
private ExtensionLoader(Class<?> type) {
    this.type = type;
    // 当创建类型为非 ExtensionFactory 类型的拓展加载器时,都尝试创建依赖工厂,以用于后续的依赖注入
    // 注意该拓展动态适配器是标记了 @Adaptive 注解的,不需要动态编译
    objectFactory = (type == ExtensionFactory.class ? null :                      ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

ExtensionFactory 有两个依赖工厂实现,且 AdaptiveExtensionFactory 会尝试遍历所有依赖工厂,找到立即终止搜索并返回。

// 提供了公开静态方法,可以外部设置 Spring ApplicationContext,后续文章会介绍相关 spring-starter
// 当调用 getExtension 方法的时候,会遍历设置的 ApplicationContext 获取实例,该方法与 spi 互斥
spring=org.apache.dubbo.config.spring.extension.SpringExtensionFactory

// 注入 拓展动态适配器
spi=org.apache.dubbo.common.extension.factory.SpiExtensionFactory

WrapperClass 组成的职责链

我们可以将一些共性的逻辑,抽离到 WrapperClass 内。
到这步的时候,所有该类型的 WrapperClass 已经被缓存到了 cachedWrapperClasses 之中,逻辑如下:

// => org.apache.dubbo.common.extension.ExtensionLoader#createExtension
private T createExtension(String name, boolean wrap) {
    // ......
    if (wrap) {

        List<Class<?>> wrapperClassesList = new ArrayList<>();
        if (cachedWrapperClasses != null) {
            wrapperClassesList.addAll(cachedWrapperClasses);
            // 对包装类进行排序,依赖 @Activate 注解的 order 值,如果缺失则为 0
            // 值越大,在洋葱模型的越内层.
            wrapperClassesList.sort(WrapperComparator.COMPARATOR);
            Collections.reverse(wrapperClassesList);
        }

        if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
            for (Class<?> wrapperClass : wrapperClassesList) {
                // @Wrapper 注解表明,使用该包装器需要什么条件
                Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
                if (wrapper == null
                    || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {

                    // (T) 向上转型为原始接口类,例如 Protocol
                    // 并对包装类再次执行依赖注入
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }
        }
    }

    // 现在我们再看 initExtension 方法,当实例被包装过后,有可能不再是 Lifecycle 实例
    // 从严谨的角度来看,职责链和此处的 Hook 方法是冲突的,容易给用户造成不必要的困惑。
    initExtension(instance);
    // ......
}

如上就是 Dubbo SPI 拓展管理的骨架流程了。在 Dubbo 框架中, SPI 是一个核心流程,值得花时间去弄明白,这对后续概念的理解很有裨益。

getActivateExtension - 获取活跃拓展实例

逻辑相对简单,获取某类型下被 @Activate 标记的实现类,并返回其实例列表:

// => org.apache.dubbo.common.extension.ExtensionLoader#getActivateExtension
public List<T> getActivateExtension(URL url, String[] values, String group) {
    // ......
    // 遍历获取被 @Activate 注解的类
    for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
        String name = entry.getKey();
        Object activate = entry.getValue();

        String[] activateGroup, activateValue;

        if (activate instanceof Activate) {
            activateGroup = ((Activate) activate).group();
            activateValue = ((Activate) activate).value();
        } else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
            activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
            activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
        } else {
            continue;
        }
        cachedActivateGroups.put(name, new HashSet<>(Arrays.asList(activateGroup)));
        // 获取生效条件
        cachedActivateValues.put(name, activateValue);
    }
    // ......

    // traverse all cached extensions
    cachedActivateGroups.forEach((name, activateGroup) -> {
        if (isMatchGroup(group, activateGroup)
            && !names.contains(name)
            && !names.contains(REMOVE_VALUE_PREFIX + name)
            // 激活条件判断,此处可以实现依据 URL 参数配置按需激活拓展
            // 如果没有配置判断条件,则直接放行
            && isActive(cachedActivateValues.get(name), url)) {

            activateExtensionsMap.put(getExtensionClass(name), getExtension(name));
        }
    });

    // ......
    return new ArrayList<>(activateExtensionsMap.values());
}

参考资料