迷迷糊糊?似懂非懂?一文让你从此对SPI了如指掌

news2024/11/16 23:55:41

迷迷糊糊?似懂非懂?一文让你从此对SPI了如指掌

  • 前言
  • 一、SPI 与 API
    • 1. SPI 在生活中的类比
    • 2. SPI 在代码上的例子
    • 3. API 与 SPI 的关系
  • 二、JAVA 的 SPI 机制
    • 1. JAVA 中的 SPI 例子
    • 2. SPI 机制的四大组件
    • 3. SPI 机制的实现
    • 4. JAVA SPI的不足


前言

你是不是有很多次,学了一些框架的知识,过不了多久又忘记了,如此反复,感觉浪费了很多时间。其实这种现象很正常,一来是掌握可能是死记硬背+走马观花,本身就掌握不深刻。二来是知识没成体系,孤零零的知识点本身就容易忘记,今天,就让我们深刻-全面的学习一下SPI吧


一、SPI 与 API

大多数开发者对于 API 的概念已经很熟了,很多时候我们需要使用某种服务,都会要求其开发者提供 API 列表,这些列表规定了入参、出参、具体功能。而 SPI 很多人的理解就比较模糊了,其实不能怪各位读者,IT领域确实有很多概念描述得很抽象,一些有联系的内容很难准确分割。

不管怎样,我们先把SPI具体的内涵说一下,再进行诠释:SPI是Service Provider Interface(服务提供者接口)的缩写,概念起源于java平台,是一种在应用程序中使用插件的实现方式,它允许在运行时动态地替换和添加组件,同时支持扩展接口的实现方式。SPI通常由两部分组成:接口和实现类。接口定义了一组方法,实现类是实现这些方法的具体类。在应用程序中,SPI的使用者只需要定义接口,并在运行时加载合适的实现类即可

1. SPI 在生活中的类比

我们以一个生活上的例子来说明两者的联系与不同

小明是个超市老板,小红是水果商。小红表示,只要你给我发短信“我需要些货”的时候,我就会准备价值十万的水果卖给你。

这样,我们就说小红提供了一个API,入参是一条短信,出参是价值十万块的水果。

就这样,过了一段时间,小明觉得这样一条短信就能办事太舒服了,想要把这种模式发扬光大。于是他告知冰箱厂的王厂长,当我给你发短信“我需要些货”时,你就给我准备总价十万的冰箱;又告诉卖文具的张老板,当我给你发短信“我需要些货”时,你就给我准备总价十万的文具… … 最后,小明干脆贴了个告示,跟我的超市合作的,当我给你发短信“我需要些货”时,你就准备好价值十万的货。

这样一来,短信还是一样的内容,但是小明选择发给不同的人,就能得到不同的结果,而且后续想和小明合作的人,也都接受了这种模式,那么我们说这种情况就形成了个SPI

所以SPI更像一种设计模式,调用者把接口变成公开的协议,对后来者开放,后来者要想加入,就要遵循这个壳(示例中的发短信),来实现相似但并不一样的功能(都是备货,但各厂家备的货不一样)

2. SPI 在代码上的例子

除了生活中的例子,我们再来看看实际代码上的例子,最广为人知的莫过于 JDBC(java database connectivity)规范

我们知道现在大部分JAVA应用都是需要连接数据库的,但是市面上的数据库有很多种,如果每个数据库的驱动都自搞一套,对于java开发者来说,连接数据库都将变成沉重又复杂的负担。这个时候JAVA就推出了JDBC规范,JDBC API定义了一套接口(如Driver、Connection、Statement等),并广而告之。需要和JAVA平台对接的数据库厂商选择接受了这个规范,按照JAVA的接口要求,在这层壳之下开发各自的驱动

如下图,我们可以看到jdk中规定的接口

在这里插入图片描述
再来看看Mysql 提供的驱动里对这些类的实现 (不知为何,我总感觉这段注释有些许怨念…)

在这里插入图片描述

3. API 与 SPI 的关系

其实从上面两个例子,我们不难看出,APISPI 有很深的联系,但 API 接口的定义权通常在被调用方(功能实现方),由被调用者主动提供,,即主动“暴露”出去的功能,调用者得按照这个API的说明来使用该功能;SPI则是基于API,但是这是调用方反客为主,自己拟定出一系列API,要求所有被调用者都来遵守。
在这里插入图片描述
在这里插入图片描述

所以,SPI机制可以认为调用方掌握了API的定制权,并把该AP形成规范的一种机制,这样一来,应用程序只需要使用统一的接口,而无需关心不同人各自的实现,只要按需选择一个人,调用该接口即可。此时这个所谓的API,现在也可以被叫做SPI
在这里插入图片描述

二、JAVA 的 SPI 机制

我们提到过,SPI机制的说法 ,是由JAVA率先提出的,那么JAVA自己到底是如何运用这个机制的呢?

1. JAVA 中的 SPI 例子

除了上面提过的 Driver驱动 的例子,其实 java 中还有一些使用SPI的例子。在JDK1.8下,我们可以看见这几种示例
在这里插入图片描述
我们可以简单介绍其中的一些:

  • 货币名称提供程序 (CurrencyNameProvider):
    为货币类提供本地化的货币符号。
  • 区域设置名称提供程序 (LocaleNameProvider):
    提供区域设置类的本地化名称。
  • 时区名称提供程序 (TimeZoneNameProvider):
    为时区类提供本地化的时区名称.
  • 日期格式提供程序 (DateFormatProvider):
    为指定的区域设置提供日期和时间格式.
  • 数字格式提供程序 (NumberFormatProvider):
    为 NumberFormat 类提供货币、整数和百分比值.
  • 驱动 (Driver):
    从版本 4.0 开始,JDBC API 支持 SPI 模式。旧版本使用 Class.forName() 方法来加载驱动程序

2. SPI 机制的四大组件

  1. 服务
    一组公开的编程接口和类,提供对某些特定应用程序功能或特性的访问
  2. 服务提供者接口
    充当服务的代理或端点的接口或抽象类。如果服务是一个接口,则它与服务提供程序接口相同。
  3. 服务提供者
    SPI的具体实现。服务提供程序包含一个或多个实现或扩展服务类型的具体类。通过我们放入资源目录 META-INF/services 中的提供程序配置文件来配置和标识服务提供程序。文件名是SPI的完全限定名称,其内容是SPI实现的完全限定名。服务提供程序是以扩展的形式安装的,这是一个jar文件,我们将其放置在应用程序类路径、Java扩展类路径或用户定义的类路径中。
  4. 服务装载机
    SPI的核心是 ServiceLoader 类。这具有惰性地发现和加载实现的作用。它使用上下文类路径来定位提供程序实现,并将它们放在内部缓存中。

3. SPI 机制的实现

其中最为重要的就是 ServiceLoader 类,这个是SPI机制实现的核心,我们可以看一下这个类的注释:

一个简单的服务提供商加载工具。
服务是一组众所周知的接口和(通常是抽象的)类。 服务提供商是服务的特定实现。 提供程序中的类通常实现接口,并对服务本身中定义的类进行子类。 服务提供程序可以以扩展的形式安装在Java平台的实现中,即放置在任何常用扩展目录中的jar文件。 还可以通过将提供程序添加到应用程序的类路径或通过其他特定于平台的方式来提供提供程序。
出于加载的目的,服务由单个类型表示,即单个接口或抽象类。 (可以使用具体类,但不建议这样做。 给定服务的提供程序包含一个或多个具体类,这些类使用特定于提供程序的数据和代码扩展此服务类型。 提供程序类通常不是整个提供程序本身,而是一个代理,其中包含足够的信息来确定提供程序是否能够满足特定请求以及可以按需创建实际提供程序的代码。 提供程序类的详细信息往往是高度特定于服务的; 没有一个类或接口可以统一它们,所以这里没有定义这样的类型。 此工具强制实施的唯一要求是提供程序类必须具有零参数构造函数,以便可以在加载期间实例化它们
通过将提供程序配置文件放在资源目录 META-INF/services 中来标识服务提供程序。 文件名是服务类型的完全限定二进制名称。 该文件包含具体提供程序类的完全限定二进制名称的列表,每行一个。 每个名称周围的空格和制表符以及空行将被忽略。 注释字符为“#”(“#”,数字符号); 在每一行上,第一个注释字符后面的所有字符都将被忽略。 该文件必须以 UTF-8 编码。
如果特定的具体提供程序类在多个配置文件中命名,或者在同一配置文件中多次命名,则会忽略重复项。 命名特定提供程序的配置文件不必与提供程序本身位于同一 jar 文件或其他分发单元中。 必须可从最初查询的同一类装入器访问提供程序以查找配置文件; 请注意,这不一定是实际从中装入文件的类装入器。
提供程序是延迟定位和实例化的,即按需。 服务加载程序维护到目前为止已加载的提供程序的缓存。 迭代器方法的每次调用都会返回一个迭代器,该迭代器首先按实例化顺序生成缓存的所有元素,然后延迟查找并实例化任何剩余的提供程序,依次将每个提供程序添加到缓存中。 可以通过重新加载方法清除缓存。
服务加载程序始终在调用方的安全上下文中执行。 受信任的系统代码通常应从特权安全上下文中调用此类中的方法以及它们返回的迭代器的方法。
此类的实例不能安全地由多个并发线程使用。

然后其中最关键的方法,我们结合代码来看

private static final String PREFIX = "META-INF/services/";

// 清除,并重新读取某个服务的所有提供者,可以看到,实际是以一个迭代器的形式存储的
public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

private class LazyIterator
        implements Iterator<S>
    {

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;

        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                	// 获取我们在包 META-INF/services/ 下写的全限定名
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
            // 把全限定名通过反射,加载进来
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

}

不难发现,JAVA自带的SPI机制,是要求服务提供者,在包下 “META-INF/services/” 内写上服务实现类的全限定名,然后在我们使用服务的时候,懒加载这个服务的所有实现类,如下来获取Mysql驱动:

// 获取所有驱动
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);

Driver driver = null;

// 遍历找到Mysql驱动
for (Driver d : loader) {
    if (d.getClass().getName().equals("com.mysql.jdbc.Driver")) {
        driver = d;
        break;
    }
}

if (driver == null) {
    throw new RuntimeException("mysql driver not found");
}

当然,其实远不用这么写,比如我们要获得Mysql驱动,只需执行如下代码

Connection conn = DriverManager.getConnection(url, user, password);

这是因为java自带了 DriverManager 类,这个类在初始化时,就会执行 ServiceLoader loader = ServiceLoader.load(Driver.class); 相当于帮我们把工作做好了。在执行 getConnection 时,会遍历这些服务提供实现类,直到能返回连接才终止循环。

4. JAVA SPI的不足

Java自带的SPI机制是一种较为简单的服务发现机制,具有以下缺点:

  • 功能有限:Java自带的SPI机制只能实现服务的发现和加载,无法解决服务的版本升级、路由控制、负载均衡、容错等问题。

  • 依赖于类加载器:Java自带的SPI机制依赖于类加载器,需要将服务实现类打包成jar文件,并将其放到类路径下,才能被ServiceLoader发现并加载。这种机制限制了服务实现类的灵活性和可扩展性。

  • 全加载:Java自带的SPI机制在对某个服务的提供者进行加载时,采用的是全部加载进来,再选择使用哪个或哪几个。一些不会被利用的服务实现也被加载进内存了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/712830.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CICD集合(四):Jenkins配置JDK,Maven,Allure报告

在Jenkins上面配置Jdk以及Maven环境 Maven和JDK Allure报告配置 当然配置Allure,得先安装Allure插件&#xff1a;

【Java项目】SpringBoot项目如何从自研配置中心拉取配置

文章目录 前言思路 前言 先简单说一下这个题目的意思是什么。 我们知道&#xff0c;如果我们的项目是SpringCloud的项目&#xff0c;我们是可以把我们的本地配置写到云端的的配置中心的&#xff0c;比如集成SpringCloud Alibaba&#xff0c;你就可以使用Nacos&#xff0c;然后…

科技云报道:智能化潮起,物联网产业链安全和效率问题何解?

科技云报道原创。 黑灯瞎火的夜。一胖一瘦两个小伙分别拿着平板和手机&#xff0c;在某知名新能源汽车周围鬼鬼祟祟地打转。 不到一分钟的时间&#xff0c;其中的瘦小伙很轻易地就用手机解开了车门锁。另外一名胖小伙&#xff0c;却用了两分钟&#xff0c;花了老大的力气&…

网站建设如何快速建站_网站建设快速建站有哪些方法

网站建设快速建站方法 1、JavaScript 压缩和模块打包 JavaScript 应用是以源码形式进行分发的&#xff0c;而源码解析的效率是要比字节码低的。对于一小段脚本来说&#xff0c;区别可以忽略不计。但是对于更大型的应用&#xff0c;脚本的大小会对应用启动时间有着负面的影响。…

dubbo Sentinet 限流 流控配置 高级 直连 关联 链路 预热冷启动 排队等待 单机 qps 并发 机器总体法制 单机均摊 集群俩种身份

目录 进入配置 单机超过10个限流 Qps 和 并发线程数区别使用思路 集群阈值模式 单机均摊 总体阈值 集群流控中共有两种身份&#xff1a; 高级-流控模式 直连 关联 链路 首先在实现类上加注解 Warm Up 预热冷启动 10秒内完成 100 预热并发效果 效果 排队等待 单…

Linux11.进程等待

1.写时拷贝 :使用fork创建子进程的时候&#xff0c;操作系统使用写时拷贝&#xff0c;类似于浅拷贝和深拷贝&#xff0c;对于只读的数据进行浅拷贝&#xff0c;对于需要写入的数据进行深拷贝。 2.cpu中有一个EIP寄存器&#xff0c;也叫pc指针(程序计数器)&#xff0c;永远指向…

【ARM】-数据访问中止异常中断处理程序的返回

文章目录 处理流程示例 处理流程 当发生数据访问中止异常中断时&#xff0c;程序要返回到该有问题的数据指令处&#xff0c;重新访问该数据。因此数据访问中止异常中断程序应该返回到该数据访问中止异常中断的指令处&#xff0c;而不是像前面两种情况下返回到发生中断的指令的…

SpringBoot实战项目整合RabbitMQ+ElaticSearch实现SKU上下架功能

文章目录 前言1、前置条件2、搭建service-search模块3、开发功能接口3.1 添加远程调用方法3.2、创建远程调用模块3.3、开发service-search 模块接口 4、RabbitMQ5、完善SKU管理商品上下架5.1、商品服务5.2、es服务 6、最终测试总结 前言 最终实现效果&#xff1a;针对SKU的上下…

Java基础---String、StringBuilder和StringBuffer的区别

目录 典型回答 String的""是如何实现的 StringBuffer和StringBuilder 不要在for循环中使用拼接字符串 典型回答 本质上都是char[]字符数组的实现在Java9之后&#xff0c;String类的实现改用byte数组存储字符串使用final关键字修饰字符数组来保存字符串&#xff0…

基于VUE3+Layui从头搭建通用后台管理系统(前端篇)四:用户注册界面及对应功能实现

一、本章内容 本章实现用户注册功能,包括短信注册界面、邮箱注册界面、短信注册修改接口、邮箱注册修改接口等相关内容,实现用户注册的完整流程。 1. 详细课程地址: 待发布 2. 源码下载地址: 待发布 二、界面预览 三、开发视频 基于VUE3+Layui从头搭建通用后台管理系统合…

打造 API 接口的堡垒

前言 伴随互联网革命快速创新发展&#xff0c;API 需求的日益剧增&#xff0c;针对 API 的攻击几乎遍布各个行业&#xff0c;据报道 2022 年全年平均每月遭受攻击的 API 数量超过 21 万&#xff0c;游戏、社交、电商、制造等行业依然是攻击者主要目标。例如社交软件某特&#…

cuda优化

希望用GPU解决更大的问题&#xff0c;更多的程序在同等的设备商运行 最大化单个kernel的运算强度&#xff0c; 最小化内存的操作时间 在第一步分析的时候&#xff0c;不要依赖直觉 类似CPU&#xff0c;单个线程处理这个事情&#xff0c;串行 把读取全局内存的地方合并以后…

7.2 文件系统的简单操作

7.2.1 磁盘与目录的容量 磁盘的整体数据是在superblock区块中&#xff0c;但是每个个别文件的容量在inode当中记载的。 df&#xff1a;列出文件系统的整体磁盘使用量&#xff1b; du&#xff1a;评估文件系统的磁盘使用量&#xff08;常用在推估目录所占容量&#xff09; d…

Postman是个好用的工具,不试一下?

忘了 postman 是被谁种草的&#xff0c;很长一段时间内 postman 都是我做接口测试的首选工具&#xff0c;之前也有小伙伴跟我安利过 IDEA 中的 RestfulToolkit 插件&#xff0c;但是一直没机会体验&#xff0c;最近抽空玩了一把&#xff0c;感觉在某些场景下还蛮不错的(不需要认…

互联网SaaS产品的账户体系应该如何设计-账户分析

在进行账户体系设计之前&#xff0c;需要先理清产品使用群体、付费群体&#xff0c;这两个统称为用户群体&#xff0c;还需要了解产品的使用场景、产品功能以及产品的商业模式。从产品战略顶层进行SaaS产品的用户体系设计。 我们首先对人的本质和价值进行深入的分析&#xff1b…

你的测试技术这么烂,不学几招怎么跳槽?

最近几年我一直担任着软件测试面试官的角色&#xff0c;正好过年回来&#xff0c;马上就要金三银四求职季了&#xff0c;所以想写点面试的经验分享给大家&#xff0c;希望能对大家有些帮助。碍于才疏学浅&#xff0c;又是理工出身&#xff0c;字里行间未免词不达意&#xff0c;…

ubuntu20.04 使用pip安装配置Pytorch

关于pytorch的安装&#xff0c;我之前其实写过一篇博客&#xff1a;解决问题&#xff1a;import torch失败和torch.cuda.is_available()返回false 但是那是在windows下的&#xff0c;在ubuntu双系统下好像情况有点不一样&#xff0c;但是所幸踩的坑不算多&#xff0c;这里总结如…

2023全云在线联合微软AIGC专场沙龙:人工智能与企业创新,促进创造力的数字化转型

6月29日&#xff0c;由全云在线平台和微软联合主办的人工智能与企业创新&#xff1a;促进创造力的数字化转型——2023AIGC微软专场沙龙在广州天河区正佳万豪酒店举行。 关于2023AIGC微软专场沙龙 GPT翻开了AGI新的一页&#xff0c;也翻开了各行各业的新篇章。 2022年11月30日…

当心僵尸:过时Linux内核的安全风险

导读设备年年新&#xff0c;内核永不换。早该被淘汰的Linux内核版本&#xff0c;依然阴魂不散地扎根在各种各样的设备中&#xff0c;驱动着这些设备如同《行尸走肉》的丧尸游荡在世界各地。 Linux内核安全漏洞是新闻头条常客。最近又有一个隐身十年之久的严重内核漏洞被曝光了…

layer做阻塞式弹出层的方法

今天遇到一个问题&#xff1a;文章来源地址https://www.yii666.com/article/301050.html?actiononAll layer弹出一个confirm提示窗&#xff0c;然后confirm还没有点击对应的按钮的时候&#xff0c;就已经执行了后续代码&#xff0c;我这里做出的判断是&#xff0c;是否需要进行…