# SPI 机制详解
SPI(Service Provider Interface),是 JDK 内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用。
必看视频:10 分钟让你彻底明白 Java SPI,附实例代码演示 #安员外很有码
这个教学视频讲的详略得当,还有代码实战。
从 SPI 机制就可以看出,接口的存在是多么的重要,比如 java.sql.Driver 接口,其他不同厂商可以针对同一接口做出不同的实现,而我们只需要调用接口即可,根本不关心实现是怎么样的,SPI 机制主要思想就是将装配的控制权转移到程序之外。整个学习过程你需要牢记一个词 —— 解耦。
# 概念和术语
这部分我就直接照搬上面视频里的内容了,你们看完了有一键三连吗?
- Sevice:一个公开的接口或抽象类,定义了一个抽象的功能模块。其实就是定义了能够提供哪些行为
- Service Provider:Service 的实现类,通常是第三方实现的。
- ServiceLoader:SPI 机制的核心组件,负责在运行时发现并加载 Service Provider。
整个简化的执行流程就是:
# 简单的 SPI 案例
如果你理解了上面的思想,那么我们就来简单的使用下,这部分请务必去 IDEA 尝试一下,相关代码下文可以获取了:
- 首先你需要建立这样的文件目录结构
注意,spi-demo 是父项目,如果不会用 IDEA 建立父子工程,给个传送门 ,创建好后,我来解释一下,company 项目是我们实际运行的项目,一个正常的,有 main 的项目。api 项目你可以当成是一个门面框架,你将会在 company 项目中用到它,但是 api 项目只有接口,没有实现,也就是定义了对应的行为,将实现交给其他厂商、社区等实现,你可以类比 jdbc。两个 provider 项目就是实现接口的项目,你可以类比 mysql 厂商实现 jdbc 和 Oracle 厂商实现 jdbc,我们要使用哪个,就导入哪个的 jar 包。
几个重要的设计点:
- 两个 provider 要实现 api 项目的接口,就要导入 api 的依赖。
- SPI 的配置文件要放在
META-INF/services
目录下,配置文件名为 service 接口的全限定名,文件内容是 Service Provider 类的全类限定名,多个 Service Provide 用多行表示。
- 实现类必须要有无参构造函数。
我们现在想用 api 这个项目(框架),那么就要导入这个依赖,同时,想用 poviderA 提供的实现,也要导入它的依赖,然后在运行项目里面编写代码:
public class Application { | |
public static void main(String[] args) { | |
ServiceLoader<ActionService> loader = ServiceLoader.load(ActionService.class); | |
for(ActionService provider : loader) { | |
provider.sendMessage("慕青の迷途"); | |
} | |
} | |
} | |
/** | |
结果: | |
A 方式发送消息:慕青の迷途 | |
A 扩展模块发送消息:慕青の迷途 | |
*/ |
相关代码放到阿里云盘上了:https://www.aliyundrive.com/s/b5CUyFZ16gX
解释一下:
ServiceLoader.load(Search.class)
在加载某接口时,会去 META-INF/services
下找接口的全限定名文件,再根据里面的内容加载相应的实现类。
这就是 SPI 的思想,接口的实现由 provider 实现,provider 只用在提交的 jar 包里的 META-INF/services
下根据平台定义的接口新建文件,并添加进相应的实现类内容就好。
# SPI 机制应用
SPI 机制应用十分广泛,之前就提到过,多用于框架开发
# JDBC DriverManager
如果你用过旧版本的 JDBC(4.0 之前,jdbc4.0 随 jdk1.6 一起发布),那么连接数据库时通常会如此加载数据库相关驱动:
Class.forName("com.mysql.jdbc.Driver") |
但是之后就不用了,这得益于 SPI 机制。
基于内容的完整性,这里给出 jdbc 的操作,你可以自己改一下 url,user,password 以及 sql 语句来适配你的数据库:
public class Application { | |
public static void main(String[] args) throws SQLException { | |
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/study?serverTimezone=Asia/Shanghai&useSSL=false", | |
"root", "123456"); | |
String sql = "select username from users"; | |
ResultSet set = conn.createStatement().executeQuery(sql); | |
while(set.next()) { | |
System.out.println(set.getString("username")); | |
} | |
} | |
} |
mysql 的依赖,上面的代码可以直接执行,不需要加载驱动。
- JDBC 接口定义:jdk 本来就有 jdbc 定义,在
java.sql.*
包下,但是没有具体的实现,这些实现由不同厂商提供。你可以看看java.sql.Driver
定义了哪些方法。 - mysql 实现:导入依赖,前文已经给出,然后直接看
mysql-connector-java-x.x.x.jar
下有个 META-INF 包。
- 同理,你可以导入其他数据库厂商实现的 jdbc 依赖,比如 postgresql。
# 源码实现
我们现在不需要使用 Class.forName("com.mysql.jdbc.Driver")
加载驱动了,直接就可以使用 DriverManager
获取连接。但是现在我们并没有看到 SPI 的使用,
我们希望通过 SPI 机制加载驱动(也就是 Driver 的实现),这个肯定不由厂商决定,所以猜都猜得到应该在 java.sql.*
包下找。
关于驱动的查找其实都在 DriverManager
中, DriverManager
是 Java 中的实现,用来获取数据库连接,在 DriverManager
中有一个静态代码块如下:
static { | |
loadInitialDrivers(); | |
println("JDBC DriverManager initialized"); | |
} |
很明显,加载 DriverManager
时,就会执行 loadInitialDrivers
方法:
private static void loadInitialDrivers() { | |
String drivers; | |
try { | |
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { | |
public String run() { | |
return System.getProperty("jdbc.drivers"); | |
} | |
}); | |
} catch (Exception ex) { | |
drivers = null; | |
} | |
AccessController.doPrivileged(new PrivilegedAction<Void>() { | |
public Void run() { | |
// 使用 SPI 的 ServiceLoader 来加载接口的实现 | |
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); | |
Iterator<Driver> driversIterator = loadedDrivers.iterator(); | |
try{ | |
while(driversIterator.hasNext()) { | |
driversIterator.next(); | |
} | |
} catch(Throwable t) { | |
// Do nothing | |
} | |
return null; | |
} | |
}); | |
// 此时 registeredDrivers 已经有 Driver 实现类了 | |
println("DriverManager.initialize: jdbc.drivers = " + drivers); | |
if (drivers == null || drivers.equals("")) { | |
return; | |
} | |
String[] driversList = drivers.split(":"); | |
println("number of Drivers:" + driversList.length); | |
for (String aDriver : driversList) { | |
try { | |
println("DriverManager.Initialize: loading " + aDriver); | |
Class.forName(aDriver, true, | |
ClassLoader.getSystemClassLoader()); | |
} catch (Exception ex) { | |
println("DriverManager.Initialize: load failed: " + ex); | |
} | |
} | |
} |
上面的代码主要步骤是:
- 从系统变量中获取有关驱动的定义。
- 使用 SPI 来获取驱动的实现。
- 遍历使用 SPI 获取到的具体实现,实例化各个实现类。
- 根据第一步获取到的驱动列表来实例化具体实现类。
解释一下关键代码:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); |
这里没有去 META-INF/services
目录下查找配置文件,也没有加载具体实现类,做的事情就是封装了我们的接口类型和类加载器,并初始化了一个迭代器。
// 获取迭代器 | |
Iterator<Driver> driversIterator = loadedDrivers.iterator(); | |
// 遍历所有的驱动实现 | |
while(driversIterator.hasNext()) { | |
driversIterator.next(); | |
} |
在遍历的时候,首先调用 driversIterator.hasNext()
方法,这里会搜索 classpath 下以及 jar 包中所有的 META-INF/services
目录下的 java.sql.Driver
文件,并找到文件中的实现类的名字,此时并没有实例化具体的实现类。
然后是调用 driversIterator.next();
方法,此时就会根据驱动名字具体实例化各个实现类了。现在驱动就被找到并实例化了。
看一下 Driver 实现类的源码:
package com.mysql.cj.jdbc; | |
import java.sql.DriverManager; | |
import java.sql.SQLException; | |
public class Driver extends NonRegisteringDriver implements java.sql.Driver { | |
public Driver() throws SQLException { | |
} | |
static { | |
try { | |
DriverManager.registerDriver(new Driver()); | |
} catch (SQLException var1) { | |
throw new RuntimeException("Can't register driver!"); | |
} | |
} | |
} |
可以看到当加载 Driver 类时,执行静态代码块就已经将实现类的实例化对象注册到 DriverManager
里面了。
# SPI 实现原理
ServiceLoader
源码有 200 + 行代码,有时间可以看一下:
//ServiceLoader 实现了 Iterable 接口,可以遍历所有的服务实现者 | |
public final class ServiceLoader<S> | |
implements Iterable<S> | |
{ | |
// 查找配置文件的目录 | |
private static final String PREFIX = "META-INF/services/"; | |
// 表示要被加载的服务的类或接口 | |
private final Class<S> service; | |
// 这个 ClassLoader 用来定位,加载,实例化服务提供者 | |
private final ClassLoader loader; | |
// 访问控制上下文 | |
private final AccessControlContext acc; | |
// 缓存已经被实例化的服务提供者,按照实例化的顺序存储 | |
private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); | |
// 迭代器 | |
private LazyIterator lookupIterator; | |
// 重新加载,就相当于重新创建 ServiceLoader 了,用于新的服务提供者安装到正在运行的 Java 虚拟机中的情况。 | |
public void reload() { | |
// 清空缓存中所有已实例化的服务提供者 | |
providers.clear(); | |
// 新建一个迭代器,该迭代器会从头查找和实例化服务提供者 | |
lookupIterator = new LazyIterator(service, loader); | |
} | |
// 私有构造器 | |
// 使用指定的类加载器和服务创建服务加载器 | |
// 如果没有指定类加载器,使用系统类加载器,就是应用类加载器。 | |
private ServiceLoader(Class<S> svc, ClassLoader cl) { | |
service = Objects.requireNonNull(svc, "Service interface cannot be null"); | |
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; | |
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; | |
reload(); | |
} | |
// 解析失败处理的方法 | |
private static void fail(Class<?> service, String msg, Throwable cause) | |
throws ServiceConfigurationError | |
{ | |
throw new ServiceConfigurationError(service.getName() + ": " + msg, | |
cause); | |
} | |
private static void fail(Class<?> service, String msg) | |
throws ServiceConfigurationError | |
{ | |
throw new ServiceConfigurationError(service.getName() + ": " + msg); | |
} | |
private static void fail(Class<?> service, URL u, int line, String msg) | |
throws ServiceConfigurationError | |
{ | |
fail(service, u + ":" + line + ": " + msg); | |
} | |
// 解析服务提供者配置文件中的一行 | |
// 首先去掉注释校验,然后保存 | |
// 返回下一行行号 | |
// 重复的配置项和已经被实例化的配置项不会被保存 | |
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, | |
List<String> names) | |
throws IOException, ServiceConfigurationError | |
{ | |
// 读取一行 | |
String ln = r.readLine(); | |
if (ln == null) { | |
return -1; | |
} | |
//# 号代表注释行 | |
int ci = ln.indexOf('#'); | |
if (ci >= 0) ln = ln.substring(0, ci); | |
ln = ln.trim(); | |
int n = ln.length(); | |
if (n != 0) { | |
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0)) | |
fail(service, u, lc, "Illegal configuration-file syntax"); | |
int cp = ln.codePointAt(0); | |
if (!Character.isJavaIdentifierStart(cp)) | |
fail(service, u, lc, "Illegal provider-class name: " + ln); | |
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) { | |
cp = ln.codePointAt(i); | |
if (!Character.isJavaIdentifierPart(cp) && (cp != '.')) | |
fail(service, u, lc, "Illegal provider-class name: " + ln); | |
} | |
if (!providers.containsKey(ln) && !names.contains(ln)) | |
names.add(ln); | |
} | |
return lc + 1; | |
} | |
// 解析配置文件,解析指定的 url 配置文件 | |
// 使用 parseLine 方法进行解析,未被实例化的服务提供者会被保存到缓存中去 | |
private Iterator<String> parse(Class<?> service, URL u) | |
throws ServiceConfigurationError | |
{ | |
InputStream in = null; | |
BufferedReader r = null; | |
ArrayList<String> names = new ArrayList<>(); | |
try { | |
in = u.openStream(); | |
r = new BufferedReader(new InputStreamReader(in, "utf-8")); | |
int lc = 1; | |
while ((lc = parseLine(service, u, r, lc, names)) >= 0); | |
} | |
return names.iterator(); | |
} | |
// 服务提供者查找的迭代器 | |
private class LazyIterator | |
implements Iterator<S> | |
{ | |
Class<S> service;// 服务提供者接口 | |
ClassLoader loader;// 类加载器 | |
Enumeration<URL> configs = null;// 保存实现类的 url | |
Iterator<String> pending = null;// 保存实现类的全名 | |
String nextName = null;// 迭代器中下一个实现类的全名 | |
private LazyIterator(Class<S> service, ClassLoader loader) { | |
this.service = service; | |
this.loader = loader; | |
} | |
private boolean hasNextService() { | |
if (nextName != null) { | |
return true; | |
} | |
if (configs == null) { | |
try { | |
String fullName = PREFIX + service.getName(); | |
if (loader == null) | |
configs = ClassLoader.getSystemResources(fullName); | |
else | |
configs = loader.getResources(fullName); | |
} | |
} | |
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); | |
} | |
if (!service.isAssignableFrom(c)) { | |
fail(service, "Provider " + cn + " not a subtype"); | |
} | |
try { | |
S p = service.cast(c.newInstance()); | |
providers.put(cn, p); | |
return p; | |
} | |
} | |
public boolean hasNext() { | |
if (acc == null) { | |
return hasNextService(); | |
} else { | |
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { | |
public Boolean run() { return hasNextService(); } | |
}; | |
return AccessController.doPrivileged(action, acc); | |
} | |
} | |
public S next() { | |
if (acc == null) { | |
return nextService(); | |
} else { | |
PrivilegedAction<S> action = new PrivilegedAction<S>() { | |
public S run() { return nextService(); } | |
}; | |
return AccessController.doPrivileged(action, acc); | |
} | |
} | |
public void remove() { | |
throw new UnsupportedOperationException(); | |
} | |
} | |
// 获取迭代器 | |
// 返回遍历服务提供者的迭代器 | |
// 以懒加载的方式加载可用的服务提供者 | |
// 懒加载的实现是:解析配置文件和实例化服务提供者的工作由迭代器本身完成 | |
public Iterator<S> iterator() { | |
return new Iterator<S>() { | |
// 按照实例化顺序返回已经缓存的服务提供者实例 | |
Iterator<Map.Entry<String,S>> knownProviders | |
= providers.entrySet().iterator(); | |
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(); | |
} | |
}; | |
} | |
// 为指定的服务使用指定的类加载器来创建一个 ServiceLoader | |
public static <S> ServiceLoader<S> load(Class<S> service, | |
ClassLoader loader) | |
{ | |
return new ServiceLoader<>(service, loader); | |
} | |
// 使用线程上下文的类加载器来创建 ServiceLoader | |
public static <S> ServiceLoader<S> load(Class<S> service) { | |
ClassLoader cl = Thread.currentThread().getContextClassLoader(); | |
return ServiceLoader.load(service, cl); | |
} | |
// 使用扩展类加载器为指定的服务创建 ServiceLoader | |
// 只能找到并加载已经安装到当前 Java 虚拟机中的服务提供者,应用程序类路径中的服务提供者将被忽略 | |
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) { | |
ClassLoader cl = ClassLoader.getSystemClassLoader(); | |
ClassLoader prev = null; | |
while (cl != null) { | |
prev = cl; | |
cl = cl.getParent(); | |
} | |
return ServiceLoader.load(service, prev); | |
} | |
public String toString() { | |
return "java.util.ServiceLoader[" + service.getName() + "]"; | |
} | |
} |
首先,ServiceLoader 实现了 Iterable
接口,所以它有迭代器的属性,这里主要都是实现了迭代器的 hasNext
和 next
方法。这里主要都是调用的 lookupIterator
的相应 hasNext
和 next
方法, lookupIterator
是懒加载迭代器。
其次, LazyIterator
中的 hasNext
方法,静态变量 PREFIX 就是 ”META-INF/services/”
目录,这也就是为什么需要在 classpath
下的 META-INF/services/
目录里创建一个以服务接口命名的文件。
最后,通过反射方法 Class.forName()
加载类对象,并用 newInstance
方法将类实例化,并把实例化后的类缓存到 providers
对象中,( LinkedHashMap<String,S>
类型)然后返回实例对象。
所以我们可以看到 ServiceLoader
不是实例化以后,就去读取配置文件中的具体实现,并进行实例化。而是等到使用迭代器去遍历的时候,才会加载对应的配置文件去解析,调用 hasNext
方法的时候会去加载配置文件进行解析,调用 next
方法的时候进行实例化并缓存。
所有的配置文件只会加载一次,服务提供者也只会被实例化一次,重新加载配置文件可使用 reload
方法。
可以看到,如果是第一次执行这段代码:
Iterator<ActionService> iterator = loader.iterator(); | |
while(iterator.hasNext()) { | |
iterator.next().sendMessage("慕青の迷途"); | |
} |
因为 provider 的实例化是懒加载,所以 LinkedHashMap
每次迭代,size 才 + 1,那么上面迭代器 hashNext
和 next
走的路径都是 lookupIterator.hasNext()/next()
。
之后再调用这段代码(如果没有调用 reload ()),就会直接走 if
语句,因为此时 LinkedHashMap
已经缓存了 provider 实例。
# 参考
jdbc 操作
https://pdai.tech/md/java/advanced/java-advanced-spi.html
https://www.bilibili.com/video/BV1RY4y1v7mN/?spm_id_from=333.788&vd_source=5acf5a7b23d28e7633e5a9b381c57c42
https://zhuanlan.zhihu.com/p/305056347