Dubbo 源码分析 – SPI 机制

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

1.简介

SPI 全称为 Service Provider Interface是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中并由服务加载器读取配置文件加载实现类。这样可以在运行时动态为接口 加载实现类。正因此特性我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过Dubbo 并未使用 Java 原生的 SPI 机制而是对其进行了增强使其能够更好的满足需求。在 Dubbo 中SPI 是一个非常重要的模块。如果大家想要学习 Dubbo 的源码SPI 机制务必弄懂。下面我们先来了解一下 Java SPI 与 Dubbo SPI 的使用方法然后再来分析 Dubbo SPI 的源码。

2.SPI 示例

2.1 Java SPI 示例

前面简单介绍了 SPI 机制的原理本节通过一个示例来演示 JAVA SPI 的使用方法。首先我们定义一个接口名称为 Robot。

public interface Robot {
    void sayHello();
}

接下来定义两个实现类分别为擎天柱 OptimusPrime 和大黄蜂 Bumblebee。

public class OptimusPrime implements Robot {

    @Override
    public void sayHello() {
        System.out.println("Hello, I am Optimus Prime.");
    }
}

public class Bumblebee implements Robot {

    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
}

接下来 META-INF/services 文件夹下创建一个文件名称为 Robot 的全限定名 com.tianxiaobo.spi.Robot。文件内容为实现类的全限定的类名如下

com.tianxiaobo.spi.OptimusPrime
com.tianxiaobo.spi.Bumblebee

做好了所需的准备工作接下来编写代码进行测试。

public class JavaSPITest {

    @Test
    public void sayHello() throws Exception {
        ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
        System.out.println("Java SPI");
        serviceLoader.forEach(Robot::sayHello);
    }
}

最后来看一下测试结果如下

从测试结果可以看出我们的两个实现类被成功的加载并输出了相应的内容。关于 Java SPI 的演示先到这接下来演示 Dubbo SPI。

2.2 Dubbo SPI 示例

Dubbo 并未使用 Java SPI而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中通过 ExtensionLoader我们可以加载指定的实现类。Dubbo SPI 的实现类配置放置在 META-INF/dubbo 路径下下面来看一下配置内容。

optimusPrime = com.tianxiaobo.spi.OptimusPrime
bumblebee = com.tianxiaobo.spi.Bumblebee

与 Java SPI 实现类配置不同Dubbo SPI 是通过键值对的方式进行配置这样我们就可以按需加载指定的实现类了。另外在测试 Dubbo SPI 时需要在 Robot 接口上标注 @SPI 注解。下面来演示一下 Dubbo SPI 的使用方式

public class DubboSPITest {

    @Test
    public void sayHello() throws Exception {
        ExtensionLoader<Robot> extensionLoader = 
            ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee");
        bumblebee.sayHello();
    }
}

测试结果如下

演示完 Dubbo SPI下面来看看 Dubbo SPI 对 Java SPI 做了哪些改进以下内容引用至 Dubbo 官方文档。

  • JDK 标准的 SPI 会一次性实例化扩展点所有实现如果有扩展实现初始化很耗时但如果没用上也加载会很浪费资源。
  • 如果扩展点加载失败连扩展点的名称都拿不到了。比如JDK 标准的 ScriptEngine通过 getName() 获取脚本类型的名称但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在导致 RubyScriptEngine 类加载失败这个失败原因被吃掉了和 ruby 对应不起来当用户执行 ruby 脚本时会报不支持 ruby而不是真正失败的原因。
  • 增加了对扩展点 IOC 和 AOP 的支持一个扩展点可以直接 setter 注入其它扩展点。

在以上改进项中第一个改进项比较好理解。第二个改进项没有进行验证就不多说了。第三个改进项是增加了对 IOC 和 AOP 的支持这是什么意思呢这里简单解释一下Dubbo SPI 加载完拓展实例后会通过该实例的 setter 方法解析出实例依赖项的名称。比如通过 setProtocol 方法名可知道目标实例依赖 Protocal。知道了具体的依赖接下来即可到 IOC 容器中寻找或生成一个依赖对象并通过 setter 方法将依赖注入到目标实例中。说完 Dubbo IOC接下来说说 Dubbo AOP。Dubbo AOP 是指使用 Wrapper 类可自定义实现对拓展对象进行包装Wrapper 类中包含了一些自定义逻辑这些逻辑可在目标方法前行前后被执行类似 AOP。Dubbo AOP 实现的很简单其实就是个代理模式。这个官方文档中有所说明大家有兴趣可以查阅一下。

关于 Dubbo SPI 的演示以及与 Java SPI 的对比就先这么多接下来加入源码分析阶段。

3. Dubbo SPI 源码分析

上一章我简单演示了 Dubbo SPI 的使用方法。我们首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。这其中getExtensionLoader 用于从缓存中获取与拓展类对应的 ExtensionLoader若缓存未命中则创建一个新的实例。该方法的逻辑比较简单本章就不就行分析了。下面我们从 ExtensionLoader 的 getExtension 方法作为入口对拓展类对象的获取过程进行详细的分析。

public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    if ("true".equals(name)) {
        // 获取默认的拓展实现类
        return getDefaultExtension();
    }
    // Holder 仅用于持有目标对象没其他什么逻辑
    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) {
                // 创建拓展实例并设置到 holder 中
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

上面代码的逻辑比较简单首先检查缓存缓存未命中则创建拓展对象。下面我们来看一下创建拓展对象的过程是怎样的。

private T createExtension(String name) {
    // 从配置文件中加载所有的拓展类形成配置项名称到配置类的映射关系
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            // 通过反射创建实例
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        // 向实例中注入依赖
        injectExtension(instance);
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            // 循环创建 Wrapper 实例
            for (Class<?> wrapperClass : wrapperClasses) {
                // 将当前 instance 作为参数创建 Wrapper 实例然后向 Wrapper 实例中注入属性值
                // 并将 Wrapper 实例赋值给 instance
                instance = injectExtension(
                    (T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("...");
    }
}

createExtension 方法的逻辑稍复杂一下包含了如下的步骤

  1. 通过 getExtensionClasses 获取所有的拓展类
  2. 通过反射创建拓展对象
  3. 向拓展对象中注入依赖
  4. 将拓展对象包裹在相应的 Wrapper 对象中

以上步骤中第一个步骤是加载拓展类的关键第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。在接下来的章节中我将会重点分析 getExtensionClasses 方法的逻辑以及简单分析 Dubbo IOC 的具体实现。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6