温习 SPI 机制 (Java SPI 、Spring SPI、Dubbo SPI)

news/2025/1/23 2:10:58/文章来源:https://www.cnblogs.com/makemylife/p/18511622

SPI 全称为 Service Provider Interface,是一种服务发现机制。

SPI 的本质是将接口实现类全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。

1 Java SPI 示例

本节通过一个示例演示 Java SPI 的使用方法。首先,我们定义一个接口,名称为 Robot。

public interface Robot {void sayHello();
}

接下来定义两个实现类,分别为 OptimusPrimeBumblebee

public class OptimusPrime implements Robot {@Overridepublic void sayHello() {System.out.println("Hello, I am Optimus Prime.");}}public class Bumblebee implements Robot {@Overridepublic void sayHello() {System.out.println("Hello, I am Bumblebee.");}}

接下来 META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 org.apache.spi.Robot 。文件内容为实现类的全限定的类名,如下:

org.apache.spi.OptimusPrime
org.apache.spi.Bumblebee

做好所需的准备工作,接下来编写代码进行测试。

public class JavaSPITest {@Testpublic void sayHello() throws Exception {ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);System.out.println("Java SPI");// 1. forEach 模式serviceLoader.forEach(Robot::sayHello);// 2. 迭代器模式Iterator<Robot> iterator = serviceLoader.iterator();while (iterator.hasNext()) {Robot robot = iterator.next();//System.out.println(robot);//robot.sayHello();}}
}

最后来看一下测试结果,如下 :

2 经典 Java SPI 应用 : JDBC DriverManager

JDBC4.0 之前,我们开发有连接数据库的时候,通常先加载数据库相关的驱动,然后再进行获取连接等的操作。

// STEP 1: Register JDBC driver
Class.forName("com.mysql.jdbc.Driver");
// STEP 2: Open a connection
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url,username,password);

JDBC4.0之后使用了 Java 的 SPI 扩展机制,不再需要用 Class.forName("com.mysql.jdbc.Driver") 来加载驱动,直接就可以获取 JDBC 连接。

接下来,我们来看看应用如何加载 MySQL JDBC 8.0.22 驱动:

首先 DriverManager类是驱动管理器,也是驱动加载的入口。

/*** Load the initial JDBC drivers by checking the System property* jdbc.properties and then use the {@code ServiceLoader} mechanism*/
static {loadInitialDrivers();println("JDBC DriverManager initialized");
}

在 Java 中,static 块用于静态初始化,它在类被加载到 Java 虚拟机中时执行。

静态块会加载实例化驱动,接下来我们看看loadInitialDrivers 方法。

加载驱动代码包含四个步骤:

  1. 系统变量中获取有关驱动的定义。

  2. 使用 SPI 来获取驱动的实现类(字符串的形式)。

  3. 遍历使用 SPI 获取到的具体实现,实例化各个实现类。

  4. 根据第一步获取到的驱动列表来实例化具体实现类。

我们重点关注 SPI 的用法,首先看第二步,使用 SPI 来获取驱动的实现类 , 对应的代码是:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

这里没有去 META-INF/services目录下查找配置文件,也没有加载具体实现类,做的事情就是封装了我们的接口类型和类加载器,并初始化了一个迭代器。

接着看第三步,遍历使用SPI获取到的具体实现,实例化各个实现类,对应的代码如下:

Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍历所有的驱动实现
while(driversIterator.hasNext()) {driversIterator.next();
}

在遍历的时候,首先调用driversIterator.hasNext()方法,这里会搜索 classpath 下以及 jar 包中所有的META-INF/services目录下的java.sql.Driver文件,并找到文件中的实现类的名字,此时并没有实例化具体的实现类。

然后是调用driversIterator.next()方法,此时就会根据驱动名字具体实例化各个实现类了,现在驱动就被找到并实例化了。

3 Java SPI 机制源码解析

我们根据第一节 JDK SPI 示例,学习 ServiceLoader 类的实现。

ServiceLoader类

进入 ServiceLoader 类的load方法:

public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}public static <S> ServiceLoader<S> load(Class<S> service , ClassLoader loader) {return new ServiceLoader<>(service, loader);
}

上面的代码,load 方法会通过传递的服务类型类加载器 classLoader 创建一个 ServiceLoader 对象。

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();
}// 缓存已经被实例化的服务提供者,按照实例化的顺序存储
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();public void reload() {providers.clear();lookupIterator = new LazyIterator(service, loader);
}

私有构造器会创建懒迭代器 LazyIterator 对象 ,所谓懒迭代器,就是对象初始化时,仅仅是初始化,只有在真正调用迭代方法时,才执行加载逻辑

示例代码中创建完 serviceLoader 之后,接着调用iterator()方法:

Iterator<Robot> iterator = serviceLoader.iterator();// 迭代方法实现
public Iterator<S> iterator() {return new Iterator<S>() {Iterator<Map.Entry<String,S>> knownProviders= providers.entrySet().iterator();public boolean hasNext() {if (knownProviders.hasNext())return true;return lookupIterator.hasNext();}public S next() {if (knownProviders.hasNext())return knownProviders.next().getValue();return lookupIterator.next();}public void remove() {throw new UnsupportedOperationException();}};
}

迭代方法的实现本质是调用懒迭代器 lookupIterator 的 hasNext()next() 方法。

1、hasNext() 方法

public boolean hasNext() {if (acc == null) {return hasNextService();} else {PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {public Boolean run() { return hasNextService(); }};return AccessController.doPrivileged(action, acc);}
}
public S next() {if (acc == null) {return nextService();} else {PrivilegedAction<S> action = new PrivilegedAction<S>() {public S run() { return nextService(); }};return AccessController.doPrivileged(action, acc);}
}

懒迭代器的hasNextService方法首先会通过加载器通过文件全名获取配置对象 Enumeration<URL> configs ,然后调用解析parse方法解析classpath下的META-INF/services/目录里以服务接口命名的文件。

private boolean hasNextService() {if (nextName != null) {return true;}if (configs == null) {try {String fullName = PREFIX + service.getName();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()) {if (!configs.hasMoreElements()) {return false;}pending = parse(service, configs.nextElement());}nextName = pending.next();return true;
}

hasNextService 方法返回 true , 我们可以调用迭代器的 next 方法 ,本质是调用懒加载器 lookupIterator 的 next() 方法:

2、next() 方法

Robot robot = iterator.next();// 调用懒加载器 lookupIterator 的  `next()` 方法
private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn = nextName;nextName = null;Class<?> c = null;try {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 {S p = service.cast(c.newInstance());providers.put(cn, p);return p;} catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x);}throw new Error();  // This cannot happen}

通过反射方法 Class.forName() 加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型,然后返回实例对象。

4 Java SPI 机制的缺陷

通过上面的解析,可以发现,我们使用 JDK SPI 机制的缺陷 :

  • 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
  • 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
  • 多个并发多线程使用 ServiceLoader 类的实例是不安全的。

5 Spring SPI 机制

Spring SPI 沿用了 Java SPI 的设计思想,Spring 采用的是 spring.factories 方式实现 SPI 机制,可以在不修改 Spring 源码的前提下,提供 Spring 框架的扩展性。

1、创建 MyTestService 接口

public interface MyTestService {void printMylife();
}

2、创建 MyTestService 接口实现类

  • WorkTestService :
public class WorkTestService implements MyTestService {public WorkTestService(){System.out.println("WorkTestService");}public void printMylife() {System.out.println("我的工作");}
}
  • FamilyTestService :
public class FamilyTestService implements MyTestService {public FamilyTestService(){System.out.println("FamilyTestService");}public void printMylife() {System.out.println("我的家庭");}
}

3、在资源文件目录,创建一个固定的文件 META-INF/spring.factories

#key是接口的全限定名,value是接口的实现类
com.courage.platform.sms.demo.service.MyTestService = com.courage.platform.sms.demo.service.impl.FamilyTestService,com.courage.platform.sms.demo.service.impl.WorkTestService

4、运行代码

// 调用 SpringFactoriesLoader.loadFactories 方法加载 MyTestService 接口所有实现类的实例
List<MyTestService> myTestServices = SpringFactoriesLoader.loadFactories(MyTestService.class,Thread.currentThread().getContextClassLoader()
);for (MyTestService testService : myTestServices) {testService.printMylife();
}

运行结果:

FamilyTestService
WorkTestService
我的家庭
我的工作 

Spring SPI 机制非常类似 ,但还是有一些差异:

  • Java SPI 是一个服务提供接口对应一个配置文件,配置文件中存放当前接口的所有实现类,多个服务提供接口对应多个配置文件,所有配置都在 services 目录下。
  • Spring SPI 是一个 spring.factories 配置文件存放多个接口及对应的实现类,以接口全限定名作为key,实现类作为value来配置,多个实现类用逗号隔开,仅 spring.factories 一个配置文件。

和 Java SPI 一样,Spring SPI 也无法获取某个固定的实现,只能按顺序获取所有实现

6 Dubbo SPI 机制

基于 Java SPI 的缺陷无法支持按需加载接口实现类,Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。

Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。

Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容如下:

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。

另外,在测试 Dubbo SPI 时,需要在 Robot 接口上标注 @SPI 注解。

下面来演示 Dubbo SPI 的用法:

public class DubboSPITest {@Testpublic void sayHello() throws Exception {ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);Robot optimusPrime = extensionLoader.getExtension("optimusPrime");optimusPrime.sayHello();Robot bumblebee = extensionLoader.getExtension("bumblebee");bumblebee.sayHello();}
}

测试结果如下 :

另外,Dubbo SPI 除了支持按需加载接口实现类,还增加了 IOC 和 AOP 等特性


Dubbo SPI :

https://cn.dubbo.apache.org/zh-cn/docsv2.7/dev/source/dubbo-spi/

JDK/Dubbo/Spring 三种 SPI 机制,谁更好 ?

https://segmentfault.com/a/1190000039812642

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

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

相关文章

.NET周刊【10月第3期 2024-10-20】

国内文章 我被 .NET8 JIT 的一个BUG反复折磨了半年之久(JIT tier1 finally optimizations) https://www.cnblogs.com/calvinK/p/18469889 作者分享了一次在公司中API服务器从.NET 6升级到.NET 8后遇到的JIT BUG经历。升级后一个实例在某些部署中出现AES解密明文字符丢失的问题,…

mysql弱密码爆破

mySQL弱密码靶场:/vulhub/mysql/CVE-2012-2122启动: docker-compose up -d扫描端口 nmap -Sv -Pn -T4 靶机ip看到在3306端口开启了mysql服务爆破账号密码1.使用超级弱口令检测工具(github下载)爆破出root/1234562.使用Hydra爆破 hydra -L 用户名字典 —P 密码字典 靶机IP m…

本人高分硕士论文项目:工业异常检测基准引擎

1. 架构 如图所示,IADBE(Industrial Anomaly Detection Benchmark Engine)系统由三个主要部分组成: IADBE、IADBE 服务器和 IADBE 后台。IADBE 是系统的核心,API 和 CLI 是网关。数据集、模型和指标是系统最重要的部分。模型基于开源的 Anomalib 和 YOLOv8。系统有三个主要…

CSP-S 2024 复赛游记

CSP-S 2024 游记Day -2 空白的一天。huge 不想太多天连着打模拟赛,并且想在明天安排一场,所以安排了专题。 今天是 dp 专题。 听了丁真的去做 AT 的 dp 专题 了,很晚才看 Vjudge。 效率有点低啊,这状态怎么打复赛(。 Day -1 全真模拟,换了座位。模拟赛有关。 挂了 40pts,…

现在才投简历还来得及吗?

某客热帖“现在才投互联网还有没有 HC?”,一时间引发了广泛的讨论。事情是这样的:有个小哥,其自身条件也不错,本硕 985 院校,求职意向是 Java 后端研发工程师,拿过国奖、有实习经历,各方面条件都不错。 但就是比较刚,秋招开始后只投了央国企、银行、运营商之类的工作,…

MaskGCT,AI语音克隆大模型本地部署(Windows11),基于Python3.11,TTS,文字转语音

前几天,又一款非自回归的文字转语音的AI模型:MaskGCT,开放了源码,和同样非自回归的F5-TTS模型一样,MaskGCT模型也是基于10万小时数据集Emilia训练而来的,精通中英日韩法德6种语言的跨语种合成。数据集Emilia是全球最大且最为多样的高质量多语种语音数据集之一。 本次分享…

图像处理领域的加速算子收集

1、Simd库——CPU指令集加速 算子 Simd Library Documentation. 部分算子截图: 2、VPI库——CPU、GPU(CUDA)加速 算子 VPI - Vision Programming Interface: Algorithms 部分算子截图: 3、CV-CUDA库 算子 CV-CUDA — CV-CUDA Beta documentation 部分算子截图:

postgresql 下载安装

一、postgresql 下载 pg官网:postgres.org一般推荐用源码安装,下载 .tar.gz 包 二、安装 本文以12.6版本安装为例: 2.1、安装前要求和环境配置 # 1、要求GNU make版本3.80或以上(GNU make有时以名字gmake安装),要测试make版本可以使用以下命令(如果是安装其他版本的pg具…

“药品追溯到客户管理:数字化转型下的药企发展之路”

随着科技进步和市场环境的变化,医药企业面临着前所未有的机遇和挑战。数字化转型已成为药企创新管理模式、提升市场竞争力的关键举措。在这一过程中,药品追溯和客户管理作为重要环节,通过数字化手段可以实现信息的高效流通和透明管理。以下将从药品追溯、客户管理以及未来发…

sgx模拟执行,不需要sgx硬件---sgx executed in simulation,No need to support hardware for SGX

sgx executed in simulation 使用项目:https://github.com/intel/linux-sgx.git前言:目前国内和国外互联网上关于使用模拟模式来完成sgx的博客我是真的一点没有找到,因此自己写一份博客来完成记录 环境:Ubuntu22.04不支持sgx,没有硬件存在(mac也可以按照本教程来完成工作…

算法定制视频分析网关拍照检测工业园区/厂区/工厂智慧安监方案

一、方案背景 随着工业化进程的加速,特别是制造业、建筑业、化工等高风险行业,生产安全事故频发,对人们的生命安全和健康构成了严重威胁。为了有效预防和减少重大事故的发生,提高安全管理水平,智慧安监方案应运而生。 二、方案内容 智慧安监方案的核心在于利用物联网、大数…

IIC通信协议详解 PCF8591应用(Verilog实现)

详细介绍了IIC通信协议并给出如何使用PCF8591,用verilog实现.该文章结合PCF8591 8-bit AD/DA 模数/数模转换器来详细介绍IIC通信协议,尽量做到条理清晰,通俗易懂。该文图片均从PCF8591手册中截取,一定程度上引导读者学习阅读data sheet。 1. PCF8591引脚2. 功能介绍 2.1 地…