前言
讲 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:调用方只会去依赖上图的标准服务接口,而不会去管实现类,这样做的优点有哪些呢?
- 如果你想再增加一个实现类,你只需要去实现这个接口就可以,其他地方的代码都不用动,这是扩展性的体现
- 又或者是某天你不需要实现类A了,那直接把实现类A去掉就可以了,对整个系统不会有大的改动,这就是可插拔和组件化思想的好处,此时整个系统还实现了充分的解偶
SPI 应用案例之 JDBC DriverManager
众所周知,关系型数据库有很多种,如:MySQL、Oracle、PostgreSQL 等等。Java 的 JDBC 提供了一套 API 供 Java 应用与数据库进行交互,但是,不同的数据库在底层实现上是有区别的呀,我现在想用这一套 API 对所有数据库都适用,那怎么办勒?此时就出现了一个东西,这个东西就是驱动。
**举个例子:**这就相比于你说中文,但是你的客户可能有说英语、法语、德语等等,此时你是不是希望有个翻译,而且是有多个翻译,有翻译成英语的,翻译法语的等等(假设一个翻译只能把中文翻译成一种语言),有了不同的翻译之后,这样就可以把你说的中文翻译给不同语言的人听,而驱动就是翻译
实现 SPI 的四步:
- 服务的调用者要先定义好接口
- 服务的提供者提供了接口的实现
- 需要在类目录下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。
- 当其他的程序需要这个服务(服务提供者提供的)的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的 META-INF/services/ 中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。
接下来看看 JDBC 的实践是怎么做的:
- 这是 java.sql.Driver(JDK)中定义驱动的接口(对应在服务的调用者要先定义好接口)
- 这是MySQL驱动中的Driver类,它实现了上面的Driver接口
- 并且我们发现在META-INF/services/ 目录里创建一个以服务接口(java.sql.Driver)命名的文件,这个文件里的内容就是这个接口的具体的实现类
- 怎么去把驱动的服务提供给调用者呢?现在常用的就是直接引入依赖就可以
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