首页 > 其他分享 >spi概念

spi概念

时间:2024-08-07 23:05:37浏览次数:8  
标签:概念 服务 SPI 接口 spi Java ServiceLoader 加载

参见:https://zhuanlan.zhihu.com/p/692383462


----- 博客原文 -----

SPI机制介绍

前言

讲 SPI 机制之前,先说说 API ,从面向接口编程的思想来看,「服务调用方」应该通过调用「接口」而不是「具体实现」来处理逻辑。那么,对于「接口」的定义,应该在「服务调用方」还是「服务提供方」呢?

一般来说,会有两种选择:

  • 「接口」定义在「服务提供方」
  • 「接口」定义在「服务调用方」

情况1: 先来看看「接口」属于「提供方」的情况。这个很容易理解,提供方同时提供了「接口」和「实现类」,「调用方」可以调用接口来达到调用某实现类的功能,这就是我们日常使用的 API 。 API 的显著特征就是:

接口和实现都在服务提供方中。自定义接口,自己去实现这个接口,也就是提供实现类,最后提供给外部去使用

情况2: 那么再来看看「接口」属于「调用方」的情况。这个其实就是 SPI 机制。以 JDBC 驱动为例,「调用方」定义了java.sql.Driver接口(没有实现这个接口),这个接口位于「调用方」JDK 的包中,各个数据库厂商(也就是服务提供方)实现了这个接口,比如 MySQL 驱动 com.mysql.jdbc.Driver 。 SPI最显著的特征就是:

「接口」在「调用方」的包,「调用方」定义规则,而实现类在「服务提供方」中

总结一下:

  • API 其实是服务提供方,它把接口和实现都做了,然后提供给服务调用方来用,服务提供方是处于主导地位的,此时服务调用方是被动的
  • SPI 则是服务调用方去定义了某种标准(接口),你要给我提供服务,就必须按照我的这个标准来做实现,此时服务调用方的处于主导的,而服务提供方是被动的

概念

SPI 全称:Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。

这是一种JDK内置的一种服务发现的机制,用于制定一些规范,实际实现方式交给不同的服务厂商。如下图:

 

面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可插拔的原则,如果需要替换一种实现,就需要修改代码。

Q:上图的基于接口编程体现在哪里?可插拔又是指什么? A:调用方只会去依赖上图的标准服务接口,而不会去管实现类,这样做的优点有哪些呢? 1. 如果你想再增加一个实现类,你只需要去实现这个接口就可以,其他地方的代码都不用动,这是扩展性的体现 2. 又或者是某天你不需要实现类A了,那直接把实现类A去掉就可以了,对整个系统不会有大的改动,这就是可插拔和组件化思想的好处,此时整个系统还实现了充分的解偶

SPI 应用案例之 JDBC DriverManager

众所周知,关系型数据库有很多种,如:MySQL、Oracle、PostgreSQL 等等。Java 的 JDBC 提供了一套 API 供 Java 应用与数据库进行交互,但是,不同的数据库在底层实现上是有区别的呀,我现在想用这一套 API 对所有数据库都适用,那怎么办勒?此时就出现了一个东西,这个东西就是驱动。

举个例子:这就相比于你说中文,但是你的客户可能有说英语、法语、德语等等,此时你是不是希望有个翻译,而且是有多个翻译,有翻译成英语的,翻译法语的等等(假设一个翻译只能把中文翻译成一种语言),有了不同的翻译之后,这样就可以把你说的中文翻译给不同语言的人听,而驱动就是翻译

实现 SPI 的四步:

  1. 服务的调用者要先定义好接口
  2. 服务的提供者提供了接口的实现
  3. 需要在类目录下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。
  4. 当其他的程序需要这个服务(服务提供者提供的)的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。

接下来看看 JDBC 的实践是怎么做的:

  1. 这是 java.sql.Driver(JDK)中定义驱动的接口(对应在服务的调用者要先定义好接口)

 1.这是MySQL驱动中的Driver类,它实现了上面的Driver接口

 1.并且我们发现在META-INF/services/ 目录里创建一个以服务接口(java.sql.Driver)命名的文件,这个文件里的内容就是这个接口的具体的实现类

 1.怎么去把驱动的服务提供给调用者呢?现在常用的就是直接引入依赖就可以

 

SPI 原理

上文中,我们了解了使用 Java SPI 的方法。那么 Java SPI 是如何工作的呢?实际上,Java SPI 机制依赖于 ServiceLoader 类去解析、加载服务。因此,掌握了 ServiceLoader 的工作流程,就掌握了 SPI 的原理。ServiceLoader 的代码本身很精练,接下来,让我们通过读源码的方式,逐一理解 ServiceLoader 的工作流程。

ServiceLoader 的成员变量

先看一下 ServiceLoader 类的成员变量,大致有个印象,后面的源码中都会使用到。

public final class ServiceLoader<S> implements Iterable<S> {

    // SPI 配置文件目录
    private static final String PREFIX = "META-INF/services/";

    // 将要被加载的 SPI 服务
    private final Class<S> service;

    // 用于加载 SPI 服务的类加载器
    private final ClassLoader loader;

    // ServiceLoader 创建时的访问控制上下文
    private final AccessControlContext acc;

    // SPI 服务缓存,按实例化的顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 懒查询迭代器
    private LazyIterator lookupIterator;

    // ...
}

ServiceLoader 的工作流程

(1)ServiceLoader.load 静态方法

 

应用程序加载 Java SPI 服务,都是先调用 ServiceLoader.load 静态方法,这个方法会new ServiceLoader对象 ServiceLoader.load 静态方法的作用是: ① 指定类加载 ClassLoader 和访问控制上下文; ② 然后,重新加载 SPI 服务

  • 清空缓存中所有已实例化的 SPI 服务
  • 根据 ClassLoader 和 SPI 类型,创建懒加载迭代器

这里,摘录 ServiceLoader.load 相关源码,如下:

// service 传入的是期望加载的 SPI 接口类型
// loader 是用于加载 SPI 服务的类加载器
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
  return new ServiceLoader<>(service, loader);
}

public void reload() {
    // 清空缓存中所有已实例化的 SPI 服务
  providers.clear();
    // 根据 ClassLoader 和 SPI 类型,创建懒加载迭代器
  lookupIterator = new LazyIterator(service, loader);
}

// 私有构造方法
// 重新加载 SPI 服务
private ServiceLoader(Class<S> svc, ClassLoader cl) {
  service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // 指定类加载 ClassLoader 和访问控制上下文
  loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
  acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    // 然后,重新加载 SPI 服务
  reload();
}

(2)应用程序通过 ServiceLoader 的 iterator 方法遍历 SPI 实例 ServiceLoader 的类定义,明确了 ServiceLoader 类实现了 Iterable 接口,所以,它是可以迭代遍历的。实际上,ServiceLoader 类维护了一个缓存 providers( LinkedHashMap 对象),缓存 providers 中保存了已经被成功加载的 SPI 实例,这个 Map 的 key 是 SPI 接口实现类的全限定名,value 是该实现类的一个实例对象。 当应用程序调用 ServiceLoader 的 iterator 方法时,ServiceLoader 会先判断缓存 providers 中是否有数据:如果有,则直接返回缓存 providers 的迭代器;如果没有,则返回懒加载迭代器的迭代器。

public Iterator<S> iterator() {
  return new Iterator<S>() {

        // 缓存 SPI providers
    Iterator<Map.Entry<String,S>> knownProviders
      = providers.entrySet().iterator();

        // lookupIterator 是 LazyIterator 实例,用于懒加载 SPI 实例
    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();
    }

  };
}

(3)懒加载迭代器的工作流程 上面的源码中提到了,lookupIterator 是 LazyIterator 实例,而 LazyIterator 用于懒加载 SPI 实例。那么, LazyIterator 是如何工作的呢? 这里,摘取 LazyIterator 关键代码 hasNextService 方法:

  • 拼接 META-INF/services/ + SPI 接口全限定名
  • 通过类加载器,尝试加载资源文件
  • 解析资源文件中的内容,获取 SPI 接口的实现类的全限定名 nextName

nextService 方法:

  • hasNextService () 方法解析出了 SPI 实现类的的全限定名 nextName,通过反射,获取 SPI 实现类的类定义 Class。
  • 然后,尝试通过 Class 的 newInstance 方法实例化一个 SPI 服务对象。如果成功,则将这个对象加入到缓存 providers 中并返回该对象。
private boolean hasNextService() {
  if (nextName != null) {
    return true;
  }
  if (configs == null) {
    try {
            // 1.拼接 META-INF/services/ + SPI 接口全限定名
            // 2.通过类加载器,尝试加载资源文件
            // 3.解析资源文件中的内容
      String fullName = PREFIX + service.getName();
      if (loader == null)
        configs = ClassLoader.getSystemResources(fullName);
      else
        configs = 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;
}

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 s");
  }
  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
}

SPI 和类加载器

通过上面两个章节中,走读 ServiceLoader 代码,我们已经大致了解 Java SPI 的工作原理,即通过 ClassLoader 加载 SPI 配置文件,解析 SPI 服务,然后通过反射,实例化 SPI 服务实例。我们不妨思考一下,为什么加载 SPI 服务时,需要指定类加载器 ClassLoader 呢? 学习过 JVM 的读者,想必都了解过类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的 BootstrapClassLoader 外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。 双亲委派机制约定了:一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载。 双亲委派的好处:使得 Java 类伴随着它的类加载器,天然具备一种带有优先级的层次关系,从而使得类加载得到统一,不会出现重复加载的问题:

  1. 系统类防止内存中出现多份同样的字节码
  2. 保证 Java 程序安全稳定运行

例如:java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 的类并放到 classpath 中,程序可以编译通过。因为双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 classpath 中的 Object 优先级更高,因为 rt.jar 中的 Object 使用的是启动类加载器,而 classpath 中的 Object 使用的是应用程序类加载器。正因为 rt.jar 中的 Object 优先级更高,因为程序中所有的 Object 都是这个 Object。 双亲委派的限制:子类加载器可以使用父类加载器已经加载的类,而父类加载器无法使用子类加载器已经加载的。—— 这就导致了双亲委派模型并不能解决所有的类加载器问题。Java SPI 就面临着这样的问题:

  • SPI 的接口是 Java 核心库的一部分,是由 BootstrapClassLoader 加载的;
  • 而 SPI 实现的 Java 类一般是由 AppClassLoader 来加载的。BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给 AppClassLoader,因为它是最顶层的类加载器。这也解释了本节开始的问题 —— 为什么加载 SPI 服务时,需要指定类加载器 ClassLoader 呢?因为如果不指定 ClassLoader,则无法获取 SPI 服务。

如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是 AppClassLoader。在核心类库使用 SPI 接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。 通常可以通过 Thread.currentThread ().getClassLoader () 和 Thread.currentThread ().getContextClassLoader () 获取线程上下文类加载器。

再回顾

刚才 1.3 讲了 JDBC 这个例子,1.4 讲了 SPI 原理,理解了上面的知识之后,我们再来看一下,Java 数据库驱动加载的原理是怎样的,下面是用 JDBC 去连接 MySQL 数据库的示例代码,程序在加载DriverManager 类时,会将 MySQL 的 Driver 对象注册进 DriverManager 中,这是 SPI 思想的一个典型的实现。得益于 SPI 思想,应用程序中无需指定类似 "com.mysql.cj.jdbc.Driver" 这种全类名,尽可能地将第三方驱动从应用程序中解耦出来。

public static void main(String[] args) {
    // JDBC连接URL
    String url = "jdbc:mysql://localhost:3306/mydatabase";
    String username = "root";
    String password = "password";

    try {
        // 创建数据库连接
        Connection connection = DriverManager.getConnection(url, username, password);

        // 在此处执行数据库操作

        // 关闭连接
        connection.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}
}

源码分析: DriverManager 是管理 JDBC 驱动的基础服务类,位于 Java.sql 包中,由 boot 类加载器来进行加载。加载该类时,会先执行如下代码块:

 

然后我们点进loadInitialDrivers()方法,是不是对下图的红框部分有种似曾相识的感觉,没错,这就是咱们前面 1.4.2 小节第 2 点提到的,帮大家回忆一下,主要是下面这 4 点

  1. 应用程序加载 Java SPI 服务,都是先调用 ServiceLoader.load 静态方法,这个方法会new ServiceLoader对象
  2. 当应用程序调用 ServiceLoader 的 iterator 方法时,ServiceLoader 会先判断缓存 providers 中是否有数据:如果有,则直接返回缓存 providers 的迭代器;如果没有,则返回懒加载迭代器的迭代器。
  3. driversIterator.hasNext():此方法用于查找Driver类;
  4. driversIterator.next():在实现的"next()"方法中进行类加载,使用上面的线程上下文类加载器。

 

 如果你这里还是没看明白的话,建议再去 1.4.2 节理解一下,next() 方法会最终调用到 nextService() 方法,会尝试通过 Class 的 newInstance 方法实例化一个 SPI 服务对象。如果成功,则将这个对象加入到缓存 providers 中并返回该对象。 经历了上述ServiceLoader类中的一系列操作之后(包括服务发现和类加载),位于 MySQL 驱动包中的Driver类会被初始化。该类如下所示

 我们点进去这个注册方法,会发现这里会将该 MySQL 驱动添加到成员变量 registeredDrivers 中,该成员变量存放已注册的 JDBC 驱动列表,

 

这样一来,服务发现、类加载、驱动注册便到此结束。接下来,应用程序执行数据库连接操作时,会调用“getConnection”方法,里面会遍历 registeredDrivers,获取驱动,建立数据库连接。

Q:这里又会引申出一个问题,既然是遍历 registeredDrivers 列表,那是怎么区分多个驱动的呢? A:我们来看看 getConnection 方法的核心步骤,DriverManager.getConnection 中会遍历所有已经加载的驱动实例去创建连接,(aDriver.driver.connect这个方法会测试输入的URL的这个驱动是否匹配)当一个驱动创建连接成功时就会返回这个连接,同时不再调用其他的驱动实例。

 

大总结: 现在我们再来看看下面这段话,我相信理解起来没有任何难度

当应用程序需要使用某项服务时,它会在类路径下查找 META-INF/services 目录下的配置文件,文件中列出了对应服务接口的实现类。然后,应用程序可以通过 Java 标准库提供的 ServiceLoader 类动态加载并实例化这些服务提供者,从而使用其功能。这种机制被广泛应用于 Java 中各种框架和组件的扩展开发,例如数据库驱动、日志模块等。

Java SPI 的优点和不足

Java SPI 的一些优点:

  1. 实现解耦:SPI 使得接口的定义与具体业务实现分离,使得应用程序可以根据实际业务情况启用或替换具体组件。
  2. 可插拔性和可扩展性:SPI 允许第三方提供新的服务实现模块,并通过配置文件进行声明,在运行时动态加载,这样可以轻松地扩展和替换系统中的功能模块,实现了可插拔性和可扩展性。

Java SPI 存在一些不足:

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












标签:概念,服务,SPI,接口,spi,Java,ServiceLoader,加载
From: https://www.cnblogs.com/bplan/p/18348039

相关文章

  • 什么是大模型?快速了解大模型基本概念
    在人工智能的世界里,大模型就像超级大脑一样,能够处理和理解大量的信息。你可能听说过ChatGPT,它就是大模型的一个典型代表。那么,什么是大模型呢?让我们一起来探索这个神奇的领域。什么是大模型?想象一下,如果你的大脑能够记住整个图书馆的所有书籍,并且能够理解每本书的内容,那么......
  • Linux文件系统的一些基本概念
    Linux文件系统简介在Linux操作系统中,一切被操作系统管理的资源,如磁盘驱动器、打印机、普通文件或目录等,都被视为文件进行管理和访问。在Linux系统中,“一切都是文件”。Linux系统可以通过统一的文件接口来管理和操作不同类型的资源。Linux可以使用类似于读写文件的方......
  • 经过形式化验证的自动驾驶故障运行安全概念
    经过形式化验证的自动驾驶故障运行安全概念**摘要:**现代自动驾驶(AD)系统依靠安全措施来处理故障并使车辆处于安全状态。为了消除致命的道路交通事故,汽车制造商不断推出新的感知和控制系统。当代汽车设计和安全工程最佳实践适用于单独分析系统组件,而当今高度复杂且相互依赖......
  • AXI4传输层概念和跨4K边界理解
    1.AXI传输层级概念在手册的术语表中,与AXI传输相关的有三个概念,分别是transfer(beat)、burst、transaction。用一句话串联就是:在AXI传输事务(Transaction)中,数据以突发传输(Burst)的形式组织。一次突发传输中可以包含一至多个数据(Transfer)。每个transfer因为使用一个周期,又被......
  • 硬件电路基础概念汇总
    一、概述基础电路相关概念汇总:电荷电子电场电流电量电压电阻电路直流电和交流电弱电和强电家庭电路欧姆定律功率计算焦耳定律串联和并联插件和贴片仿真软件电阻器-电阻参数电阻器-碳膜电阻电阻器-金属碳膜和绕线电阻电阻器-薄膜电阻和厚膜电阻电阻器-......
  • SpringBoot - SPI机制
    SpringBoot-SPI机制  概要  什么是SPI呢,全称是ServiceProviderInterface。简单翻译的话,就是服务提供者接口,是一种寻找服务实现的机制。这个是针对厂商或者插件的。  其实就是一个规范定义、或者说是实现的标准。  一、SPI思想  SPI的思想:系统里抽象......
  • 存储系统基本概念
    存储器的层次化结构存储系统的分类存储器的种类繁多,我们可以从不同的角度对存储器进行分类......
  • 文献综述如何有助于识别研究中的关键变量和概念
    VersaBot文献综述助手进行良好的文献综述对于从多个方面确定研究的关键变量和概念起着至关重要的作用;1.揭示相关领域和理论: 通过沉浸在现有的学术研究中,你会遇到围绕你的主题的各种理论和概念。这些可以作为识别与您的研究问题相关的潜在变量的起点。2.识别关系和相互作......
  • 基于两颗CH582芯片实现GPIO模拟SPI全双工通讯__从机通过GPIO中断读写数据
    简介:此程序是根据标准SPI协议规范使用模式0编写的一份模拟SPI全双工数据收发例程,经过测试,一个字节收发时长可压缩至最低115us左右,约9091字节每秒=73Kbps的通讯速率,注释中尽可能解释了每一步的含义,后续有想法应该会对其进行优化。注:笔者开发经验较少,在编程上或许复杂了一些。......
  • 转型AI产品经理需要掌握的硬知识(一):AI产品经理能力模型和常见AI概念梳理_产品经理学ai
    关于AI常见基础概念,你了解多少?近几年,从亚马逊,Facebook,到谷歌,微软,再到国内的BAT,全球最具影响力的技术公司都将目光转向了人工智能(AI)。2016年AlphaGo战胜李世石,把公众的目光也聚集到了人工智能。创新氛围最活跃的中国,已将人工智能定位国家战略,2017年11月15日,中国新一......