探索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即可

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

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

相关文章

uniapp Android如何授权打开系统蓝牙Bluetooth?

uniapp Android如何授权打开系统蓝牙&#xff1f; 使用uniapp开发蓝牙项目过程中&#xff0c;涉及到检测手机系统蓝牙是否打开功能&#xff0c;这里介绍Android&#xff0c;iOS暂时没有找到优方法。朋友们如果有好的方案&#xff0c;欢迎评论分享~ 文章目录 uniapp Android如何…

AWS云服务器EC2实例实现ByConity快速部署

1. 前言 亚马逊是全球最大的在线零售商和云计算服务提供商。AWS云服务器在全球范围内都备受推崇&#xff0c;被众多业内人士誉为“云计算服务的行业标准”。在国内&#xff0c;亚马逊AWS也以其卓越的性能和服务满足了众多用户的需求&#xff0c;拥有着较高的市场份额和竞争力。…

华为笔记本MateBook D 14 2021款锐龙版R7集显非触屏(NbM-WFP9)原装出厂Windows10-20H2系统

链接&#xff1a;https://pan.baidu.com/s/13Kyy95GME-asli4woNN_ww?pwdbqa8 提取码&#xff1a;bqa8 HUAWEI华为MateBookD14原厂Win10系统自带所有驱动、出厂主题壁纸、系统属性专属LOGO标志、Office办公软件、华为电脑管家等预装程序

05_SHELL编程之文本处理工具SED

typora-root-url: pictures课程目标 掌握sed的基本语法结构 熟悉sed常用的命令&#xff0c;如打印p&#xff0c;删除d&#xff0c;插入i等 Windows&#xff1a;​ Linux&#xff1a; vim vi gedit nano emacs 一、sed介绍 1. sed的工作流程 首先sed把当前正在处理的行保存…

el-table中el-popover失效问题

场景&#xff1a;先有一个数据表格&#xff0c;右侧操作栏为固定列&#xff0c;另外有一个字段使用了el-popover来点击弹出框来修改值&#xff0c;发现不好用&#xff0c;点击后无法显示弹出框&#xff0c;但当没有操作栏权限时却意外的生效了。 这种问题真是不常见&#xff0…

设置指定时间之前的时间不可选

1、el-date-picker设置今天之前的日期不可选 <el-date-picker style"width: 100%" type"date" v-model"form.resetDate" align"right" :value-format"yyyy-MM-dd" placeholder"选择调整日期":disabled"t…

场景交互与场景漫游-路径漫游(7)

路径漫游 按照指定的路径进行漫游对一个演示是非常重要的。在osgViewer中&#xff0c;当第一次按下小写字母“z”时&#xff0c;开始记录动画路径;待动画录制完毕&#xff0c;按下大写字母“Z”&#xff0c;保存动画路径文件;使用osgViewer读取该动画路径文件时&#xff0c;会回…

基于STC12C5A60S2系列1T 8051单片的IIC总线器件模数芯片PCF8591实现模数转换应用

基于STC12C5A60S2系列1T 8051单片的IIC总线器件模数芯片PCF8591实现模数转换应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍IIC总线器件模数芯片PCF8591介绍通过I…

C/C++ 语言 ‘ == ‘ 运算符仅适用于算数表达式

示例代码&#xff1a; #include <stdio.h>typedef struct {int a;int b; } TestStruct;int main(void) {TestStruct testA { 0 }, testB { 0 };if (testA testB) {printf("You can do this!\n");}return 0; }

通过U盘重装Win10教程图解

如果我们发现Win10电脑系统出现了问题&#xff0c;可以通过简单的操作来解决问题。如果还是不能解决系统问题&#xff0c;这时候用户就给电脑重新安装Win10系统&#xff0c;这样就能轻松解决问题了。接下来小编给大家详细介绍关于通过U盘重新安装系统Win10的方法步骤。 准备工作…

Linux基础知识——(2)vim编辑器

目录 1 vi和vim简介2 vim三种模式3 vim命令模式3.1 光标移动3.2 复制操作3.3 剪切/删除3.4 撤销/恢复3.5 光标的快速移动 4 模式间的切换5 命令行模式5 编辑模式6 其他6.1 vim的配置文件6.2 异常退出6.3 退出方式“:x”6.4 vi编辑模式下Backspace无法退格删除6.5 修改只读【rea…

使用uniapp写小程序,真机调试的时候不显示log

项目场景&#xff1a; 当小程序文件太大的情况下使用真机调试&#xff0c;但是真机调试的调试器没有任何反应 问题描述 使用uniapp写小程序&#xff0c;真机调试的时候不显示log 原因分析&#xff1a; 提示&#xff1a;因为真机调试的时候没有压缩文件&#xff0c;所以调试的…

ruoyi-vue前后端分离版本验证码实现思路

序 时隔三个月&#xff0c;再次拿起我的键盘。 前言 ruoyi-vue是若依前后端分离版本的快速开发框架&#xff0c;适合用于项目开始搭建后台管理系统。本篇文章主要介绍其验证码实现的思路。 一、实现思路简介 1、后端会生成一个表达式&#xff0c;比如1 2 ? 3&#xff0…

react 手机端 rc-table列隐藏(根据相关条件是否隐藏)、实现图片上传操作

最近公司某一项目的手机端&#xff0c;新增需求&#xff1a;table中的附件要可以编辑&#xff0c;并且是在特定条件下可编辑&#xff0c;其他仅做展示效果。 查阅官方文档&#xff0c;没有发现是否隐藏这一属性&#xff0c;通过css控制样式感觉也比较麻烦&#xff0c;后面发现可…

再见 Excel,你好 Python Spreadsheets!⛵

Excel是大家最常用的数据分析工具之一&#xff0c;借助它可以便捷地完成数据清理、统计计算、数据分析&#xff08;数据透视图&#xff09;和图表呈现等。 但是&#xff01;大家有没有用 Excel 处理过大一些的数据&#xff08;比如几十上百万行的数据表&#xff09;&#xff0…

从矿源到指尖——周大福天然钻石的非凡实力

&#xff08;2023年11月20日&#xff0c;北京&#xff09;在近百年历程中&#xff0c;周大福珠宝集团一直致力珠宝工艺传承与创新设计的孕育&#xff0c;于1929年创立周大福品牌&#xff0c;凭借对中国传统黄金工艺的传承与创新、对中国传统文化的融合与发扬&#xff0c;将黄金…

wpf devexpress绑定grid到总计和分组统计

此主题描述了如何在gridcontrol中的视图模型和显示定义总计和分组统计 在视图模型中指定统计 1、创建 SummaryItemType 枚举你想要在GridControl中显示的统计类型&#xff1a; public enum SummaryItemType { Max, Count, None } 2、创建一个grid统计描述类 public class S…

世界坐标系,相机坐标系,像素坐标系转换 详细说明(附代码)

几个坐标系介绍&#xff0c;相机内外参的回顾参考此文。 本文主要说明如何在几个坐标系之间转换。 本文涉及&#xff1a; 使用相机内参 在 像素坐标系 和 相机坐标系 之间转换。使用相机外参&#xff08;位姿&#xff09;在相机坐标系 和 世界坐标系 之间转换。(qw,qx,qy,qz,…

【C++】pow函数实现的伽马变换详解和示例

本文通过原理和示例对伽马变换进行详解&#xff0c;并通过改变变换系数展示不同的效果&#xff0c;以帮助大家理解和使用。 原理 伽马变换是一种用于图像增强的技术&#xff0c;它可以用来提高或降低图像的对比度&#xff0c;常用于医学图像处理和计算机视觉等领域。伽马变换…

姿态估计 MediaPipe实现手势,人体姿态,面部动作估计的用法

姿态估计 MediaPipe实现手势&#xff0c;人体姿态&#xff0c;面部动作估计的用法 import mediapipe as mp import cv2 import numpy as np import time # 定义一个函数&#xff0c;计算两个点的距离 def findDis(pts1,pts2):return ((pts2[0]-pts1[0])**2 (pts2[1]-pts1[1])*…