当前位置: 首页 > news >正文

探索SPI:深入理解原理、源码与应用场景

文章目录

  • 一、初步认识
    • 1、概念
    • 2、工作原理
    • 3、作用场景
  • 二、源码分析
    • 1、ServiceLoader结构
    • 2、相关字段
    • 3、核心方法
  • 三、案例
    • connector连接器小案例
      • 1、新建SPI项目
      • 2、创建扩展实现项目1-MongoDB
      • 3、创建扩展实现项目2-Oracle
      • 4、测试
    • Spring应用
      • 1、创建study工程
      • 2、创建forlan-test工程
      • 3、进阶使用

一、初步认识

1、概念

SPI,全称为 Service Provider Interface,是Java提供的一种服务发现机制,用于实现组件之间的解耦和扩展。

它允许开发人员定义一组接口(Service Interface),并允许其他开发人员通过实现这些接口来提供具体的服务实现(Service Provider),而无需修改Java平台的源代码。

2、工作原理

  • 定义接口:开发人员首先定义一个接口,该接口定义了一组操作或功能。
  • 提供实现:其他开发人员可以通过实现该接口来提供具体的服务实现。这些实现通常以独立的模块或库的形式提供。
  • 配置文件:在Java的SPI机制中,开发人员需要在META-INF/services目录下创建一个以接口全限定名命名的文件,文件内容为提供该接口实现的类的全限定名列表。
  • 加载服务:Java的SPI机制会在运行时自动加载并实例化这些服务提供者的实现类,使得开发人员可以通过接口来访问具体的服务实现。

3、作用场景

它提供了一种松耦合的方式(可插拔的设计)来扩展应用程序的功能。通过SPI,开发人员可以在不修改核心代码的情况下,通过添加新的实现来增加应用程序的功能,像很多框架都使用到了,比如Dubbo、JDBC。

通过服务方指定好接口,具体由第三方去实现,就像JDBC中定义好了一套规范,MySQL、Oracle、MongoDB按照这套规范具体去实现,通过在ClassPath路径下的META-INF/services文件夹中查找文件,自动加载文件里所定义的类。

二、源码分析

核心类:ServiceLoader,核心方法:load。

ServiceLoader是加载SPI服务的入口,通过调用ServiceLoader.load()方法,可以加载指定的Service,会根据配置文件中指定的包名和类名,动态地加载符合条件的所有实现类,并创建一个Service Provider的集合,通过遍历这个集合,可以获取具体的实现类对象。

1、ServiceLoader结构

在这里插入图片描述

2、相关字段

// 配置文件的路径
private static final String PREFIX = "META-INF/services/";// 正在加载的服务,类或者接口
private final Class<S> service;// 类加载器
private final ClassLoader loader;// 访问控制上下文对象
private final AccessControlContext acc;// 缓存已经加载的服务类,按照顺序实例化
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();// 内部类,真正加载服务类
private LazyIterator lookupIterator;

3、核心方法

创建了一些属性service和loader等,最重要的是实例化了内部类LazyIterator

public final class ServiceLoader<S> implements Iterable<S> {/*** Creates a new service loader for the given service type, using the* current thread's {@linkplain java.lang.Thread#getContextClassLoader* context class loader}.*/public static <S> ServiceLoader<S> load(Class<S> service) {// 获取当前线程的上下文类加载器ClassLoader cl = Thread.currentThread().getContextClassLoader();// 通过请求的Class和ClassLoader创建ServiceLoaderreturn ServiceLoader.load(service, cl);}private ServiceLoader(Class<S> svc, ClassLoader cl) {// 加载的接口不能为空service = Objects.requireNonNull(svc, "Service interface cannot be null");// 类加载器loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;// 访问权限的上下文对象acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;reload();}/*** Clear this loader's provider cache so that all providers will be* reloaded.*/public void reload() {// 清空已经加载的服务类providers.clear();// 实例化内部类迭代器LazyIterator lookupIterator = new LazyIterator(service, loader);}
}

LazyIterator很重要,查找实现类和创建实现类的过程,都在它里面完成。

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() {省略详细代码...}private S nextService() {省略详细代码...}
}

当我们调用iterator.hasNext,实际上调用的是LazyIterator的hasNextService方法,判断是否还有下一个服务提供者

private boolean hasNextService() {if (nextName != null) {return true;}if (configs == null) {try {// private static final String PREFIX = "META-INF/services/";// META-INF/services/ + 该对象表示的类或接口的全限定类名(类路径+接口名)String fullName = PREFIX + service.getName();// 将文件路径转成URL对象if (loader == null)configs = ClassLoader.getSystemResources(fullName);elseconfigs = loader.getResources(fullName);} catch (IOException x) {fail(service, "Error locating configuration files", x);}}while ((pending == null) || !pending.hasNext()) {// Enumeration<URL> configs是否包含更多元素if (!configs.hasMoreElements()) {return false;}// 解析URL文件对象,读取内容pending = parse(service, configs.nextElement());}// 拿到下一个实现类的类名nextName = pending.next();return true;
}
private S nextService() {

当我们调用iterator.next方法的时候,实际上调用的是LazyIterator的nextService方法,获取下一个服务提供者,它通过反射的方式,创建实现类的实例并返回

private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn = nextName;nextName = null;Class<?> c = null;try {// 创建类的Class对象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 {// 通过newInstance实例化S p = service.cast(c.newInstance());// 放入providers缓存providers.put(cn, p);return p;} catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x);}throw new Error();          // This cannot happen
}

三、案例

connector连接器小案例

1、新建SPI项目

导入依赖到pom.xml

<artifactId>java-spi-connector</artifactId>

写1个简单接口

public interface IBaseInfo {public void url();
}

2、创建扩展实现项目1-MongoDB

导入依赖到pom.xml

<artifactId>mongodb-connector</artifactId><dependencies><dependency><groupId>cn.forlan</groupId><artifactId>java-spi-connector</artifactId><version>1.0-SNAPSHOT</version></dependency>
</dependencies>

写1个简单实现类,重新url方法,打印mongoDB:url

public class MongoDBBaseInfo implements IBaseInfo{@Overridepublic void url() {System.out.println("mongoDB:url");}
}

在resources目录下创建 META-INF/services目录,创建一个文件,命名为接口的类路径+接口名(必须),内容为实现类路径+类名
在这里插入图片描述

3、创建扩展实现项目2-Oracle

导入依赖到pom.xml

<artifactId>oracle-connector</artifactId><dependencies><dependency><groupId>cn.forlan</groupId><artifactId>java-spi-connector</artifactId><version>1.0-SNAPSHOT</version></dependency>
</dependencies>

写1个简单实现类,重新url方法,打印oracle:url

public class OracleBaseInfo implements IBaseInfo{@Overridepublic void url() {System.out.println("oracle:url");}
}

在resources目录下创建 META-INF/services目录,创建一个文件,命名为接口的类路径+接口名(必须),内容为实现类路径+类名
在这里插入图片描述

4、测试

测试方法

ServiceLoader<IBaseInfo> serviceLoader = ServiceLoader.load(IBaseInfo.class);
Iterator<IBaseInfo> iterator = serviceLoader.iterator();
while (iterator.hasNext()){IBaseInfo next = iterator.next();next.url();
}

它会根据你导入不同的依赖出现不同的效果

  • 导入MongoDB
    在这里插入图片描述
  • 导入Oracle
    在这里插入图片描述

Spring应用

我们要说的应用就是SpringFactoriesLoader工具类,类似Java中的SPI机制,只不过它更优,不会一次性加载所有类,可以根据key进行加载
作用:从classpath/META-INF/spring.factories文件中,根据key去加载对应的类到spring IoC容器中

1、创建study工程

创建ForlanCore类

package cn.forlan.spring;public class ForlanCore {public void code() {System.out.println("Forlan疯狂敲代码");}
}

创建ForlanConfig配置类

package cn.forlan.spring;import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;@Configurable
public class ForlanConfig {@Beanpublic ForlanCore forlanCore() {return new ForlanCore();}
}

2、创建forlan-test工程

打包study为jar,引入依赖

<dependency><groupId>cn.forlan</groupId><artifactId>study1</artifactId><version>1.0-SNAPSHOT</version>
</dependency>

测试获取属性

@SpringBootApplication
public class ForlanTestApplication {public static void main(String[] args) {ApplicationContext applicationContext = SpringApplication.run(ForlanTestApplication.class, args);ForlanCore fc=applicationContext.getBean(ForlanCore.class);fc.code();}
}

运行报错,原因很简单,ForlanCore在spring容器中找不到,没有注入

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'cn.forlan.spring.ForlanCore' availableat org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:352)at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:343)at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1127)at cn.forlan.ForlanTestApplication.main(ForlanTestApplication.java:12)

解决方法
在study工程的resources下新建文件夹META-INF,在文件夹下面新建spring.factories文件,配置key和value,然后重新打包即可

org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.forlan.spring.ForlanConfig

注:key=EnableAutoConfiguration的全路径,value=配置类的全路径

3、进阶使用

指定配置文件生效条件
在META-INF/增加配置文件,spring-autoconfigure-metadata.properties

cn.forlan.spring.ForlanConfig.ConditionalOnClass=cn.forlan.spring.Study

格式:自动配置的类全名.条件=值
该配置的意思是,项目中com.forlan.spring包下存在Study,才会加载ForlanConfig
执行之前的测试用例,运行报错
解决:在当前工程指定包下创建一个Study即可

相关文章:

探索SPI:深入理解原理、源码与应用场景

文章目录 一、初步认识1、概念2、工作原理3、作用场景 二、源码分析1、ServiceLoader结构2、相关字段3、核心方法 三、案例connector连接器小案例1、新建SPI项目2、创建扩展实现项目1-MongoDB3、创建扩展实现项目2-Oracle4、测试 Spring应用1、创建study工程2、创建forlan-test…...

Web3名词解释

Web3名词解释 以太坊 ERC20 Defi去中心化金融 Defi是Decentralized Finance的英文缩写。 简单理解点就是与传统的高度中心化金融体系相比&#xff0c;去中心化金融是通过区块链技术&#xff0c;比如基于区块链技术开发的手机钱包软件&#xff0c;通过智能合约代码以实现去除…...

Vatee万腾外汇市场新力量:vatee科技决策力

在当今数字化时代&#xff0c;Vatee万腾崭露头角&#xff0c;以其强大的科技决策力进军外汇市场&#xff0c;成为该领域的新力量。这一新动向将不仅塑造外汇市场的未来&#xff0c;也展现Vatee科技决策力在金融领域的引领作用。 Vatee万腾带着先进的科技决策力进入外汇市场&…...

【HarmonyOS开发】配置开发工具DevEco Studio

1、下载 注意&#xff1a; 1、安装过程中&#xff0c;一定要自定义安装位置&#xff0c;包比较大&#xff0c;包比较大&#xff0c;包比较大&#xff01;&#xff01;&#xff01; 2、可以将该工具添加到右键中&#xff0c;否则&#xff0c;如果你的项目不是HarmonyOS&#xff…...

探索亚马逊大语言模型:开启人工智能时代的语言创作新篇章

文章目录 前言一、大语言模型是什么&#xff1f;应用范围 二、Amazon Bedrock总结 前言 想必大家在ChatGPT的突然兴起&#xff0c;大家多多少少都会有各种各样的问题&#xff0c;比如&#xff1a;大语言模型和生成式AI有什么关系呢&#xff1f;大语言模型为什么这么火&#xf…...

zabbix-proxy分布式监控

Zabbix是一款开源的企业级网络监控软件&#xff0c;可以监测服务器、网络设备、应用程序等各种资源的状态和性能指标。在大型环境中&#xff0c;如果只有一个Zabbix Server来监控所有的节点&#xff0c;可能会遇到性能瓶颈和数据处理难题。 为了解决这个问题&#xff0c;Zabbi…...

springboot生成PDF,并且添加水印

/*** 导出调查问卷*/ApiLog("导出调查问卷")PostMapping("/print/{id}")ApiOperationSupport(order 23)ApiOperation(value "导出报告", notes "导出报告")public void print(PathVariable Long id, HttpServletResponse response…...

Tensorflow2.0:CNN、ResNet实现MNIST分类识别

以下仅是个人的学习笔记 &#xff0c;内容可能是错误 CNN&#xff1a; import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers# 导入数据 (x_train, y_train), (x_test, y_test) keras.datasets.mnist.load_data()# 数据预处理 x_tra…...

本地jar导入maven

一、通过dependency引入 1.1. jar包放置&#xff0c;建造lib目录 1.2. pom.xml文件 <dependency><groupId>zip4j</groupId><artifactId>zip4j</artifactId><version>1.3.2</version><!--system&#xff0c;类似provided&#x…...

数据结构与算法【堆】的Java实现

前言 之前已经说过堆的特点了&#xff0c;具体文章在数据结构与算法【队列】的Java实现-CSDN博客。因此直接实现堆的其他功能。 建堆 所谓建堆&#xff0c;就是将一个初始的堆变为大顶堆或是小顶堆。这里以大顶堆为例。展示如何建堆。 找到最后一个非叶子节点从后向前&…...

同创永益联合红帽打造一站式数字韧性解决方案

随着AI技术的快速兴起&#xff0c;IT技术已成为推动业务持续增长的重要驱动力&#xff0c;这要求企业不断尝试新事物&#xff0c;改变固有流程&#xff0c;加强IT技术与业务的合作&#xff0c;同时提升数字韧性能力&#xff0c;以实现业务目标。10月26日&#xff0c;红帽2023中…...

c++ call_once 使用详解

c call_once 使用详解 std::call_once 头文件 #include <mutex>。 函数原型&#xff1a; template<class Callable, class... Args> void call_once(std::once_flag& flag, Callable&& f, Args&&... args);flag&#xff1a;标志对象&#xf…...

【rosrun diagnostic_analysis】报错No module named rospkg | ubuntu 20.04

ubuntu20.04使用指令报错 现象 rosrun diagnostic_analysis export_csv.py my.bag -d ~/Desktop报错 Traceback (most recent call last): File "/opt/ros/noetic/lib/diagnostic_analysis/export_csv.py", line 40, in <module> import roslib; roslib.load_m…...

高防CDN有什么作用?

分布式拒绝服务攻击&#xff08;DDoS攻击&#xff09;是一种针对目标系统的恶意网络攻击行为&#xff0c;DDoS攻击经常会导致被攻击者的业务无法正常访问&#xff0c;也就是所谓的拒绝服务。 常见的DDoS攻击包括以下几类&#xff1a; 网络层攻击&#xff1a;比较典型的攻击类…...

盛元广通开放实训室管理系统2.0

开放实训室管理系统是一种基于网络和数据库的实训室信息管理系统&#xff0c;旨在提高实训室的管理水平&#xff0c;实现实训资源的优化配置和高效利用。该系统通常包括用户管理、设备管理、课程管理、考核管理等功能模块&#xff0c;能够实现实训室的预约、设备借用、课程安排…...

3D建模基础教程:编辑多边形功能命令快捷方式

一、打开3D软件并创建新模型 首先&#xff0c;打开你的3D建模软件&#xff0c;比如Blender、Maya或3ds Max。然后&#xff0c;创建一个新的3D模型。你可以使用基本几何体来创建模型&#xff0c;也可以导入现有的模型。 二、进入编辑多边形模式 在主工具栏中&#xff0c;找到并…...

SaleSmartly新增AI意图识别触发器!让客户享受更精准的自动化服务

AI意图识别技术是对话式AI中很重要的组成部分&#xff0c;通俗点来说就是一种可以识别用户在对话中表达的意图的技术。通过对大量数据的分析和学习&#xff0c;AI可以理解用户想要获得的信息&#xff0c;并根据这些信息来采取相应的行动或提供相应的响应。而在对话式AI中&#…...

计算机毕业设计选题推荐-个人博客微信小程序/安卓APP-项目实战

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

一篇详解,Postman设置token依赖步骤

前言 postman做接口测试时&#xff0c;大多数的接口必须在有token的情况下才能运行&#xff0c;我们可以获取token后设置一个环境变量供所在同一个集合中的所有接口使用。 一般是通过调用登录接口&#xff0c;获取到token的值 实战项目&#xff1a;jeecg boot项目 项目官网…...

音频录制实现 绘制频谱

思路 获取设备信息 获取录音的频谱数据 绘制频谱图 具体实现 封装 loadDevices.js /*** 是否支持录音*/ const recordingSupport () > {const scope navigator.mediaDevices || {};if (!scope.getUserMedia) {scope navigatorscope.getUserMedia || (scope.getUserM…...

vscode里如何用git

打开vs终端执行如下&#xff1a; 1 初始化 Git 仓库&#xff08;如果尚未初始化&#xff09; git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...

label-studio的使用教程(导入本地路径)

文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

模型参数、模型存储精度、参数与显存

模型参数量衡量单位 M&#xff1a;百万&#xff08;Million&#xff09; B&#xff1a;十亿&#xff08;Billion&#xff09; 1 B 1000 M 1B 1000M 1B1000M 参数存储精度 模型参数是固定的&#xff0c;但是一个参数所表示多少字节不一定&#xff0c;需要看这个参数以什么…...

《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)

CSI-2 协议详细解析 (一&#xff09; 1. CSI-2层定义&#xff08;CSI-2 Layer Definitions&#xff09; 分层结构 &#xff1a;CSI-2协议分为6层&#xff1a; 物理层&#xff08;PHY Layer&#xff09; &#xff1a; 定义电气特性、时钟机制和传输介质&#xff08;导线&#…...

解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错

出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上&#xff0c;所以报错&#xff0c;到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本&#xff0c;cu、torch、cp 的版本一定要对…...

HDFS分布式存储 zookeeper

hadoop介绍 狭义上hadoop是指apache的一款开源软件 用java语言实现开源框架&#xff0c;允许使用简单的变成模型跨计算机对大型集群进行分布式处理&#xff08;1.海量的数据存储 2.海量数据的计算&#xff09;Hadoop核心组件 hdfs&#xff08;分布式文件存储系统&#xff09;&a…...

9-Oracle 23 ai Vector Search 特性 知识准备

很多小伙伴是不是参加了 免费认证课程&#xff08;限时至2025/5/15&#xff09; Oracle AI Vector Search 1Z0-184-25考试&#xff0c;都顺利拿到certified了没。 各行各业的AI 大模型的到来&#xff0c;传统的数据库中的SQL还能不能打&#xff0c;结构化和非结构的话数据如何和…...

十九、【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建

【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建 前言准备工作第一部分:回顾 Django 内置的 `User` 模型第二部分:设计并创建 `Role` 和 `UserProfile` 模型第三部分:创建 Serializers第四部分:创建 ViewSets第五部分:注册 API 路由第六部分:后端初步测…...

jdbc查询mysql数据库时,出现id顺序错误的情况

我在repository中的查询语句如下所示&#xff0c;即传入一个List<intager>的数据&#xff0c;返回这些id的问题列表。但是由于数据库查询时ID列表的顺序与预期不一致&#xff0c;会导致返回的id是从小到大排列的&#xff0c;但我不希望这样。 Query("SELECT NEW com…...

C# winform教程(二)----checkbox

一、作用 提供一个用户选择或者不选的状态&#xff0c;这是一个可以多选的控件。 二、属性 其实功能大差不差&#xff0c;除了特殊的几个外&#xff0c;与button基本相同&#xff0c;所有说几个独有的 checkbox属性 名称内容含义appearance控件外观可以变成按钮形状checkali…...