通俗易懂的解释 Java 中的 SPI 机制

前言

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

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

  1. 「接口」定义在「服务提供方」
  2. 「接口」定义在「服务调用方」

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

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

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

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

总结一下:

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

概念

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

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

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

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

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

图2

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

图3

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

图4

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 静态方法
image.png
应用程序加载 Java SPI 服务,都是先调用 ServiceLoader.load 静态方法,这个方法会new ServiceLoader对象
ServiceLoader.load 静态方法的作用是:

  1. 指定类加载 ClassLoader 和访问控制上下文;
  2. 然后,重新加载 SPI 服务
  • 清空缓存中所有已实例化的 SPI 服务
  • 根据 ClassLoader 和 SPI 类型,创建懒加载迭代器

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

//
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值