探究 Java SPI 机制
阅读 vivo 互联网技术 《源码级深度理解 Java SPI》后记录
什么是 SPI
简介
SPI 全称 Service Provider Interface,是 Java 提供的,旨在由第三方实现或扩展的API,是一种动态加载服务的机制。
从字面意义上也能看出,服务提供者的接口,也就是服务提供者将依据此接口进行服务实现。
SPI 组成
Java SPI 四要素:
- SPI 接口:为服务提供者实现类约束的接口或抽象类。
- SPI 实现类:实际提供服务的实现类。
- SPI 配置:Java SPI 机制约定的配置文件,提供查找服务实现类的逻辑。配置文件必须置于 META-INF/services 目录中,文件名应与服务提供者接口的完全限定名保持一致。文件中每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。
- ServiceLoader:Java SPI 的核心类,用于加载 SPI 实现类。ServiceLoader 中有各种实用方法来获取特定实现、迭代它们或重新加载服务。
简单使用 SPI
https://github.com/yancy0109/code-examples/tree/main/spi
定义 SPI
package com.yancy.spi;
/**
* SPI 接口
* @author yancy0109
*/
public interface DataStorage {
String search(String key);
}
提供实现类
package com.yancy.spi.Impl;
import com.yancy.spi.DataStorage;
/**
* @author yancy0109
* @date: 2024/2/6
*/
public class MysqlStorage implements DataStorage {
@Override
public String search(String key) {
return "[Mysql] [Search] " + key + ", Result: No";
}
}
package com.yancy.spi.Impl;
import com.yancy.spi.DataStorage;
/**
* @author yancy0109
* @date: 2024/2/6
*/
public class RedisStorage implements DataStorage {
@Override
public String search(String key) {
return "[Redis] [Search] " + key + ", Result: No";
}
}
声明实现类
META-INF/services/com.yancy.spi.DataStorage
com.yancy.spi.Impl.MysqlStorage
com.yancy.spi.Impl.RedisStorage
调用
通过 ServiceLoader 加载对应类的实现类,调用接口方法
public class SpiDemo {
public static void main(String[] args) {
ServiceLoader<DataStorage> serviceLoader = ServiceLoader.load(DataStorage.class);
serviceLoader.forEach(dataStorage -> System.out.println(dataStorage.search("SPI_Test, Yes or NO?")));
}
}
SPI 原理 - ServiceLoader
通过 ServiceLoader 即可解析获取实现服务对象,那么它是如何工作的呢?接下来继续走进 ServiceLoader 的实现。
ServiceLoader 成员变量
首先来看一看 ServiceLoader 会保存什么数据
public final class ServiceLoader<S> implements Iterable<S>
{
// 我们存放SPI实现类信息的目录
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
// 表示要被加载的SPI服务的类或接口
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
// 用于加载 SPI 服务的类加载器
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
// ServiceLoader 创建时的访问控制上下文
private final AccessControlContext acc;
// Cached providers, in instantiation order
// SPI服务缓存,按照实例化顺序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
// 懒查询迭代器
private LazyIterator lookupIterator;
// .......
}
ServiceLoader 工作流程
Load 方法
通过此静态方法,对指定 Class 进行 SPI 服务加载初始化。
- 指定类加载 ClassLoader 和 访问控制上下文
- 重新加载 SPI 服务
- 清空缓存中所有已实例化的 SPI 服务
- 根据 ClassLoader 和 SPI 类型,创建懒加载迭代器
源码如下:
public final class ServiceLoader<S> implements Iterable<S>
{
// .......
/**
* service: SPI
* loader:用于加载 SPI 服务的类加载器
*/
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader(); // 省略cl,则使用当前Thread#ClassLoader
return ServiceLoader.load(service, cl);
}
public void reload() {
providers.clear(); // 清除缓存中所有已实例化的SPI服务
lookupIterator = new LazyIterator(service, loader); // 根据 ClassLoader 与 SPI类型,创建懒加载迭代器
}
// 私有构造方法
// 构建 ServiceLoader
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null"); // 检查 SPI Non Null
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; // 指定类加载器
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; // AccessControlContext初始化
reload(); // 重加载SPI服务
}
// ......
}
迭代方法
可以看到 ServiceLoader 实现了可迭代接口,他提供了一个方法获取迭代器进行对 SPI实现类 迭代。
public final class ServiceLoader<S> implements Iterable<S> {
// .......
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator(); // 从缓存中获取provider(LinkedList)迭代器,这个迭代器会在创建时被初始化,元素不会再被该变
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext(); // lookupIterator 即ServiceLoader内部LazyIterator
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}
可以看到,优先从 providers 获取迭代器,如果没有则通过懒加载器遍历。
从这里我们也可以猜到了,懒加载器将通过迭代实例化SPI服务实现类,并将其存储至 providers。
LazyIterator 工作流程
hasNextService
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName(); // 拼接全路径名
// 获取SPI配置文件
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(); // 标记 nextName,SPI下一个实现类
return true;
}
ClassLoader.getSystemResources
针对上方代码,我还进行了额外的测试
public class getResourceDemo {
public static void main(String[] args) throws IOException {
Enumeration<URL> systemResources = ClassLoader.getSystemResources("META-INF/services/com.yancy.spi.DataStorage");
while (systemResources.hasMoreElements()) {
URL url = systemResources.nextElement();
System.out.println(url.getFile());
}
}
}
输出如下:
他把所有目标文件文资源全部进行了加载,我们可以通过遍历获取所有目标 SPI 配置文件,也就可以依据于此获取目标 SPI 实现类。
接下来则通过 parse 方法,来对 SPI实现类配置 进行逐行读取。
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); // 从文件中读取行处理 直至读取结束
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator(); // 返回当前资源已读取类名迭代器
}
// Parse a single line from the given configuration file, adding the name
// on the line to the names list.
//
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); // c>=0 获取非注释内容
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); // 将目标SPI实现类加入List
}
return lc + 1;
}
nextService
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader); // 加载目标SPI实现类
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance()); // 强转SPI
providers.put(cn, p); // providers 存放当前实例缓存
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
当一轮循环结束后,下次遍历,重新获取 Iterator,就能直接从缓存中获取已实例化的实现对象了。
SPI与类加载器
ServiceLoader 在加载服务时,可以指定传入类加载器 ClassLoader 来用于加载 SPI Config 指定类,为什么加载 SPI 服务时,要指定类加载器 ClassLoader 呢?ServiceLoader 默认采用 AppClassLoader。
双亲委派模型
概念
先来了解一个双亲委派模型:一个类加载器首先将类加载器请求传送到父类加载器,只有当父类加载器无法完成类加载请求时,才尝试加载。
JVM 三个重要的 ClassLoader:
- BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++ 实现,通常表示为 null,没有父级,主要用于加载 JDK 内部(%JAVA_HOME%/lib)的核心类库。
- ExtensionClassLoader(扩展类加载器):主要负责加载 %JAVA_HOME%/lib/ext 目录下的 Jar 包和类 以及被 java.ext.dirs 系统变量所指定路径下的所有类。
- AppClasLoader(应用程序类加载器):面向用户的加载器,负责加载当前 classpath 下所有的 Jar包和类。
好处
使得 Java类 伴随着它的类加载器,天然具备一种优先级的层次关系,从而使得类加载得到统一,不会出现重复加载问题。
- 防止系统类内存中出现同样的字节码
- 保证 Java 程序安全稳定运行
- 防止恶意代码通过自定义的类加载器去替换 Java 核心类库中的关键类。
为什么需要指定类加载器
子类加载器可以使用父类加载器已经加载的类,而父类加载器无法使用子类加载器已加载的类。
- SPI 接口若为 Java核心库 的一部分,将交由 BootstrapClassLoader 加载。
- SPI 实现类则一般由 AppClassLoader 来加载。BootstrapClassLoader 无法找到其实现类,因为只加载了 Java核心库。所以,ServiceLoader 必须要指定一个 ClassLoader 才能读取到目标服务实现类。
Java 应用的线程的上下文类加载器就是 AppClassLoader。在核心类库使用 SPI 接口时,传递的类加载器使用线程上下文类加载器,就可以成功加载 SPI 实现类。线程上下文类加载器在很多 SPI 实现中都会用到。
Java SPI 的不足
不能按需加载,需要遍历所有的实现,并进行实例化,才能在循环中找到我们需要的实现。
获取实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
多个并发多线程使用 ServiceLoader 类的实例是不安全的。