1. 使用方法

1.1. 约定

Java SPI 就是这样做的,约定在 Classpath 下的 META-INF/services/ 目录里创建一个 服务接口命名的文件,然后 **文件里面记录的是此 jar 包提供的具体实现类的全限定名**。

这样当我们引用了某个 jar 包的时候就可以去找这个 jar 包的 META-INF/services/ 目录,再根据接口名找到文件,然后读取文件里面的内容去进行实现类的加载与实例化。

1.2. 示例

1.2.1. DriverManager-MySQL

比如我们看下 MySQL 是怎么做的。

image.png

再来看一下文件里面的内容。
image.png

1.2.2. SpringMVC

Spring-4、SpringMVC

image.png

2. 源码原理

以下源码分析基于 JDK8

2.1. Java SPI 入口-ServiceLoader

ServiceLoader.load()

2.2. 运行逻辑

在 ServiceLoader.load() 方法中,首先会尝试获取当前使用的 ClassLoader(获取当前线程绑定的 ClassLoader,查找失败后使用 SystemClassLoader),然后调用 reload() 方法,调用关系如下图所示:

image.png

在 reload() 方法中,首先会清理 providers 缓存(LinkedHashMap 类型的集合),==该缓存用来记录 ServiceLoader 创建的实现对象==,其中 Key 为实现类的完整类名,Value 为实现类的对象。之后创建 LazyIterator 迭代器,用于读取 SPI 配置文件并实例化实现类对象。

image.png

在前面的示例中,main() 方法中使用的迭代器底层就是调用了 ServiceLoader.LazyIterator 实现的。Iterator 接口有两个关键方法:hasNext() 方法和 next() 方法。这里的 LazyIterator 中的 next() 方法最终调用的是其 nextService() 方法,hasNext() 方法最终调用的是 hasNextService() 方法,调用关系如下图所示:

image.png

2.2.1. hasNextService- 查找

首先来看 LazyIterator.hasNextService() 方法,该方法主要 负责查找 META-INF/services 目录下的 SPI 配置文件,并进行遍历。

2.2.2. nextService- 实例化

在 hasNextService() 方法中完成 SPI 配置文件的解析之后,再来看 LazyIterator.nextService() 方法,该方法 负责实例化 hasNextService() 方法读取到的实现类,其中会将实例化的对象放到 providers 集合中缓存起来

image.png

2.3. 触发逻辑

2.3.1. DriverManager

静态代码块中的 loadInitialDrivers 方法中或者新版本的 ensureDriversInitialized 方法中,调用 ServiceLoader.load(Driver.class)

性能调优-基础-6、JVM-类装载子系统

image.png

3. 存在问题

相信大家一眼就能看出来,Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且 将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。

所以说 Java SPI 无法按需加载实现类。

4. Java 中的 SPI

4.1. SpringMVC

Spring-4、SpringMVC

4.2. DubboSPI

分布式专题-14、Dubbo-SPI

4.3. SpringBoot

SpringBoot-1、基本原理

5. 实战经验

6. 参考与感谢

6.1. 源码原理

[[03 Dubbo SPI 精析,接口实现两极反转(上).md]]
https://juejin.cn/post/6872138926216511501#heading-2

6.2. 使用方法示例代码

分布式专题-14、Dubbo-SPI