spi概念
参见:https://zhuanlan.zhihu.com/p/692383462
总结:DriverManager流程
假定整个程序处于tomcat这种场景,tomcat位于app类加载器,咱项目位于cl类加载器,cl的父是app。
1、cl类加载器servlet1.java代码里调用 DriverManager.getConnection("jdbc:mysql:xxx");//还得再执行下Thread.setContextClassLoader(cl)不然后面会有问题
2、cl交给app加载 DriverManager.class,触发执行 DriverManager 里static代码块里的loadInitialDrivers()方法
2.1、loadInitialDrivers()方法里调用 ServiceLoader 加载cl加载器范围jar包里的META-INF/services/java.sql.Driver文件
(jdk的spi流程,具体ServiceLoader加载哪些jar是由你调用它哪个方法决定,如果是两个入参方法,则调用loader入参
下负责范围所有jar,如果是一个入参方法,则调用当前线程类加载器负责范围所有jar),
ServiceLoader方法:
load(Class<S> service, ClassLoader loader)签名,
load(Class<S> service)方法里直接用了线程类加载器,
比如cl下有mysql-connector-j-8.0.33.jar和ojdbc6-11.2.0.3.jar,每个jar里都有这个文件,

内容:com.mysql.cj.jdbc.Driver oracle.jdbc.OracleDriver
app类加载器在加载ServiceLoad.class的load(Class<S> service)方法时,会通过线程类加载器加载出上面两个jar
包i的java.sql.Driver文件里的内容,ServiceLoader.load方法内最终会加载这两个驱动类class到Class对象(调用cl类加载器来做),然后实例化出2个实例对象,
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
注意cl类加载器在加载比如com.mysql.cj.jdbc.Driver.class时也会触发该Driver里的static代码块,该代码块里会执行如下代码,
java.sql.DriverManager.registerDriver(new Driver());
这段代码会把mysql的Driver实例对象存到DriverManager的registeredDrivers属性里,同理oracle也会这么干。
注:上面jdk8里DriverManager是有个static静态代码块(里面调用loadInitialDrivers),openjdk9的DriverManager里等价于jdk8的loadInitialDrivers
方法叫ensureDriversInitialized,在getConnection方法里触发调用ensureDriversInitialized方法,也就是openjdk9跟jdk8一样都会先触发jdk的spi通过
线程类加载器加载cl里的驱动类。
3、回到上面步骤1,cl类加载器servlet1代码里调用DriverManager.getConnection("jdbc:mysql:xxx"),该方法里
遍历registeredDrivers集合,一个一个去试,看谁能返回连接对象,
比如遍历到mysql驱动对象,调用该驱动对象的connect(url, info)方法获得Connection对象,到此获取conn流程结束。
加入有多个项目,则有多个加载器,比如cl1,cl2,假设cl1里有mysql5.7,cl2里有mysql8.0,DriverManager的registeredDrivers集合
在遍历时会不会用错呢,答案是不会用错,因为DriverManager.getConnection方法里一个isDriverAllowed方法,该方法里判断比如
得该驱动类的类加载器和当前执行代码的类加载器一致才行,也就是cl1的servlet1执行时如果碰到mysql8.0的驱动对象会发现它的
类加载器不等于当前cl1则跳过该驱动对象。
总结:jdk的spi使用方式la
1、编写框架的人负责编写这样几个角色的文件,分别是DriverManager(内部有个类似static代码块的地方调用ServiceLoader.loader(Driver.class)得到驱动类集合,然后把驱动对象缓存起来,方便别的api里调用驱动对象来干活)、Driver接口
2、编写项目的人负责编写Driver接口实现类,以及META-INF/services/java.sql.Driver文件,内容为foo.bar.MyDriver
注:DriverManager其实不属于app类加载器或ext类加载器,而是最顶级的boot加载器。
注:线程类加载器说明,如果没有设置则当前的线程类加载器是父线程里的线程类加载器,如果就一个线程也没设置过则是加载应用的类加载器,也就是默认是app。

----- 博客原文 -----
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 的四步:
- 服务的调用者要先定义好接口
- 服务的提供者提供了接口的实现
- 需要在类目录下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。
- 当其他的程序需要这个服务(服务提供者提供的)的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。
接下来看看 JDBC 的实践是怎么做的:
- 这是 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