首页 > 编程语言 >Dubbo源码剖析-SPI机制(超详细深度剖析篇)

Dubbo源码剖析-SPI机制(超详细深度剖析篇)

时间:2024-09-06 22:23:51浏览次数:7  
标签:Dubbo name instance 扩展 剖析 SPI 源码 type

目录

什么是SPI

SPI 的工作原理

SPI 的作用

SPI 的缺点

简单使用JDK的SPI与Dubbo的SPI

Dubbo为什么要使用SPI机制

Dubbo SPI 源码分析

小结


什么是SPI

        SPI 全称为 Service Provider Interface,一种解耦接口和实现的手段,其实现原理是将接口的实现类全名称配置在配置文件中,程序运行阶段去读取配置文件加载实现类,这个机制为程序带来 了很强的扩展性,使 得我们可以很方便的 基于某接口规范去使用任何第三方的实现。

        SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。下面,我们先来了解一下 Java SPI 与 Dubbo SPI 的使用方法,然后再来分析 Dubbo SPI 的源码。

SPI 的工作原理

  1. 定义服务接口:首先定义一个服务接口,通常这个接口会包含在 JAR 文件中。

  2. 提供服务实现:开发者可以通过创建该接口的具体实现类来提供服务。实现类的信息会被写入到 META-INF/services 目录下的一个文件中,文件名就是服务接口的全限定名,而文件内容则是实现类的全限定名。

  3. 查找服务实现:当需要使用该服务时,可以通过 ServiceLoader 类来加载所有可用的服务实现。ServiceLoader 会在类路径下搜索 META-INF/services 目录中的配置文件,并加载其中列出的服务实现。

SPI 的作用

  • 插件化:SPI 提供了一种在运行时动态发现并加载服务实现的方法,这使得系统可以在不知道具体实现的情况下使用各种不同的插件。

  • 扩展性:通过 SPI 可以轻松地添加新的服务实现而不必修改现有的代码。

  • 灵活性:SPI 允许在部署时选择不同的服务实现,这样可以根据环境的不同选择最合适的实现。

SPI 的缺点

  • 性能开销:每次启动应用时,SPI 都会扫描 META-INF/services 目录下的配置文件,如果有很多服务需要加载,这可能会导致较大的性能开销。

  • 初始化延迟:由于 SPI 是在运行时加载服务实现的,所以如果服务实现比较复杂或者有很多实现类,那么第一次使用服务可能会有明显的延迟。

  • 兼容性问题:如果多个服务提供者同时存在,可能会导致命名冲突或其他兼容性问题。

  • 安全性考虑:SPI 加载的类来自外部,如果没有适当的沙箱机制或安全检查,可能会引入安全风险。

尽管 SPI 存在一些缺点,但它仍然是 Java 中非常有用的设计模式,特别是在需要实现插件化和模块化系统时。开发者应该权衡其优缺点,并在适当的情景下使用它。

简单使用JDK的SPI与Dubbo的SPI

http://t.csdnimg.cn/JbqbB

Dubbo为什么要使用SPI机制

        dubbo作为一个rpc框架,在它发送RPC请求时,整个过程会经历很多个关键事件节点,比如集群容错,负载均衡,数据序列化,通信协议编码,网络传输等,每个关键节点都有抽象出对应的接口,而且有多种不同的实现,且用户可自行扩展,那实际运行阶段 dubbo如何根据用户的配置参数来选择具体的实现呢,这就促使dubbo需要一种可插拔的接口实现发现机制。

        dubbo采用微内核架构,将每一个功能接口当作一个可插拔的扩展点接口,内核层面只负责按流程组装并引导执行每个扩展点接口 ,具体的功能和逻辑由具体的扩展点实现来完成,提高了系统的扩展性和灵活性,而SPI就是实现微内核的手段。

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

Dubbo SPI 源码分析

上面我们简单演示了 Dubbo SPI 的使用方法。接下来我们来分析其源码,

dubbo版本:2.7.19-relesse

我们首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension方法获取扩展点对象。这其中,getExtensionLoader 用于从EXTENSION_LOADERS全局缓存中获取与扩展点对应的 ExtensionLoader扩展点执行器,若缓存未命中,则创建一个新的实例

    // 每个SPI接口都对应一个ExtensionLoader实例
    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);


    @SuppressWarnings("unchecked")
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        if (type == null) {
            throw new IllegalArgumentException("Extension type == null");
        }
        if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
        }
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type (" + type +
                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        }

        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

拿到扩展点执行器后调用ExtensionLoader 的 getExtension 方法,此方法有两个参数name为扩展点名称,wrap标识是否进行Wrapper增强(Wrapper是SPI的一个高级特性,后续章节会讲到)

  1. 判断扩展点名称是否为true如果是则返回默认的扩展点
  2. 如果不为true,则获取全局缓存cachedInstances中的扩展点实例
  3. 接下来就是采用了单例模式的双重检查锁机制来检查缓存,缓存未命中则创建扩展点
    // 缓存 当前接口下所有完整的扩展点实例
    private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();

    @SuppressWarnings("unchecked")
    public T getExtension(String name) {
        return getExtension(name, true);
    }


    // 获取或创建扩展点实例
    private Holder<Object> getOrCreateHolder(String name) {
        Holder<Object> holder = cachedInstances.get(name);
        if (holder == null) {
            cachedInstances.putIfAbsent(name, new Holder<>());
            holder = cachedInstances.get(name);
        }
        return holder;
    }

    // 获取扩展点
    public T getExtension(String name, boolean wrap) {
        if (StringUtils.isEmpty(name)) {
            throw new IllegalArgumentException("Extension name == null");
        }
        if ("true".equals(name)) {
            return getDefaultExtension();
        }
        // 扩展点都是单实例的
        final Holder<Object> holder = getOrCreateHolder(name);
        Object instance = holder.get();
        if (instance == null) {
            synchronized (holder) {
                instance = holder.get();
                if (instance == null) {
                    instance = createExtension(name, wrap);
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }

下面我们来看一下创建扩展点对象的过程是怎样的

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

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

/**
     * 创建扩展点
     * 1、完成文件解析,加载,
     * 2、完成基础实例的创建
     * 3、完成实例的注入
     * 4、完成实例的wrapper包装
     */
    @SuppressWarnings("unchecked")
    private T createExtension(String name, boolean wrap) {
        /**
         * getExtensionClasses方法很重要:
         *  完成了配置文件解析及加载,筛选
         */
        Class<?> clazz = getExtensionClasses().get(name);
        if (clazz == null || unacceptableExceptions.contains(name)) {
            throw findException(name);
        }
        try {
            // 创建并保存扩展点原始实例
            T instance = (T) EXTENSION_INSTANCES.get(clazz);
            if (instance == null) {
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            }
            // 完成扩展点实例的 inject 操作
            injectExtension(instance);

            // 完成对扩展点实例的包装
            if (wrap) {

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

                if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                    for (Class<?> wrapperClass : wrapperClassesList) {
                        // wrapper 类上可以用 Wrapper 注解来标注 当前wrapper 是否对某扩展点进行增强
                        Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
                        if (wrapper == null
                                || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
                            // wrapper 有接口类型的构造
                            instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                        }
                    }
                }
            }

            initExtension(instance);
            return instance;// 返回最终的扩展点实例
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                    type + ") couldn't be instantiated: " + t.getMessage(), t);
        }
    }

我们在通过名称获取扩展点之前,首先需要根据配置文件加载解析出名称到扩展点的映射,也就是 Map<名称, 扩展点>。之后再从 Map 中筛选出响应的扩展点即可。这里也是先检查缓存,若缓存未命中,则采用了单例模式的双重检查锁机制来检查缓存,则加载扩展点。前面所分析的 getExtension 方法中有相似的代码。下面分析 loadExtensionClasses 方法的逻辑。

    //缓存解析完的当前接口所有扩展点的 Class,key是扩展点名称
    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();

    /**
     * 完成 当前SPI扩展点接口的加载,解析,筛选
     * @return
     */
    private Map<String, Class<?>> getExtensionClasses() {
        Map<String, Class<?>> classes = cachedClasses.get();
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    classes = loadExtensionClasses();// 只加载解析1次
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }

loadExtensionClasses 方法总共做了两件事情

  1. 对 SPI 注解进行解析,缓存默认扩展点名称
  2. 调用 loadDirectory 方法加载指定文件夹配置文件

下面我们来看一下 loadDirectory 做了哪些事情

    // 注解解析,缓存默认扩展点名称
    private void cacheDefaultExtensionName() {
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if (defaultAnnotation == null) {
            return;
        }

        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
            String[] names = NAME_SEPARATOR.split(value);
            if (names.length > 1) {
                throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
                        + ": " + Arrays.toString(names));
            }
            if (names.length == 1) {
                cachedDefaultName = names[0];
            }
        }
    }

    /**
     * synchronized in getExtensionClasses
     */
    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());
            loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"),
                    strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        }

        return extensionClasses;
    }
后续明日待完善。。。。

小结

提示:这里可以添加总结

例如:

提供先进的推理,复杂的指令,更多的创造力。

标签:Dubbo,name,instance,扩展,剖析,SPI,源码,type
From: https://blog.csdn.net/weixin_55721317/article/details/141960672

相关文章

  • Java毕业设计-基于SSM框架的高校外事管理系统项目实战(附源码+论文)
    大家好!我是程序猿老A,感谢您阅读本文,欢迎一键三连哦。......
  • Java毕业设计-基于SSM框架的图书借阅管理系统项目实战(附源码+论文)
    大家好!我是程序猿老A,感谢您阅读本文,欢迎一键三连哦。......
  • Spring 源码解读:实现Spring容器的启动流程
    引言Spring容器的启动流程是Spring框架中最为基础且重要的部分。通过对Spring容器的启动机制进行解读,我们可以更加清晰地理解Spring是如何管理Bean的生命周期、如何处理依赖注入等核心功能。本篇文章将通过手动实现一个简化的Spring容器启动流程,并与Spring实际的启动过程......
  • jsp仓储管理系统9e8ai 本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上
    jsp仓储管理系统9e8ai本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表项目功能客户,库存人员,入库人员,出库人员,商品类别,商品信息,仓区信息,商品入库,商品出库开题报告内容一、项目背景与意义随着电子商......
  • vue.js项目实战案例源码
    关注我,持续分享逻辑思维&管理思维&面试题;可提供大厂面试辅导、及定制化求职/在职/管理/架构辅导;推荐专栏《10天学会使用asp.net编程AI大模型》,目前已完成所有内容。一顿烧烤不到的费用,让人能紧跟时代的浪潮。从普通网站,到公众号、小程序,再到AI大模型网站。干货满满。学成后可......
  • 汉服女装商城购物主题html网页成品 | html学生网页源码
    文章目录网站主题网站描述网站介绍网站演示学习理念更多干货一、网站主题女装商城网站、女装购物网站、服装商城网站、购物商城网站、电商主题网站、网页设计与制作二、网站描述网页简介:此作品为学生网页设计毕设服装商城购物网页设计题材(汉服,是汉民族的传统服饰。又称......
  • 识别并应对动态归纳类算法题:深入剖析与实战指南
    《识别并应对动态归纳类算法题:深入剖析与实战指南》在编程的世界里,算法题犹如一座座充满挑战的山峰,等待着开发者们去攀登。其中,动态归纳类算法题因其复杂性和灵活性,常常成为开发者们进阶路上的一道难关。本文将深入探讨如何识别并应对动态归纳类算法题,为大家提供一份全面的......
  • 基于WiFi的智能照明控制系统的设计与实现(论文+源码)
    1系统方案设计本设计智能照明控制系统,结合STM32F103单片机、光照检测模块、显示模块、按键模块、太阳能板、LED灯模块、WIFI模块等器件构成整个系统,在功能上可以实现光照强度检测,并且在自动模式下可以自动调节照明亮度,在手动模式下,用户可以手动调节亮度,并且借助ESP8266WiFi他......
  • 如何将源码压缩后发布到 GitHub 或其他平台
    在软件开发过程中,源码的管理和发布是非常关键的环节。特别是在需要对源码进行共享、分发或归档时,压缩和上传源码到平台如GitHub、GitLab、或其他云存储服务上是常见的做法。为什么需要压缩源码在一些场景下,开发者可能需要将源码进行压缩后发布,例如:1.文件体积较大:项目文件......
  • 【商城源码开发周期是多久?】
    文章目录前言一、项目规模二、开发方式三、开发团队四、测试质量五、开发周期案例总结前言商城源码的开发周期通常从几天到几个月不等,具体时间取决于项目的规模、复杂度、开发方式和团队实力等因素。在确定开发周期时,需要考虑多个因素,包括项目规模与复杂度、开发方......