SPI在Java中的实现与应用 | 京东物流技术团队

news2025/4/5 17:16:06

1 SPI的概念

API

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

简单形容就是这样的:

图1:API

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

SPI

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

SPI 的全称是Service Provider Interface,字面意思就是服务提供者的接口,是由服务提供者定义的接口。

图2:SPI

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

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

2 为什么要使用SPI

  • 面向接口编程: 面向对象的设计与编程中,我们经常强调“依赖抽象而不是具体”,这样做就是为了实现高内聚、低耦合,提供代码灵活性和可维护性等等。
  • 提供标准标准但没有具体实现的业务场景: SPI 机制的使用场景就是没有统一实现标准的业务场景。一般就是,服务调用方有定义好的标准接口,但是没有统一的实现,需要服务提供方提供其具体实现。
  • 解耦: SPI 机制优势就是低耦合。将接口的定义以及具体实现分离,可以实现运行时根据业务实际场景启用或者替换具体实现类。

3 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的服务发现机制。

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

(1)定义接口

作为服务调用方,需要先定义一套接口规范,用来规范之后的服务提供方按规范来实现接口。这样,不管是谁提供的实现方法,调用方都可以按相同的方式来调用接口。

创建一个项目search-standard,提供一个查找服务标准接口,先定义调用方的内容查找方法:

package com.gwz.spi.learn;

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

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

(2)服务实现

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

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

实现定义好的接口:

package com.gwz.file.search;

import com.gwz.spi.learn.Search;

public class FileSearch implements Search {

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

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

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

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

package com.gwz.database.search;

import com.gwz.spi.learn.Search;

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

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

(3)服务发现

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

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

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

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

package com.gwz.search.impl;

import com.gwz.spi.learn.Search;
import java.util.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);
        }
    }
}

测试结果:

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

4 Java SPI原理

了解了SPI的工作流程,我们应该有以下疑问:

  • 为什么要在服务提供方的META-INF/services/目录里同时创建一个以服务接口命名的文件?放在其他目录里面不行吗,文件名我随意命名不可以吗?
  • 为什么文件的内容需要是实现该服务接口的具体实现类?
  • ServiceLoader是如何发现接口的服务类的?

接下来我们看一下ServiceLoader的源码就可以解答了。

上述例子中,通过ServiceLoader.load(Search.class) 来加载Search接口的实现类,我们知道Java加载类都离不开类加载器,查看ServiceLoader.load()方法的源码就会发现,SPI加载类使用的是线程上下文加载器,可通过java.lang.Thread#setContextClassLoader方法进行设置,若未设置则会从父线程中继承,在应用程序全局都未设置的情况下,默认是应用程序类加载器,线程上下文加载器加载所需的SPI代码,实际上是父类加载器请求子类加载器来完成加载类的动作,打破了双亲委派模型的层次结构。

load方法实际上构造了一个ServiceLoader实例对象,该对象保持了一个加载SPI类代码的线程上下文加载器的引用loader、一个所需要加载实现类的接口类型的引用service、一个已经成功服务提供者(接口的具体实现类)的缓存providers。

上述例子中我们使用了foreach遍历调用接口方法,本质上是通过调用迭代器Iterable的next()方法来获取的具体实现类,因为ServiceLoader实现了Iterable这一接口,而整个服务发现的核心,就在它的iterator()方法中。

这里面有两个关键的东西,一是providers,在迭代器中会先从服务类缓存中查找服务类,若查不到就用lookupIterator查找。接着往下看LazyIterator的hasNext()和next()源码实现。

acc是一个安全管理器,debug 看值是null,所以看hasNextService()和nextService()方法就可以了。在hasNextService()方法中,会通过PREFIX + service.getName()来拼接一个资源路径URL,拼接这个URL的目的就是为了能供通过该URL获取文件中的内容,看到这里应该就明白了为什么实现SPI服务时,需要创建名为META-INF/services/的文件夹,以及以接口名命名的文件,这是由PREFIX与service.getName()决定的,那么下图中的实现类名称是如何来的呢?

既然我们这里可以通过PREFIX + service.getName()来拼接一个资源路径URL,那是不是可以通过该URL来获取所指向的文件资源的内容?答案是肯定的,正如源码中parse()方法的实现一样, 通过Java提供的InputStream读取URL指向的文件内容。

在读取文件内容时ServiceLoader主要做了几件事:

  • 获取文本内容(实现类全路径名);
  • 校验内容合法性(是否符合Java类命名规范);
  • 若providers缓存中不存在该实现类(未加载),则保存该实现类全路径名以供下面流程进行加载该类;

接下来,在nextService()方法中,则会通过上述解析到的实现类全路径名加载这个实现类,然后实例化对象,最终放入缓存中去。

在迭代器的迭代过程中,会基于Java 反射机制去完成所有实现类的实例化,这样就可以调用接口方法来使用SPI服务提供方实现的具体功能。

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

5 应用

(1)日志框架slf4j

SPI的实际应用,最常见的应该是日志框架slf4j,它就是个日志门面,并不提供具体的实现,需要绑定其他具体实现。例如可使用log4j2作为具体的绑定器,只需要在 pom 中引入slf4j-log4j12,就可以使用具体功能。

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.3</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>2.0.3</version>
</dependency>

引入项目后,点开它的 jar 包看一下具体结构:

jar 包的META-INF.services里面,通过 SPI 注入了Reload4jServiceProvider这个实现类,它实现了SLF4JServiceProvider这一接口,在它的初始化方法initialize()中,会完成初始化等工作,后续可以继续获取到LoggerFactory和Logger等具体日志对象。

(2)DriverManager

DriverManager是JDBC里管理数据库驱动的的工具类。一个数据库可能会存在不同实现的数据库驱动。我们在使用特定的驱动实现时,通过一个简单的配置就而不用修改代码就可以达到效果。 例如,在运用Class.forName(“com.mysql.jdbc.Driver”)加载mysql驱动后,会执行其中的静态代码把driver注册到DriverManager中。

查看JDBC源码可知,驱动实现接口java.sql.Driver,然后通过registerDriver把当前driver加载到DriverManager中。查看DriverManager的源码,可以看到其内部的静态代码块loadInitialDrivers方法中使用了ServiceLoader:

查看mysql-connector-java的jar包在META-INF/services接口路径文件中的内容,可以看到com.mysql.jdbc.Driver。

(3)sharding-jdbc

sharding-jdbc是一款用于分库分表的中间件,在数据库分布式场景中,为了保证数据库主键的唯一性,会采取相应的主键生成策略,而主键生成策略有很多种实现。sharding-jdbc在主键生成策略使用了SPI进行装配。

源码中的 ShardingRule.class主要封装分库分表的策略规则,包括主键生成,核心在于底层调用的register方法,其中也是使用了ServiceLoader:

这里就是应用的SPI机制,再看下Jar包的META-INF/services/目录:

有两个实现,对应了sharding-jdbc的提供的两种生成策略分别是雪花算法和UUID。

6 总结

Java 中的 SPI 提供了一种比较特别的服务发现和调用机制,通过接口将服务调用与服务提供者分离,将接口提供给第三方实现扩展,体现了依赖倒置的设计思想,是高内聚、低耦合的一种体现。但SPI也有缺点,就是加载一个接口,会把所有实现类都加载进来,可能会加载到不需要的冗余服务。总的来说,SPI是一种非常不错的扩展、集成的思路。

参考资料

https://blog.csdn.net/CHIKA2333/article/details/132082244

《深入理解Java虚拟机:JVM高级特性与最佳实践》–周志明

作者:京东物流 郭纬宙

来源:京东云开发者社区  自猿其说Tech 转载请注明来源

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

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

相关文章

【深度学习实验】前馈神经网络(六):自动求导

目录 一、实验介绍 二、实验环境 1. 配置虚拟环境 2. 库版本介绍 三、实验内容 0. 导入必要的工具包 1. 标量求导 2. 矩阵求导 3. 计算图 一、实验介绍 PyTorch提供了自动求导机制&#xff0c;它是PyTorch的核心功能之一&#xff0c;用于计算梯度并进行反向传播。自动求…

C++流插入和流提取的重载!

C作为C语言的衍生&#xff0c;其弥补了C语言中的很多不足&#xff0c;也对C语言进行了一定的优化&#xff01;今日就来讲解一下C中输入/出流相关的知识&#xff01;以及对输入/出的重载&#xff01;&#xff0c;希望读完本篇文章&#xff0c;能让读者们对C中输入/出流有更深一步…

Java之IO概述以及

1.1 什么是IO 生活中&#xff0c;你肯定经历过这样的场景。当你编辑一个文本文件&#xff0c;忘记了ctrls &#xff0c;可能文件就白白编辑了。当你电脑上插入一个U盘&#xff0c;可以把一个视频&#xff0c;拷贝到你的电脑硬盘里。那么数据都是在哪些设备上的呢&#xff1f;键…

散列(哈希)查找的定义,常见的散列函数设计以及处理哈希冲突方法

1.散列表 1.散列表的定义 散列表(Hash Table)&#xff0c;又称哈希表。 是一种数据结构&#xff0c;特点是:数据元素的关键字与其存储地址直接相关。 特点&#xff1a; 若不同的关键字通过散列函数映射到同一个值&#xff0c;则称它们为“同义词”。通过散列函数确定的位置…

Maven 设置环境变量(Windows、Linux)

文章目录 Windows 配置 Maven 环境变量Linux 配置 Maven 环境变量 如果想在任意路径下都能通过 mvn 命令运行 Maven 程序&#xff0c;就需要将 Maven 程序路径设置到环境变量中&#xff0c; 否则使用 mvn 命令时每次都要加上 Maven 程序的全路径 核心就一句话&#xff0c;把 M…

坚鹏:中国邮政储蓄银行金融科技前沿技术发展与应用场景第4期

中国邮政储蓄银行金融科技前沿技术发展与应用场景第4期培训圆满结束 中国邮政储蓄银行拥有优良的资产质量和显著的成长潜力&#xff0c;是中国领先的大型零售银行。2016年9月在香港联交所挂牌上市&#xff0c;2019年12月在上交所挂牌上市。中国邮政储蓄银行拥有近4万个营业网点…

学习记忆——英语篇——右脑记忆单词

文章目录 英语字母形象起源右脑记忆单词的原则四大步骤第一步&#xff1a;摄取信息第二步&#xff1a;处理信息第三步&#xff1a;储存信息第四步&#xff1a;提取信息 训练例子字母形象训练 右脑记忆单词5大方法字源法编码法字母编码法字母组合编码法 拼音法全拼法拼音组合 熟…

springcloud3 分布式事务解决方案seata之XA模式4

一 seata的模式 1.1 seata的几种模式比较 Seata基于上述架构提供了四种不同的分布式事务解决方案&#xff1a; XA模式&#xff1a;强一致性分阶段事务模式&#xff0c;牺牲了一定的可用性&#xff0c;无业务侵入 TCC模式&#xff1a;最终一致的分阶段事务模式&#xff0c;有…

操作系统:中断和异常

1.中断的作用 CPU上会运行两种程序&#xff0c;一种是操作系统内核程序&#xff08;是整个系统的管理者&#xff09;&#xff0c;一种是应用程序。 1.中断的特点 在合适的情况下&#xff0c;操作系统内核会把CPU的使用权主动让给应用程序。“中断”是让操作系统内核夺回CPu使…

java智慧园区系统源码 智慧园区小程序源码

java智慧园区系统源码 智慧园区小程序源码 技术框架&#xff1a; 核心框架&#xff1a;Spring Boot 2.4.0 安全框架&#xff1a;JwtPermission 3.1.1 前端&#xff1a;Ant Design Vue 1.6.2 持久层框架&#xff1a;MyBatis-Plus 3.4.1 关系型数据库: Mysql 8.0.22 数据库…

【开发篇】一、热部署

文章目录 1、手工启动热部署2、自动启动热部署3、热部署范围配置4、关闭热部署功能 1、手工启动热部署 日常开发与调试&#xff0c;改几行代码想看效果就得手动点重启&#xff0c;很繁琐&#xff0c;接下来考虑启动热部署。首先引入springboot开发者工具&#xff1a; <dep…

找视频背景音乐素材,就上这6个网站。

找背景音乐、BGM、音效素材&#xff0c;就上这6个网站&#xff0c;国内外都有&#xff0c;免费下载&#xff0c;还可以商用&#xff0c;建议收藏起来~ 1、菜鸟图库 https://www.sucai999.com/audio.html?vNTYwNDUx 菜鸟图库是一个综合性素材网站&#xff0c;站内涵盖设计、图…

ceph分布式存储部署

一、概述 是一个统一的分布式存储系统&#xff0c;设计初衷是提供较好的性能、可靠性和可扩展性。 特点 1、统一存储 虽然 ceph 底层是一个分布式文件系统&#xff0c;但由于在上层开发了支持对象和块的接口。所以在开源存储软件中&#xff0c;能够一统江湖。至于能不能千秋万…

单片机学习--->Keil多文件工程

1、在文件夹中创建新的文件夹 目录&#xff1a; App 文件夹&#xff1a;用于存放外设驱动文件&#xff0c;如 LED、数码管、定时器等。 Obj 文件夹&#xff1a;用于存放编译产生的 c/汇编/链接的列表清单、调试信息、 hex 文件、预览信息、封装库等文件。 Public 文件夹&#x…

D. Edge Split

Problem - D - Codeforces 思路&#xff1a;思路想到了&#xff0c;但是不知道用什么方法写。。首先我们先看只有一个树的情况&#xff0c;那么如果我们所有的边是一个颜色&#xff0c;那么答案是1n&#xff0c;如果我们将其中的一条边变色&#xff0c;那么产生的答案是2n-1&am…

buuctf web [ACTF2020 新生赛]Upload

明了但不明显的文件上传 传个试试 行&#xff0c;抓包吧&#xff0c;php格式不行&#xff0c;就先上传要求的格式&#xff1a;jpg、png、gif 抓到上传的包之后&#xff0c;再修改成我们想要的 常见的php格式绕过有&#xff1a;php,php3,php4,php5,phtml,pht 挨个试试 这是上个…

Python 根据身高体重计算体质(BMI)指数

""" 根据身高体重计算体质(BMI)指数知识点&#xff1a;1、计算公式&#xff1a;体质指数(BMI) 体重(KG) / (身高(M) * 身高(M))2、变量类型转换3、运算符幂**&#xff0c;例如&#xff1a;3 ** 2 9 <> 3 * 34、更多的运用请参考&#xff1a;https://blo…

基于矩阵分解算法的智能Steam游戏AI推荐系统——深度学习算法应用(含python、ipynb工程源码)+数据集(一)

目录 前言总体设计系统整体结构图系统流程图 运行环境Python环境TensorFlow环境PyQt5环境 模块实现1. 数据预处理 相关其它博客工程源代码下载其它资料下载 前言 本项目采用了矩阵分解算法&#xff0c;用于对玩家已游玩的数据进行深入分析。它的目标是从众多游戏中筛选出最适合…

糖果传递问题(超详细的数论公式推导+贪心结论+均分问题)

糖果传递问题 文章目录 糖果传递问题问题描述问题分析【公式推导过程】代码 问题描述 有 n 个小朋友坐成一圈&#xff0c;每人有 a[i] 个糖果。 每人只能给左右两人传递糖果。 每人每次传递一个糖果代价为 1。 求使所有人获得均等糖果的最小代价。 输入格式 第一行输入一个正…

【2】贪心算法-综述

前言 从前&#xff0c;有一个很穷的人救了一条蛇的命&#xff0c;蛇为了报答他的救命之 恩&#xff0c;于是就让这个人提出要求&#xff0c;满足他的愿望。这个人一开始只要求简 单的衣食&#xff0c;蛇都满足了他的愿望&#xff0c;后来慢慢地贪欲升起&#xff0c;要求做官&am…