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

news2024/11/25 10:33:23

一、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会读取文件中的全路径名,通过反射机制实例化接口实现类。

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

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

相关文章

已解决:rm: 无法删除“/opt/module/zookeeper-3.4.10/zkData/zookeeper_server.pid“: 权限不够

解决&#xff1a; ZooKeeper JMX enabled by default Using config: /opt/module/zookeeper-3.4.10/bin/../conf/zoo.cfg Stopping zookeeper ... /opt/module/zookeeper-3.4.10/bin/zkServer.sh: 第 182 行:kill: (4149) - 不允许的操作 rm: 无法删除"/opt/module/zooke…

开发知识点-Python

Python从小白到入土 python渗透测试安全工具开发锦集Python安全工具编程基础第一章 Python在网络安全中的应用第一节 Python黑客领域的现状第二节 我们可以用Python做什么第三节 第一章课程内容总结 第二章 python安全应用编程入门第一节 Python正则表达式第二节 Python Web编程…

C++二分查找算法:阶乘函数后 K 个零

涉及知识点 二分查找 数学 题目 f(x) 是 x! 末尾是 0 的数量。回想一下 x! 1 * 2 * 3 * … * x&#xff0c;且 0! 1 。 例如&#xff0c; f(3) 0 &#xff0c;因为 3! 6 的末尾没有 0 &#xff1b;而 f(11) 2 &#xff0c;因为 11! 39916800 末端有 2 个 0 。 给定 k&a…

Python--列表及其应用场景

1.为什么需要列表 思考&#xff1a;有一个人的姓名(laowang)怎么书写存储程序&#xff1f; 用 变量。如&#xff1a;name laowang 但是&#xff0c;如果要记录很多人的名字&#xff0c;怎么办&#xff1f; 思考&#xff1a; 如果一个班级100位学生&#xff0c;每个人的…

17 Linux 中断

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

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

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

mac 卸载第三方输入法

输入法设置里的移除&#xff0c;并不是真的卸载&#xff0c;点击还是能添加回来 在活动监视器里强制退出此输入法在访达界面使用快捷键 ShiftcommandG在弹出的对话框内输入以下路径&#xff08;/资源库/Input Methods&#xff09;&#xff0c;再点击下面的前往找到你要卸载的输…

HadSky+内网穿透打造个人专属社区论坛并远程访问

文章目录 前言1. 网站搭建1.1 网页下载和安装1.2 网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;2.3 Cpolar稳定隧道&#xff08;本地设置&#xff09;2.4 公网访问测试 总结 前言 经过多年的基础…

盘点那些开发中经常用到的git命令

入职第一天 配置邮箱账号 git config —global user.email "XXXX" git config —global user.name "XXXX" 生成公钥 ssh-keygen -t rsa -C "你的邮箱"生成的文件默认在c盘:/用户/当前用户/.ssh文件夹下&#xff0c;也可以指定文件 打开git网页&…

MySQL(12):MySQL数据类型

MySQL中的数据类型 常见数据类型的属性&#xff1a; 整数类型 整数类型一共有 5 种&#xff0c;包括 TINYINT、SMALLINT、MEDIUMINT、INT&#xff08;INTEGER&#xff09;和 BIGINT。 CREATE TABLE test_int1 ( X TINYINT, y SMALLINT, z MEDIUMINT, m INT, n BIGINT );…

Postgresql数据类型-时间类型

PostgreSQL对时间、日期数据类型的支持丰富而灵活&#xff0c;本节介绍PostgreSQL支持的时间、日期类型&#xff0c;及其操作符和常用函数。 PostgreSQL支持的时间、日期类型如表所示。 我们通过一个简单的例子理解这几个时间、日期数据类型&#xff0c;先来看看系统自带的now…

高性能网络编程 - 解读5种I/O模型

文章目录 服务端处理网络请求流程图基础概念阻塞调用 vs 非阻塞调用同步处理 vs 异步处理阻塞、非阻塞 和 同步、异步的区别recvfrom 函数 五种I/O模型I/O模型1&#xff1a;阻塞式 I/O 模型(blocking I/O&#xff09;I/O模型2&#xff1a;非阻塞式 I/O 模型(non-blocking I/O&a…

实用技巧:嵌入式人员使用http服务模拟工具模拟http服务器测试客户端get和post请求

文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/134305752 红胖子(红模仿)的博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬结…

卡尔曼滤波EKF

目录 一、概述 二、卡尔曼滤波的5个公式 三、应用案例&#xff1a;汽车运动 四、应用案例&#xff1a;温度估计 五、总结 一、概述 初学者对于卡尔曼滤波5个公式有点懵&#xff0c;本文先接地气地介绍5个公式&#xff0c;然后举两个常用例子加强理解&#xff0c;同时附有M…

杰里6969系列控制RGB彩灯模式

1、任务目标 实现可以接收到APP传输过来的数据&#xff0c;可以通过传输的数据前几个字节识别到所需要控制的模式&#xff0c;然后提取数据的后几个字节来分别对RGB灯进行不同的的PWM波控制&#xff0c;从而实现跳变和渐变的效果。 2.跳变&#xff08;分布调试记录&#xff09…

创建多层级行索引,创建多层级行索引的DataFrameMultiIndex.from_product()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 创建多层级行索引, 创建多层级行索引的DataFrame MultiIndex.from_product() [太阳]选择题 使用pd.MultiIndex.from_product()&#xff0c;下列输出正确的是&#xff1a; import pandas as pd…

一键批量删除文件名中的空格,高效整理您的文件

你是否曾经因为文件名中多余的空格而烦恼&#xff1f;这些空格不仅影响了文件的美观&#xff0c;还可能导致一些不必要的错误。现在&#xff0c;我们向您介绍一款全新的工具&#xff0c;它可以帮助您一键批量删除文件名中的空格&#xff0c;让您的文件整理更加轻松、高效&#…

一款功能强大的web目录扫描器专业版

dirpro 简介 dirpro 是一款由 python 编写的目录扫描器&#xff0c;操作简单&#xff0c;功能强大&#xff0c;高度自动化。 自动根据返回状态码和返回长度&#xff0c;对扫描结果进行二次整理和判断&#xff0c;准确性非常高。 已实现功能 可自定义扫描线程 导入url文件进…

劲松中西医医院专家谭巍医生解读:生殖衣原体阳性是什么意思

生殖衣原体阳性是指生殖系统出现了衣原体感染。衣原体是我们身体中常见的菌群&#xff0c;生殖系统常见的衣原体有&#xff1a;沙眼衣原体&#xff0c;当然还包括一些支原体&#xff0c;如&#xff1a;人型支原体、解脲支原体等&#xff0c;都可引起生殖系统出现一系列的感染症…

华为L410上制作内网镜像模板02

原文链接&#xff1a;华为L410上制作离线安装软件模板02 hello&#xff0c;大家好啊&#xff0c;今天给大家带来第二篇在内网搭建Apache服务器&#xff0c;用于安装完内网操作系统后&#xff0c;在第一次开机时候&#xff0c;为系统安装软件的文章&#xff0c;今天给大家介绍在…