参考文档:《软件框架设计的艺术》第八章
API(Application Programming Interface,应用程序接口)和 SPI(Service Provider Interface,服务提供接口)本质上都是对外提供的规范接口。两者的主要区分
SPI 机制本身也是对外提供的API 定义,不同点在于 SPI 的接口是提供给他人进行功能扩展用的。
SPI 提供的功能扩展使得代码定义更加灵活,在多数框架以及实现中都能够看到其使用的身影,其中最典型的就是 JDBC 规范中 Driver
接口,JDBC 定义了 Driver
接口但是并不提供相关实现,具体实现由不同厂商实现,如:Mysql、PG 等根据自身数据库功能提供JDBC 规范的统一实现。
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.Driver
class 文件即可完成 com.mysql.jdbc.Driver
class 加载,实例化,以及注册到 DriverManager
中。
1.2、使用 JDK SPI 机制动态加载 Driver 实现类
JDK 在 DriverManager 提供了动态加载 SPI 接口实现类的机制,从而实现只需要导入不同服务厂商提供的代码包,无需配置即可实现程序和数据库间的通讯。
具体代码以及实现如下:
二、JDK SPI 机制原理
2.1、JDK SPI 需要的相关配置
SPI 实现通常需要
- 接口定义,如:
java.sql.Driver
- 接口实现类,如:
com.mysql.jdbc.Driver
- SPI 配置实现类绝对路径,如:
META-INF/services/java.sql.Driver
2.2、SPI 机制实现原理
以 mysql 数据库中 java.sql.Driver
实现类动态加载为例,关键代码如下:
如代码所示,关键入口对象为 ServiceLoader
ServiceLoader 对象关键变量
ServiceLoader 对象中的关键变量由
private static final String PREFIX = "META-INF/services/"
SPI 接口实现类配置文件存放路径Class<S> service
被加载实现类对应的接口 classClassLoader loader
类加载器LinkedHashMap<String,S> providers = new LinkedHashMap<>()
已经被加载的实现类 class 缓存LazyIterator lookupIterator
懒加载迭代器,在迭代获取接口实现类时,在动态解析配置文件进行 class 的加载和实例化
LazyIterator 懒加载迭代器
接口实现类的加载默认为懒加载模式,只有当获取对应实现类时才会开始加载class类以及实例化对象。代码例如:
懒加载 LazyIterator#hasNext()
和 LazyIterator#next()
代码流程:
关键步骤如下
- 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 接口实现类的动态加载,但是在实现上有几个不足点
- 所有实现类的加载都会被便利加载,无法按需加载
- 获取某个实现类的方式只能通过迭代的方式获取,而不同直接根据某个参数获取