【爆肝两万字 收藏向】从用法到源码,一篇文章让你精通Dubbo的SPI机制(1)

简介: 【爆肝两万字 收藏向】从用法到源码,一篇文章让你精通Dubbo的SPI机制

前言

我们之前说过SPI机制,不仅谈到过SPI与API的区别,也讲了JAVA中的SPI机制实例。其实一切都是为了今天作铺垫,没错,今天我们要来讲讲Dubbo的重要设计——Dubbo-SPI机制。这一次直接爆肝2W字长文,从示例到源码,力求讲清讲透。事不宜迟,现在就开始我们的学习吧.....



一、Dubbo为什么要自己实现SPI

其实这个问题应该分成两部分:


Dubbo为什么要用SPI机制

Dubbo为什么不用原生的JAVA-SPI实现

466e55c1c7ea470faa9357b89cc3b095.png

1. Dubbo为什么需要SPI机制

要回答这个问题,我们必须先复习一遍SPI机制的特点与好处:解耦合、扩展性强、兼容性好


但我们如果仅使用Dubbo而不做任何改造的话,SPI的作用其实就相当于策略模式,一个接口内置了多个实现,可以根据入参或配置选择一个实现类。如果仅是如此,Dubbo的SPI机制显得就完全多余,可以由设计模式来替代了。


e4a37b4110c5408bbf7606664c3b2e58.png

所以。Dubbo使用SPI机制的主要原因是为了实现可插拔的扩展性。 具体来说,Dubbo 的 SPI 机制不仅可以使内置的几种实现类能灵活选用,还可以让用户通过配置文件或者注解的方式,自定义实现某个接口的实现类,然后在运行时自动通过 SPI 机制来加载并实例化对应的实现类。这样可以大大提高 Dubbo 的灵活性和可扩展性,同时也方便了 Dubbo 的用户进行自定义定制。在 Dubbo 中,所有内部实现和第三方实现都是平等的,用户可以基于自身业务需求,替换 Dubbo 提供的原生实现。


2. 为什么不用原生的JAVA-SPI实现

知道了Dubbo的SPI机制主要是为了可插拔的扩展性,那为什么不直接用JAVA自带的SPI机制呢,主要其实还是JAVA-SPI无法满足Dubbo的设计意图:


JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。

如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。

原生的SPI,获得实现类后,不支持dubbo的SPI机制增加了对IOC、AOP的支持,一个扩展点可以直接通过setter注入到其他扩展点。

二、Dubbo 到底哪些地方定了SPI

1. 官网介绍

我们说Dubbo 是为了支持可插拔的扩展,那么我们先来看看,Dubbo到底有哪些地方能支持扩展?我们在官网看到了下图:

f2d4d9a0f19a491cbc8dd03f31c1738f.png


协议与编码扩展:通信协议、序列化编码协议等

流量管控扩展:集群容错策略、路由规则、负载均衡、限流降级、熔断策略等

服务治理扩展:注册中心、配置中心、元数据中心、分布式事务、全链路追踪、监控系统等

诊断与调优扩展:流量统计、线程池策略、日志、QoS 运维命令、健康检查、配置加载等

2. 代码查阅

我们以其中的协议 Protocol 为例来看看。可以看出Protocol 为其定义在rpc包下的一个接口,而其内置的实现类数量也很多。有通用的Http协议、自研的Dubbo协议,java内置的Rmi协议,当然也支持其他框架的协议如thrift、grpc。

d3b41a61b87940768e2cd534819a2bfa.png


再比如让调用方、被调用方注册的注册中心,也有多种容器可选,如常见的Redis、Zookeeper、Nacos以及自己的Dubbo注册中心

82157b305e674be698ae5fefe8ddc434.png

可以说,Dubbo几乎是每个关键部件都提供了扩展的功能。我们过往会有很多公司有自研框架,自研协议。这种场景使用Dubbo,就只需要在有限的几个地方对接好其 SPI,就能很轻松的用上Dubbo,这对于开发者和架构来说,是个非常实用的设计。


三、Dubbo - SPI的使用及原理

1. JAVA 与 SPI 使用方式对比

我们先复习一遍JAVA 的原生SPI使用方式


JAVA 的原生使用

在jar包的 META-INF/services/ 下填入我们对SPI的实现类的全限定名,如 com.mysql.jdbc.Driver

在代码中使用 ServiceLoader loader = ServiceLoader.load(Driver.class); 就能获得一个加载了所有驱动实现类的对象

Dubbo中的使用.

Dubbo中的使用较为复杂,我们先来看它最基本的用法

如果我们自定义了RPC协议,那就在 META-INF/dubbo/org.apache.dubbo.rpc.Protocol 文件里填上我们的协议类的简称和它的全限定名,如myprotocol=com.zhanfu.samples.protocol.MyProtocol

在代码中使用 Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(“myprotocol”); 就能准确获取到我们指定的协议

在代码中使用 Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); 就能获取到自适应指定的的协议

可以看到,Dubbo 实现 SPI机制的核心是 ExtensionLoader,它取代了 JDK 自带的 ServiceLoader,我们慢慢来看其用法及实现原理


2. 配置文件

(1)配置规则

Dubbo 规定的三个配置目录分别为 META-INF/services/ 、META-INF/dubbo/internal/ 、META-INF/dubbo/,我们注意到 META-INF/services/ 其实就是原生JAVA - SPI 使用的目录、META-INF/dubbo/internal/则是Duubo自己内部实现的一些扩展配置,其实三个目录本身没有功能的区别,默认是全部都扫描的,一般我们建议将配置文件放在META-INF/dubbo/下

配置文件的文件名:必须是扩展点的全限定名,比如我们想使用自定义的协议,即protocol,我们就需要在上述某个目录下加上名为 org.apache.dubbo.rpc.Protocol 的文件

文件内容:采用“简称 = 类名”的形式,一行一个,比如 http=org.apache.dubbo.rpc.protocol.http.HttpProtocol

示例:下图就是Dubbo类型转换器的扩展配置

be918ce0231b41a7aec8407fc2b32c82.png

(2)加载配置与解析的原理

参考过去的JAVA原生的ServiceLoader,以及 getExtension 和 getAdaptiveExtension 两个方法,我们不难得出,扩展加载器至少有这么几个功能:


加载并保存各个扩展组件

能按照简称获得指定的扩展实现类

能提供默认的扩展实现类

需要注意的是,一个ExtensionLoader实例只包含一个扩展点,比如“协议”扩展点,那么该实例中,就只会加载“协议”接口的实现类


526b833137f64eb698ac5bbb2b5dcfdf.png

其实其中的 1 和 2,相当于原生SPI的略微改动,原生JAVA的SPI 会在启动后实例化所有扩展实现类,并保存。而 Dubbo 则是启动后保存<简称 ,类信息>的Map,你给定某个简称,我再为你实例化某个实现类。


那么其具体如何做的,我们直接来看其源码实现(默认实现类又是什么呢?我们又该如何指定默认实现类呢?这些问题请看@SPI部分)

// 按照指定简称,返回实现类
public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    // 当传入的名称为“true”时,获取默认实现类   
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    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) {
              // 为指定简称创建实现类
                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);
        }
        // 如果该实现类内引用了其他扩展点,则自动为其注入扩展点,类似于Spring体系下的IOC
        injectExtension(instance);
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        // 如果设定了装饰器类,则把实现类扔进这个装饰器,再把装饰器实例返回,如果有多个装饰器类,就会造成层层套壳
        // 与Spring中的AOP类似,但此处 wrapperClass 存在 ConcurrentHashSet,无法指定套壳的顺序
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            for (Class<?> wrapperClass : wrapperClasses) {
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                type + ")  could not be instantiated: " + t.getMessage(), t);
    }
}
// 获取加载扩展点<简称,类>的映射
private Map<String, Class<?>> getExtensionClasses() {
    Map<String, Class<?>> classes = cachedClasses.get();
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}
// 从配置文件中加载扩展点的信息
private Map<String, Class<?>> loadExtensionClasses() {
    // 获取本扩展点的默认实现
    cacheDefaultExtensionName();
    Map<String, Class<?>> extensionClasses = new HashMap<>();
    // 遍历策略,加载所有目录下指定的扩展点的信息。
    // 注意,只会扫描文件末尾是扩展点名的文件,比如“协议”扩展点,只会扫描类似 org.apache.dubbo.rpc.Protocol 文件
    for (LoadingStrategy strategy : strategies) {
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
    }
    return extensionClasses;
}

其中策略有三个实现类


6022298cb9804167a674f4e5f0362652.png

分别对应着下列目录


META-INF/services/

META-INF/dubbo/internal/

META-INF/dubbo/

所以,至此,我们已经明白了其实说到底,还是从上述三个目录中获取某个扩展点的所有实现情况。

而其具体从配置文件解析的过程,源码如下

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.");
    }
    // 实现类上有@Adaptive注解,表示这个实现类就为自适应实现类,详见第3小节
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        cacheAdaptiveClass(clazz, overridden);
    // 实现类上有带一个同类型入参的构造方法,表示这个实现类就为装饰器类,详见第2小节
    } else if (isWrapperClass(clazz)) {
        cacheWrapperClass(clazz);
    } else {
    // 普通的实现类
        clazz.getConstructor();
        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);
            }
        }
        String[] names = NAME_SEPARATOR.split(name);
        if (ArrayUtils.isNotEmpty(names)) {
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                cacheName(clazz, n);
                saveInExtensionClass(extensionClasses, clazz, n, overridden);
            }
        }
    }
}

可以看到,都是实现类,却也有不同的情况。可以是普通实现类,可以是装饰器类,也可以是自适应实现类,后两者我们在下面来讲。


目录
相关文章
|
3月前
|
Dubbo Java 应用服务中间件
Dubbo服务暴露机制解密:深入探讨服务提供者的奥秘【九】
Dubbo服务暴露机制解密:深入探讨服务提供者的奥秘【九】
24 0
|
4月前
|
存储 负载均衡 Dubbo
深入理解Dubbo-4.Dubbo扩展SPI
深入理解Dubbo-4.Dubbo扩展SPI
76 1
|
5月前
|
缓存 Dubbo Java
趁同事上厕所的时间,看完了 Dubbo SPI 的源码,瞬间觉得 JDK SPI 不香了
趁同事上厕所的时间,看完了 Dubbo SPI 的源码,瞬间觉得 JDK SPI 不香了
|
5月前
|
Dubbo Java 应用服务中间件
从源码全面解析 dubbo 服务端服务调用的来龙去脉
从源码全面解析 dubbo 服务端服务调用的来龙去脉
|
4月前
|
缓存 Dubbo Java
Dubbo 第三节_ Dubbo的可扩展机制SPI源码解析
Dubbo会对DubboProtocol对象进⾏依赖注⼊(也就是⾃动给属性赋值,属性的类型为⼀个接⼝,记为A接⼝),这个时候,对于Dubbo来说它并不知道该给这个属性赋什么值,换句话说,Dubbo并不知道在进⾏依赖注⼊时该找⼀个什么的的扩展点对象给这个属性,这时就会预先赋值⼀个A接⼝的⾃适应扩展点实例,也就是A接⼝的⼀个代理对象。在调⽤getExtension去获取⼀个扩展点实例后,会对实例进⾏缓存,下次再获取同样名字的扩展点实例时就会从缓存中拿了。Protocol是⼀个接。但是,不是只要在⽅法上加了。
|
21天前
|
设计模式 JSON Dubbo
超越接口:探索Dubbo的泛化调用机制
超越接口:探索Dubbo的泛化调用机制
26 0
|
2月前
|
Dubbo 网络协议 应用服务中间件
分布式微服务框架dubbo原理与机制
分布式微服务框架dubbo原理与机制
|
3月前
|
XML 缓存 Dubbo
Dubbo的魔法之门:深入解析SPI扩展机制【八】
Dubbo的魔法之门:深入解析SPI扩展机制【八】
31 0
|
3月前
|
XML 负载均衡 Dubbo
了解Dubbo配置:优先级、重试和容错机制的秘密【五】
了解Dubbo配置:优先级、重试和容错机制的秘密【五】
37 0
|
4月前
|
Dubbo Java 应用服务中间件
微服务框架(十七)Dubbo协议及编码过程源码解析
  此系列文章将会描述Java框架Spring Boot、服务治理框架Dubbo、应用容器引擎Docker,及使用Spring Boot集成Dubbo、Mybatis等开源框架,其中穿插着Spring Boot中日志切面等技术的实现,然后通过gitlab-CI以持续集成为Docker镜像。   本文为Dubbo协议、线程模型、和其基于Netty的NIO异步通讯机制及源码