参考文档:《软件框架设计的艺术》第八章

API(Application Programming Interface,应用程序接口)和 SPI(Service Provider Interface,服务提供接口)本质上都是对外提供的规范接口。两者的主要区分

  • API 更多是提供给他人调用(如:开发人员),以实现某项功能。
  • SPI 更多是提供扩展点给他人进行功能扩展(如:基础设施、框架开发人员),以实现对功能的扩展。

    一、JDK SPI 机制

SPI 机制本身也是对外提供的API 定义,不同点在于 SPI 的接口是提供给他人进行功能扩展用的。

SPI 提供的功能扩展使得代码定义更加灵活,在多数框架以及实现中都能够看到其使用的身影,其中最典型的就是 JDBC 规范中 Driver接口,JDBC 定义了 Driver接口但是并不提供相关实现,具体实现由不同厂商实现,如:Mysql、PG 等根据自身数据库功能提供JDBC 规范的统一实现。

image.png

Driver接口实现类无论是哪个厂商提供的,都需要将其加载到 JVM 中并实例化。

下面以 Mysql 数据库提供的 mysql-connector-java为例。

Mysql 数据库厂商遵循 JDBC 规范提供了 mysql-connector-java代码包供开发者使用。
在引入对应的 mysql-connector-java代码包的同时也引入了 Driver接口对应的实现 com.mysql.jdbc.Driver
在实际使用过程中,需要将 com.mysql.jdbc.Driver实例化并将对象注册到 DriverManager中,然后通过 DriverManager即可获取到与数据库的连接对象 Connection,有了Connection即可和数据库进行通讯。

1.1、不使用 JDK SPI 机制加载 Driver 实现类

在不使用 SPI 机制时,通常采用的方如下代码

直接通过 Class.forName加载 com.mysql.jdbc.Driverclass 文件即可完成 com.mysql.jdbc.Driverclass 加载,实例化,以及注册到 DriverManager中。

具体代码如下:
image.png

1.2、使用 JDK SPI 机制动态加载 Driver 实现类

JDK 在 DriverManager 提供了动态加载 SPI 接口实现类的机制,从而实现只需要导入不同服务厂商提供的代码包,无需配置即可实现程序和数据库间的通讯。

具体代码以及实现如下:
image.png

二、JDK SPI 机制原理

2.1、JDK SPI 需要的相关配置

SPI 实现通常需要

  • 接口定义,如:java.sql.Driver
  • 接口实现类,如:com.mysql.jdbc.Driver
  • SPI 配置实现类绝对路径,如:META-INF/services/java.sql.Driver
    image.png

2.2、SPI 机制实现原理

以 mysql 数据库中 java.sql.Driver实现类动态加载为例,关键代码如下:
image.png
如代码所示,关键入口对象为 ServiceLoader

ServiceLoader 对象关键变量

image.png
ServiceLoader 对象中的关键变量由

  • private static final String PREFIX = "META-INF/services/"
    SPI 接口实现类配置文件存放路径
  • Class<S> service
    被加载实现类对应的接口 class
  • ClassLoader loader
    类加载器
  • LinkedHashMap<String,S> providers = new LinkedHashMap<>()
    已经被加载的实现类 class 缓存
  • LazyIterator lookupIterator
    懒加载迭代器,在迭代获取接口实现类时,在动态解析配置文件进行 class 的加载和实例化

LazyIterator 懒加载迭代器

接口实现类的加载默认为懒加载模式,只有当获取对应实现类时才会开始加载class类以及实例化对象。代码例如:
image.png

懒加载 LazyIterator#hasNext()LazyIterator#next() 代码流程:

image.png

关键步骤如下

  • 1、hasNext()方法判断是否存在下一个时,动态加载配置在 META-INF/services/xxxx文件中的类全限定名,并存放到变量 nextName
  • 2、next()方法在获取对象时,根据 nextName指定的全限定名尝试使用 Class.forName加载 class 类,并通过反射的方式进行实例化,并将成功实例化的数据存放到缓存 LinkedHashMap<String,S> providers中。

    三、JDK SPI 总结

    3.1、优点

    SPI 机制的核心思想是解耦
    针对系统设计提供统一的流程接口控制,而具体实现可以有对应的开发人员进行实现提供可拔插的设计。

3.2、不足

JDK SPI 通过 ServiceLoadr实现了 SPI 接口实现类的动态加载,但是在实现上有几个不足点

  • 所有实现类的加载都会被便利加载,无法按需加载
  • 获取某个实现类的方式只能通过迭代的方式获取,而不同直接根据某个参数获取