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

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

1 Java SPI 示例

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

  1. public interface Robot {
  2. void sayHello();
  3. }

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

  1. public class OptimusPrime implements Robot {
  2. @Override
  3. public void sayHello() {
  4. System.out.println("Hello, I am Optimus Prime.");
  5. }
  6. }
  7. public class Bumblebee implements Robot {
  8. @Override
  9. public void sayHello() {
  10. System.out.println("Hello, I am Bumblebee.");
  11. }
  12. }

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

  1. org.apache.spi.OptimusPrime
  2. org.apache.spi.Bumblebee

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

  1. public class JavaSPITest {
  2. @Test
  3. public void sayHello() throws Exception {
  4. ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
  5. System.out.println("Java SPI");
  6. // 1. forEach 模式
  7. serviceLoader.forEach(Robot::sayHello);
  8. // 2. 迭代器模式
  9. Iterator<Robot> iterator = serviceLoader.iterator();
  10. while (iterator.hasNext()) {
  11. Robot robot = iterator.next();
  12. //System.out.println(robot);
  13. //robot.sayHello();
  14. }
  15. }
  16. }

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

2 经典 Java SPI 应用 : JDBC DriverManager

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3 Java SPI 机制源码解析

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

进入 ServiceLoader 类的load方法:

  1. public static <S> ServiceLoader<S> load(Class<S> service) {
  2. ClassLoader cl = Thread.currentThread().getContextClassLoader();
  3. return ServiceLoader.load(service, cl);
  4. }
  5. public static <S> ServiceLoader<S> load(Class<S> service , ClassLoader loader) {
  6. return new ServiceLoader<>(service, loader);
  7. }

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

  1. private ServiceLoader(Class<S> svc, ClassLoader cl) {
  2. service = Objects.requireNonNull(svc, "Service interface cannot be null");
  3. loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
  4. acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
  5. reload();
  6. }
  7. // 缓存已经被实例化的服务提供者,按照实例化的顺序存储
  8. private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
  9. public void reload() {
  10. providers.clear();
  11. lookupIterator = new LazyIterator(service, loader);
  12. }

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

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

  1. Iterator<Robot> iterator = serviceLoader.iterator();
  2. // 迭代方法实现
  3. public Iterator<S> iterator() {
  4. return new Iterator<S>() {
  5. Iterator<Map.Entry<String,S>> knownProviders
  6. = providers.entrySet().iterator();
  7. public boolean hasNext() {
  8. if (knownProviders.hasNext())
  9. return true;
  10. return lookupIterator.hasNext();
  11. }
  12. public S next() {
  13. if (knownProviders.hasNext())
  14. return knownProviders.next().getValue();
  15. return lookupIterator.next();
  16. }
  17. public void remove() {
  18. throw new UnsupportedOperationException();
  19. }
  20. };
  21. }

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

1、hasNext() 方法

  1. public boolean hasNext() {
  2. if (acc == null) {
  3. return hasNextService();
  4. } else {
  5. PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
  6. public Boolean run() { return hasNextService(); }
  7. };
  8. return AccessController.doPrivileged(action, acc);
  9. }
  10. }
  1. public S next() {
  2. if (acc == null) {
  3. return nextService();
  4. } else {
  5. PrivilegedAction<S> action = new PrivilegedAction<S>() {
  6. public S run() { return nextService(); }
  7. };
  8. return AccessController.doPrivileged(action, acc);
  9. }
  10. }

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

  1. private boolean hasNextService() {
  2. if (nextName != null) {
  3. return true;
  4. }
  5. if (configs == null) {
  6. try {
  7. String fullName = PREFIX + service.getName();
  8. if (loader == null)
  9. configs = ClassLoader.getSystemResources(fullName);
  10. else
  11. configs = loader.getResources(fullName);
  12. } catch (IOException x) {
  13. fail(service, "Error locating configuration files", x);
  14. }
  15. }
  16. while ((pending == null) || !pending.hasNext()) {
  17. if (!configs.hasMoreElements()) {
  18. return false;
  19. }
  20. pending = parse(service, configs.nextElement());
  21. }
  22. nextName = pending.next();
  23. return true;
  24. }

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

2、next() 方法

  1. Robot robot = iterator.next();
  2. // 调用懒加载器 lookupIterator 的 `next()` 方法
  3. private S nextService() {
  4. if (!hasNextService())
  5. throw new NoSuchElementException();
  6. String cn = nextName;
  7. nextName = null;
  8. Class<?> c = null;
  9. try {
  10. c = Class.forName(cn, false, loader);
  11. } catch (ClassNotFoundException x) {
  12. fail(service,
  13. "Provider " + cn + " not found");
  14. }
  15. if (!service.isAssignableFrom(c)) {
  16. fail(service,
  17. "Provider " + cn + " not a subtype");
  18. }
  19. try {
  20. S p = service.cast(c.newInstance());
  21. providers.put(cn, p);
  22. return p;
  23. } catch (Throwable x) {
  24. fail(service,
  25. "Provider " + cn + " could not be instantiated",
  26. x);
  27. }
  28. throw new Error(); // This cannot happen
  29. }

通过反射方法 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 接口

  1. public interface MyTestService {
  2. void printMylife();
  3. }

2、创建 MyTestService 接口实现类

  • WorkTestService :
  1. public class WorkTestService implements MyTestService {
  2. public WorkTestService(){
  3. System.out.println("WorkTestService");
  4. }
  5. public void printMylife() {
  6. System.out.println("我的工作");
  7. }
  8. }
  • FamilyTestService :
  1. public class FamilyTestService implements MyTestService {
  2. public FamilyTestService(){
  3. System.out.println("FamilyTestService");
  4. }
  5. public void printMylife() {
  6. System.out.println("我的家庭");
  7. }
  8. }

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

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

4、运行代码

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

运行结果:

  1. FamilyTestService
  2. WorkTestService
  3. 我的家庭
  4. 我的工作

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 路径下,配置内容如下:

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

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

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

下面来演示 Dubbo SPI 的用法:

  1. public class DubboSPITest {
  2. @Test
  3. public void sayHello() throws Exception {
  4. ExtensionLoader<Robot> extensionLoader =
  5. ExtensionLoader.getExtensionLoader(Robot.class);
  6. Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
  7. optimusPrime.sayHello();
  8. Robot bumblebee = extensionLoader.getExtension("bumblebee");
  9. bumblebee.sayHello();
  10. }
  11. }

测试结果如下 :

另外,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

温习 SPI 机制 (Java SPI 、Spring SPI、Dubbo SPI)的更多相关文章

  1. 某大厂面试题:说一说Java、Spring、Dubbo三者SPI机制的原理和区别

    大家好,我是三友~~ 今天来跟大家聊一聊Java.Spring.Dubbo三者SPI机制的原理和区别. 其实我之前写过一篇类似的文章,但是这篇文章主要是剖析dubbo的SPI机制的源码,中间只是简单地 ...

  2. 一文搞懂Java/Spring/Dubbo框架中的SPI机制

    几天前和一位前辈聊起了Spring技术,大佬突然说了SPI,作为一个熟练使用Spring的民工,心中一紧,咱也不敢说不懂,而是在聊完之后赶紧打开了浏览器,开始的学习之路,所以也就有了这篇文章.废话不多 ...

  3. Dubbo 源码分析 - SPI 机制

    1.简介 SPI 全称为 Service Provider Interface,是 Java 提供的一种服务发现机制.SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加 ...

  4. 聊聊Java SPI机制

    一.Java SPI机制 SPI(Service Provider Interface)是JDK内置的服务发现机制,用在不同模块间通过接口调用服务,避免对具体服务服务接口具体实现类的耦合.比如JDBC ...

  5. dubbo源码分析01:SPI机制

    一.什么是SPI SPI全称为Service Provider Interface,是一种服务发现机制,其本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件.这样可以在运行时,动态为 ...

  6. 搞懂Dubbo SPI可拓展机制

    前言 阅读本文需要具备java spi的基础,本文不讲java spi,please google it. 一.Dubbo SPI 简介 SPI(Service Provider Interface) ...

  7. Java是如何实现自己的SPI机制的? JDK源码(一)

    注:该源码分析对应JDK版本为1.8 1 引言 这是[源码笔记]的JDK源码解读的第一篇文章,本篇我们来探究Java的SPI机制的相关源码. 2 什么是SPI机制 那么,什么是SPI机制呢? SPI是 ...

  8. Java编程技术之浅析SPI服务发现机制

    SPI服务发现机制 SPI是Java JDK内部提供的一种服务发现机制. SPI->Service Provider Interface,服务提供接口,是Java JDK内置的一种服务发现机制 ...

  9. 面试常问的dubbo的spi机制到底是什么?

    前言 dubbo是一款微服务开发框架,它提供了 RPC通信 与 微服务治理 两大关键能力.作为spring cloud alibaba体系中重要的一部分,随着spring cloud alibaba在 ...

  10. 深入理解 Java 中 SPI 机制

    本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/vpy5DJ-hhn0iOyp747oL5A作者:姜柱 SPI(Service Provider ...

随机推荐

  1. [POI2008] POC-Trains 题解

    前言 题目链接:洛谷. 时间复杂度和输入同阶的做法. 题意简述 有 \(n\)(\(n \leq 10^3\))个长 \(m\) 的字符串,\(q\)(\(q \leq 10^5\))次操作,交换两个 ...

  2. 在 SQLAlchemy 中实现数据处理的时候,实现表自引用、多对多、联合查询,有序id等常见的一些经验总结

    有时候,我们在使用SQLAlchemy操作某些表的时候,需要使用外键关系来实现一对多或者多对多的关系引用,以及对多表的联合查询,有序列的uuid值或者自增id值,字符串的分拆等常见处理操作. 1.在 ...

  3. Homebrew 使用

    使用 brew install brew uninstall|remove|rm brew list # *显示已安装软件列表 brew upgrade # 更新 Homebrew brew sear ...

  4. .NET 网络唤醒

    本文介绍下电脑设备关机的情况下如何通过网络唤醒设备,之前电源S状态 计算机Power电源状态- 唐宋元明清2188 - 博客园 (cnblogs.com) 有介绍过远程唤醒设备,后面这俩天了解多了点所 ...

  5. C#自定义控件—文本显示、文本设值

    C#用户控件之文本显示.设定组件 如何绘制一个便捷的文本显示组件.文本设值组件(TextShow,TextSet)? 绘制此控件的目的就是方便一键搞定标签显示(可自定义方法显示文本颜色等),方便自定义 ...

  6. Linux IP 命令

    改MAC 地址 ip link set dev nic1 downip link set dev nic1 address 0c:42:a1:8f:a4:47ip link set dev nic1 ...

  7. 神经网络之卷积篇:详解卷积神经网络示例(Convolutional neural network example)

    详解卷积神经网络示例 假设,有一张大小为32×32×3的输入图片,这是一张RGB模式的图片,想做手写体数字识别.32×32×3的RGB图片中含有某个数字,比如7,想识别它是从0-9这10个数字中的哪一 ...

  8. 日志与追踪的完美融合:OpenTelemetry MDC 实践指南

    前言 在前面两篇实战文章中: OpenTelemetry 实战:从零实现分布式链路追踪 OpenTelemetry 实战:从零实现应用指标监控 覆盖了可观测中的指标追踪和 metrics 监控,下面理 ...

  9. Angular 学习笔记 (Typescript 高级篇)

    由于 typescript 越来越复杂. 所以特意开多一个篇幅来记入一些比较难的, 和一些到了一定程度需要知道的基础. 主要参考 https://basarat.gitbook.io/typescri ...

  10. tailwindcss 经验

    树摇时扫描的文件范围 根据 tailwindcss.config.js 中 content 的配置,跟打包软件加载的模块无关.即未使用的模块中的 class 也会被包含进来.