【Java】SPI在Java中的实现与应用

一、SPI的概念

1.1、什么是API?

API在我们日常开发工作中是比较直观可以看到的,比如在 Spring 项目中,我们通常习惯在写 service 层代码前,添加一个接口层,对于 service 的调用一般也都是基于接口操作,通过依赖注入,可以使用接口实现类的实例。

如上图所示,服务调用方无需关心接口的定义与实现,只进行调用即可,接口、实现类都是由服务提供方提供。服务提供方提供的接口与其实现方法就可称为API,API中所定义的接口无论是在概念上还是具体实现,都更接近服务提供方(实现方),通常接口与实现类在同一包中。

1.2、什么是SPI?

如果我们将接口的定义放在调用方,服务的调用方定义一个接口规范,可以由不同的服务提供者实现。并且,调用方能够通过某种机制来发现服务提供方,通过调用接口使用服务提供方提供的功能,这就是SPI的思想。

SPI 全称 Service Provider Interface ,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。

服务提供方按接口规范实现服务,服务调用方通过某种机制为这个接口寻找到这个服务, SPI的特点很明显:接口的定义(调用方提供)与具体实现是隔离的(服务提供方提供),使用接口的实现类需要依赖某种服务发现机制。

通过对比,我们可以看出接口在APISPI中的含义还是有很大的不同,总的来说,API 中的接口是更像是服务提供者给调用者的一个功能列表,而 SPI 中更多强调的是,服务调用者对服务实现的一种约束。

二、为什么要使用SPI?

  • 面向接口编程: 面向对象的设计与编程中,我们经常强调“依赖抽象而不是具体”,这样做就是为了实现高内聚、低耦合,提供代码灵活性和可维护性等等。

  • 提供标准标准但没有具体实现的业务场景: SPI 机制的使用场景就是没有统一实现标准的业务场景。一般就是,服务调用方有定义好的标准接口,但是没有统一的实现,需要服务提供方提供其具体实现。

  • 解耦 SPI 机制优势就是低耦合。将接口的定义以及具体实现分离,可以实现运行时根据业务实际场景启用或者替换具体实现类。

三、Java中如何使用SPI

接口定义、服务实现这些我们都轻车熟路,调用方直接依赖接口不依赖具体实现,这是依赖倒置原则,我们在Spring项目中使用API时,会使用Spring的依赖注入(DI)来实现“服务发现”,同样地,SPI的重点也是如何让调用方发现接口的具体实现,也就是上文提到的某种服务发现机制。

SPI的服务发现机制是由ServiceLoader提供,ServiceLoader是Java在JDK 6中引进的新特性,它主要是用来发现并加载一系列的service provider。当服务的提供者,提供了服务接口的一种实现之后,只需要在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件,该文件的内容就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并加载实现类,完成依赖的注入,这就是Java SPI的服务发现机制。

下面就结合一个示例来具体讲讲。若有这样一个需求,需要使用一个接口来完成内容查找服务,接口的具体实现交给其他服务提供方,实现可能是基于文件系统的查找,也可能是基于数据库的查找。

3.1、定义接口

提供一个查找服务标准接口,先定义调用方的内容查找方法:

// 查找服务接口
public interface Search {
    // 按关键字查询内容方法
     String searchDoc(String keyword);
}

这个接口就是给服务提供方来实现的,将它打包发布mvn clean install,确保maven仓库中有该jar包,之后提供者在项目中就可以引入这个 jar 包了。

3.2、服务实现

制定并发布完标准接口后,我们假设第一个服务提供方提供了一种文件查找的实现。新建项目search-file,并引入刚才发布的标准接口jar包:

<dependency>
    <groupId>com.blblccc.search</groupId>
    <artifactId>search-standard</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

实现定义好的接口:

public class FileSearch implements Search {

    @Override
    public String searchDoc(String keyword) {
        return "文件查找:" + keyword;
    }
}

并在项目的resources的目录下,创建META-INF/services目录,然后以前面定义的接口名com.blblccc.spi.learn.Search创建文件,并在文件中写入实现类的全限定名。

com.blblccc.file.search.FileSearch

一个服务方的简单实现就完成了,用maven打成 jar 包,发布到maven之后就可以提供给调用方使用了。

接着,按上述实现方式,再创建一个项目search-database使用数据库的实现接口:

public class DatabaseSearch implements Search {
    @Override
    public String searchDoc(String keyword) {
        return "数据库查找:" + keyword;
    }
}

同样,打包发布后就可以提供给调用方使用了。

3.1、服务发现

接下来关键的一步就是服务发现,服务发现需要依赖ServiceLoader的使用。创建一个新项目search-sever,引入上面打好的两个提供方的 jar 包。

<dependencies>
    <dependency>
        <groupId>com.blblccc.search</groupId>
        <artifactId>search-file</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.blblccc.search</groupId>
        <artifactId>search-database</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

虽然每个服务提供者对于接口都有不同的实现,但是作为调用者来说,它并不需要关心具体的实现类,我们要做的是通过接口来调用服务提供者实现的方法。

下面,就是关键的服务发现环节,使用ServiceLoader来加载具体的实现类,调用方只需调用对应接口方法即可。

public class SearchDoc {

    public static void main(String[] args) {
        new SearchDoc().searchDocByKeyWord("hello world");
    }

    public void searchDocByKeyWord(String keyWord) {

        ServiceLoader<Search> searchServiceLoader = ServiceLoader.load(Search.class);

        for (Search search : searchServiceLoader){
            String doc = search.searchDoc(keyWord);
            System.out.println(doc);
        }
    }
}

测试结果:

文件查找:hello world
数据库查找:hello world

可以看到,通过定义的Search发现了两个实现类。整段代码中没有出现过具体的服务实现类,操作都是通过接口调用。

四、SPI实现原理

4.1、Java的类加载器

首先Java的类加载器可被分为四类

启动类加载器Bootstrap ClassLoader

  • 用于加载Java的核心类,由底层的 C++ 实现。启动类加载器不属于 Java 类库,无法被 Java 程序直接引用。

  • Bootstrap ClassLoader 的 parent 属性为 null

标准扩展类加载器 Extension ClassLoader

  • 由 sun.misc.Launcher$ExtClassLoader 实现

  • 负责加载 JAVA_HOME 下 libext 目录下的或者被 java.ext.dirs 系统变量所指定的路径中的所有类库

应用类加载器 Application ClassLoader

  • 由 sun.misc.Launcher$AppClassLoader 实现

  • 负责在 JVM 启动时加载用户类路径上的指定类库

用户自定义类加载器 User ClassLoader

  • 当上述 3 种类加载器不能满足开发需求时,用户可以自定义加载器

  • 自定义类加载器时,需要继承 java.lang.ClassLoader 类。如果不想打破双亲委派模型,那么只需要重写 findClass 方法即可;如果想打破双亲委派模型,则需要重写 loadClass 方法

4.2、双亲委派机制

  • 双亲委派机制的关系链如下:

Bootstrap引导类加载器 → Extension拓展类加载器 → Application系统类加载器 → User自定义类加载器

  • 双亲委派模式的好处:这种自下而上的层级设计,能避免类的重复加载,同时防止Java的核心API类在运行时被篡改,减少性能开销和安全隐患问题。

  • 双亲委派模式的弊端: 无法做到不委派(必须首先上浮到最顶层),也无法向下委派(顶层优先)。

4.3、SPI为什么要打破双亲委派机制?

这里有一条重要原则:类加载器可见性原则

  • 子类加载器能查看父类加载器加载的所有类,但是父类加载器不能查看子类加载器所加载的类

位于rt.jar包中的SPI接口,是由Bootstrap类加载器完成加载的,而classpath路径下的SPI实现类,则是App类加载器进行加载的。但往往在SPI接口中,会经常调用实现者的代码,所以一般会需要先去加载自己的实现类,但实现类并不在Bootstrap类加载器的加载范围内,而经过前面的双亲委派机制的分析,我们已经得知:子类加载器可以将类加载请求委托给父类加载器进行加载,但这个过程是不可逆的。也就是父类加载器是不能将类加载请求委派给自己的子类加载器进行加载的,所以此时就出现了这个问题:如何加载SPI接口的实现类?答案是打破双亲委派模型。

以JDBC举例来说:

ServiceLoader和DriverManager类以及SPI接口类都是rt核心包的系统类,它们都是启动类加载器bootstrap classloader加载的,而第三方jar包是在classPath下的,启动类加载器加载不了,也无法委派给父加载器加载,所以我们就需要破坏双亲委派机制,指定一个类加载器去加载。

4.4、SPI底层实现原理

要搞清楚这个问题的原因,得先确认我们使用SPI的入口:

ServiceLoader<Xxxx> serviceLoader = ServiceLoader.load(Xxxx.class);

进入该方法,寻找其实现的方式:

注意此处获取了当前线程的类加载器,而在线程中调用该类方法的是我们用户自己。那么这里就理解为获取到了用户的类加载器。

再往该方法中查找,找到该段代码:

注意该段代码中,cl为上一步获取到的类加载器,如果发现类加载器不存在,会再次获取系统默认加载器,这个系统默认加载器在常规情况下是用于加载启动类的加载器(jdk注释中解释),而启动类则是我们用户自己定义的类,这里毋庸置疑也会是应用类加载器。

从上面的代码中我们总结出来,ServiceLoader获取了我们的应用类加载器,至此load方法入口基本上没有其他内容可以细看。

为减轻文章阅读压力,直接跳转到该方法

java.util.ServiceLoader.LazyIterator#nextService

注意这里的loader是我们前面获取到的应用类加载器,这个方法中是获取到了具体需要实例化的实现类,即将对其进行实例化, 在这之前需要先获取到Class,这里使用Class.forName(class, false, ClassLoader)方法,这个方法的含义是使用指定的类加载器去加载指定的类。既然这里的类加载器是应用类加载器,那么类加载顺序自然就又回到了应用类加载器-->扩展类加载器-->BootStrap类加载器-->扩展类加载器-->应用类加载器,能加载到我们想要的类也就不奇怪了。

综上,Java SPI的实现是依靠ServiceLoader,ServiceLoader通过使用线程上下文类加载器来加载SPI接口实现类,实现类的全路径名需配置在META-INF/services/目录下,以接口名命名的文件内容中,ServiceLoader会读取文件中的全路径名,通过反射机制实例化接口实现类。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Git】如何安装git,项目中使用git上传到远程仓库,使用git中对多人使用出现的版本问题的解决

前言&#xff1a; 一&#xff0c;Git的介绍&#xff0c;安装&#xff0c;与SVN的对比 1.1Git的介绍 Git 是一个开源的分布式版本控制系统&#xff0c;用于敏捷高效地处理任何或小或大的项目。 Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控…

民生画派创始人张龙(天驰)作品

简介 张龙&#xff08;天驰&#xff09; 中国民生画派创始人 首届“陆俨少奖”金奖得主 人民大学巨幅主题创作高级研修班导师 中央美院客座教授 神舟十二号载人飞船遨游太空搭载作品创作者 被评为2021、2022年年度最具收藏价值艺术家 中国美术家协会会员 中国美术家协…

【数据结构】单链表OJ题(二)

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《Linux》《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 文章目录 一、分割链表二、回文链表三、相交链表四、环形链表 I五、环形链表 II 六、链表的深度拷…

17 Linux 中断

一、Linux 中断简介 1. Linux 中断 API 函数 ① 中断号 每个中断都有一个中断号&#xff0c;通过中断号可以区分出不同的中断。在 Linux 内核中使用一个 int 变量表示中断号。 ② request_irq 函数 在 Linux 中想要使用某个中断是需要申请的&#xff0c;request_irq 函数就是…

【python海洋专题四十四】海洋指数画法--多色渐变柱状图

【python海洋专题四十四】海洋指数画法–多色渐变柱状图

winform开发小技巧

如果我们不知道怎么在代码中new 一个控件&#xff0c;我们可以先在窗体中拉一个然后看Form1.Designer.cs 里面生成的代码就是我们要的 我们会在下面看到 还有泛型的使用&#xff0c;马上更新

Termius for Mac:掌控您的云端世界,安全高效的SSH客户端

你是否曾经在Mac上苦苦寻找一个好用的SSH客户端&#xff0c;让你能够远程连接到Linux服务器&#xff0c;轻松管理你的云端世界&#xff1f;现在&#xff0c;我们向你介绍一款强大而高效的SSH客户端——Termius。 Termius是一款专为Mac用户设计的SSH客户端&#xff0c;它提供了…

JavaScript从入门到精通系列第三十二篇:详解正则表达式语法(一)

文章目录 一&#xff1a;正则表达式 1&#xff1a;量词设置次数 2&#xff1a;检查字符串以什么开头 3&#xff1a;检查字符串以什么结尾 4&#xff1a; 同时使用开头结尾 5&#xff1a;同值开头同值结尾 二&#xff1a;练习 1&#xff1a;检查是否是一个手机号 大神链…

『MySQL快速上手』-⑤-数据类型

文章目录 1.数据类型有哪些2.数值类型2.1 tinyint 类型2.2 bit 类型2.3 小数类型2.3.1 float2.3.2 decimal3.字符串类型3.1 char3.2 varchar3.2 char 和 varchar 比较4.日期和时间类型5.enum和set1.数据类型有哪些 MySQL支持多种数据类型,这些数据类型可用于定义表中的列,以…

Selenium关于内容信息的获取读取

在进行自然语言处理、文本分类聚类、推荐系统、舆情分析等研究中,通常需要使用新浪微博的数据作为语料,这篇文章主要介绍如果使用Python和Selenium爬取自定义新浪微博语料。因为网上完整的语料比较少,而使用Selenium方法有点简单、速度也比较慢,但方法可行,同时能够输入验…

【Unity ShaderGraph】| 如何快速制作一个炫酷的 全息投影效果

前言 【Unity ShaderGraph】| 如何快速制作一个炫酷的 全息投影效果一、效果展示二、 全息投影效果 前言 本文将使用ShaderGraph制作一个 炫酷的 全息投影效果 &#xff0c;可以直接拿到项目中使用。对ShaderGraph还不了解的小伙伴可以参考这篇文章&#xff1a;【Unity Shader…

三国志14信息查询小程序(历史武将信息一览)制作更新过程06-复现小程序

0&#xff0c;所需文件 所需全部文件下载 文件总览&#xff1a; 代码&#xff1a; 数据库&#xff1a; 1&#xff0c;前期准备 假定你已经看过前面的文章&#xff0c;并完成了下列准备&#xff1a; &#xff08;1&#xff09;一台有公网IP的云服务器&#xff0c;服务器上…

Oracle 三种分页方法(rownum、offset和fetch、row_number() over())

Oracle的三种分页指的是在进行分页查询时&#xff0c;使用三种不同的方式来实现分页效果&#xff0c;分别是使用rownum、使用offset和fetch、使用row_number() over() 1、使用rownum rownum是oracle中一个伪劣&#xff0c;它用于表示返回的行的序号。使用rownum进行分页查询的方…

数据结构之单链表基本操作

&#x1f937;‍♀️&#x1f937;‍♀️&#x1f937;‍♀️ 今天给大家分享的是单链表的基本操作。 清风的个人主页 &#x1f389;欢迎&#x1f44d;点赞✍评论❤️收藏 &#x1f61b;&#x1f61b;&#x1f61b;希望我的文章能对你有所帮助&#xff0c;有不足的地方还请各位…

大数据毕业设计选题推荐-农作物观测站综合监控平台-Hadoop-Spark-Hive

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

计算当月工作日时间进度

目录 1.按一个月平均算 2.除去星期六星期天算 3.自定义节假日算 1.按一个月平均算 // 获取当前时间 const now new Date(); // 获取当前年份和月份 const currentYear now.getFullYear(); const currentMonth now.getMonth() 1; // 计算当月天数 const daysInMonth ne…

【Docker】iptables基本原理

在当今数字化时代&#xff0c;网络安全问题变得越来越重要。为了保护我们的网络免受恶意攻击和未经授权的访问&#xff0c;我们需要使用一些工具来加强网络的安全性。其中&#xff0c;iptables是一个强大而受欢迎的防火墙工具&#xff0c;它可以帮助我们控制网络流量并保护网络…

【剑指offer|图解|双指针】训练计划 I + 删除有序数组中的重复项

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;数据结构、算法模板 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. ⛳️训练计划 I二. ⛳️查找总价格为目标值的两个商品三. ⛳️删除有序数组中的…

【JAVA学习笔记】67 - 坦克大战1.5 - 1.6,防止重叠,记录成绩,选择是否开新游戏或上局游戏,播放游戏音乐

项目代码 https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter20/src 增加功能 1.防止敌人坦克重叠运动 2.记录玩家的成绩&#xff0c;存盘退出 3.记录当时的敌人坦克坐标&#xff0c;存盘退出 4.玩游戏时&#xff0c;可以选择是开新游戏还是继续上局…

尚硅谷大数据项目《在线教育之实时数仓》笔记007

视频地址&#xff1a;尚硅谷大数据项目《在线教育之实时数仓》_哔哩哔哩_bilibili 目录 第9章 数仓开发之DWD层 P053 P054 P055 P056 P057 P058 P059 P060 P061 P062 P063 P064 P065 第9章 数仓开发之DWD层 P053 9.6 用户域用户注册事务事实表 9.6.1 主要任务 读…