本文是在做API网关项目的第13章时,需要用到Java SPI机制的思想,由于笔者以往没有了解过这一块的东西,所以在学习相关的内容后,对其做了一个梳理。
SPI机制简介
SPI 即 Service Provider Interface :字面意思就是:“服务提供者的接口”,是一种JDK内置的动态加载实现扩展点的机制。SPI将接口和接口的实现分离开来,将调用方和服务方解耦。上层代码只依赖一个服务接口(Service Interface),而真正的服务实现(Service Provider)可以在运行时被发现与装配,不用改动调用方代码、不用手动 new 指定实现。这个机制有一些类似于IOC的控制反转(将组件装配的控制权移交给了程序之外),提供了一个服务提供者,在代码运行的过程中,若要使用到调用的服务接口,只需要将接口类型交给服务提供者,服务提供者会加载出改服务接口的所有实现类的对象,最后给调用方使用。
在这里我们要区分一下SPI 与 API,IOC的区别
API:调用方直接依赖具体类或工厂,耦合更紧(接口和实现类都在实现方的包中);
SPI:调用方只依赖接口,运行时自动发现实现,零改动热插拔。(接口和实现类是不在一起的)
IoC 容器(Spring 等):更强的装配与生命周期管理;Spring 也有自己的“SPI/自动装配”机制(主要是通过 配置文件(spring2.x 时META-INF/spring.factories)+ 反射 + IOC 容器 来实现自动发现和装配。),但它和 JDK
ServiceLoader
是两套体系。
而SPI的核心组成部分主要有四个部分:
服务接口(Service Interface)
定义功能契约,例如:
java.sql.Driver
。
服务实现(Service Provider)
任意类库/JAR 可以提供该接口的实现,例如 MySQL JDBC 驱动。
服务加载器(ServiceLoader)
JDK 提供的工具类
java.util.ServiceLoader
,用来扫描和实例化配置的实现类
配置文件
路径固定:
META-INF/services/
文件名:接口的全限定名,例如
META-INF/services/java.sql.Driver
文件内容:一行一个实现类的全限定名
入门案例
项目结构如下
Service Interface
首先我们创建一个SPI_interface
模块,在其中设置一个接口
package com.zshunbao.spi;
/**
* @program: SPI
* @ClassName User
* @description: SPI 演示接口
* @author: zs宝
* @create: 2025-08-24 09:06
* @Version 1.0
**/
public interface User {
public void sayName();
}
这个就是我们后续的服务接口。
Service Provider
SPI机制中接口与其实现是分离的,解耦的。因此我们在这里再创建两个专门针对于次接口的实现模块
首先是SPI_impl1
服务接口的实现类为
package com.zshunbao.spi;
/**
* @program: SPI
* @ClassName Zhangsan
* @description:
* @author: zs宝
* @create: 2025-08-24 09:08
* @Version 1.0
**/
public class Zhangsan implements User{
@Override
public void sayName() {
System.out.println("我是张三");
}
}
需要注意的是Java SPI通过 META-INF/services
下的文件来声明实现类,JDK 提供 ServiceLoader
去加载。
因此我们在resources
目录下会创建 META-INF/services
目录,并创建文件com.zshunbao.spi.User
(这里注意创建的文件名必须为接口的源根路径,不可更改,后续在源码分析中会看到加载时是依靠这个来确定资源的),其中写的就是有关实现类的来自源根的路径
com.zshunbao.spi.Zhangsan
最后无论是哪一个实现模块都需要引入服务接口的依赖
<dependency>
<groupId>com.zshunbao.spi</groupId>
<artifactId>SPI_interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
同理我们创建第二个实现模块SPI_impl2
实现类为
package com.zshunbao.spi;
/**
* @program: SPI
* @ClassName Zhangsan
* @description:
* @author: zs宝
* @create: 2025-08-24 09:08
* @Version 1.0
**/
public class LIsi implements User{
@Override
public void sayName() {
System.out.println("我是李四");
}
}
`resources/META-INF/services
下创建com.zshunbao.spi.User
文件
com.zshunbao.spi.LIsi
测试验证
现在我们就可以基于Java SPI动态加载到接口的实现类并执行了,我们写一个简单的测试模块做验证。我们新建一个测试模块,把他当作服务调用方
pom文件中引入接口和实现模块
<dependencies>
<dependency>
<groupId>com.zshunbao.spi</groupId>
<artifactId>SPI_interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.zshunbao.spi</groupId>
<artifactId>SPI_impl1</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.zshunbao.spi</groupId>
<artifactId>SPI_impl2</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
测试类
package com.zshunbao.spi;
import java.util.Iterator;
import java.util.ServiceLoader;
/**
* @program: SPI
* @ClassName SPITest
* @description:
* @author: zs宝
* @create: 2025-08-24 09:16
* @Version 1.0
**/
public class SPITest {
public static void main(String[] args) {
ServiceLoader<User> userLoader = ServiceLoader.load(User.class);
Iterator<User> userIterator = userLoader.iterator();
while (userIterator.hasNext()){
User user = userIterator.next();
user.sayName();
}
}
}
执行上述代码,ServiceLoader
会加载到META-INF.services
目录下的配置文件,找到对应接口全名文件,读取文件中的类名,在通过反射将实现类实例化。既然已经有了子类的实例化对象,那么就可以通过父类引用指向子类对象,从而调用到子类的对应方法。
运行结果如下
源码分析
接下来,我们就来打上断点,看一看Java SPI的源码到底是一个怎样的执行机制
debug运行
ServiceLoader.load
我们进入这段代码
来到了ServiceLoader.java
类下的
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
在这里Thread.currentThread().getContextClassLoader();
加载了当前环境的上下文信息,接着我们进入ServiceLoader.load(service, cl);
中去看看到底干了些什么,进入ServiceLoader#load
函数
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
我们返现这里其实是在调用ServiceLoader
的一个构造函数,返回了一个新的ServiceLoader
对象,我们进入这个构造函数
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();
}
我们发现,这个构造函数里面函数,我们再点击进入的时候,它并不会直接进入这个构造函数之中,而是先给了我们一个ServiceLoader
的属性变量providers
,这个变量在初次加载是是空的,而且源码的注解表示这是专门存储服务实例(在这里是存储User接口实现类对象)的集合
,接下来我们debug再点击下一步,正式进入private ServiceLoader(Class<S> svc, ClassLoader cl)
构造函数当中
在其中判断svc,cl是否为空,然后将非空的赋值给service,loader。其中acc是专门用在服务实现类的安全权限访问方面的,我们这里没有涉及到acc,所以暂时不要考虑这个东西。
接着我们进入其中的reload
方法
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
在这个里面,我们发现它将专门存储服务实例(在这里是存储User接口实现类对象)的集合providers
清空,并实例化ServiceLoader.java
类的lookupIterator
属性,这里我们粘贴一下ServiceLoader
中的属性
public final class ServiceLoader<S>
implements Iterable<S>
{
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
接着我们来看一下new LazyIterator(service, loader);
,LazyIterator
是ServiceLoader
的一个内部类
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
....................
LazyIterator实现了Iterator迭代器接口,根据类名可以看出,这是一个Lazy懒加载形式的迭代器。延迟加载,说明项目启动时不会立马加载,而是需要被用到的时候,才会动态去加载。实现了Iterator迭代器接口的LazyIterator对象,就具备延迟加载的功能.
所以现在来看reload
方法也就是
清空providers
将ServiceLoader的属性lookupIterator初始化为一个LazyIterator
当reload
函数执行完后,ServiceLoader.load(User.class)
也就过完了,接下来的就是一只往会返回结果ServiceLoader
最后来总结下ServiceLoader.load(User.class)
的作用吧
根据目标接口类初始化创建一个ServiceLoader实例对象
这个实例对象将拥有当前上下文语境所有信息loader,并且清空了属性字段providers,同时实例化了其属性字段的lookupIterator为一个懒加载器。(这里先写一下:这个懒加载器后续在使用时,会遍历我们的指定目录
META-INF.services
下的所有文件,将文件中的类实例化,并将其按类名,示例的形式存储在providers中)
userLoader.iterator()
接下来我们就进入Iterator<User> userIterator = userLoader.iterator();
看看这一句会干些什么
debug进入后发现他直接给我们创建了一个匿名的Iterator对象,同时为我们提供了一个knownProviders,这个knownProviders是由providers提供而来。以及提供hasNext,和函数
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();
}
};
}
然后就将这个匿名的Iterator对象返回给我们拿到。这里其实看完后面的分析你会发现重点不在于匿名类叫什么,而在于匿名类有hasNext和next方法可以让我们调用到ServiceLoader的懒加载类变量lookupIterator中去,但是又尽量避免我们能够直接操纵到它。
接下来我们就去看入门案例中while循环中的逻辑是怎样运行的
userIterator.hasNext()
进入循环
debug进入
函数来到了我们之前创建的匿名Iterator对象对象中的hasNext函数
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
由于我们的providers是空的,所以knownProviders.hasNext()也是空的,因此逻辑进入ServiceLoader类的属性lookupIterator的lookupIterator.hasNext()中,即ServiceLoader类的内部私有类LazyIterator的hasNext函数中去
由于我们没有使用过安全相关的东西,因此acc为null,我们接着进入hasNextService()函数的逻辑
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);
} 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;
}
我们这里会先进入configs == null
的逻辑中去,在其中我们发现我们会在这个里面将扫描META-INF/services/com.zshunbao.spi.User扫描出来,并加载到对应的文件资源(注意fullName的拼接方式,它是用的PREFIX + service.getName(),其中PREFIX是写死的META-INF/services,而service是我们定义的接口的class,它在getName时用的是接口的源根路径)
获得文件资源后,进入下面的while循环
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private boolean hasNextService() {
.............................
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
........................
然后会来到
pending = parse(service, configs.nextElement());
之中
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函数用来将文件资源中的内容依次读取(一行一行),然后返回服务实现类的源根路径集合,如下图
然后在集合收集完文件中的资源后,pending中也就不再为null了,所以它会按照逻辑将迭代器的下一个值赋值给懒加载器也就是lookupIterator的nextName属性字段,这就为后续取值做准备,然后返回true
所以最终userIterator.hasNext()就会为true
所以接下来就会进入User user = userIterator.next();
的逻辑
userIterator.next()
我们debug进入其中来看看具体的执行逻辑
进入后我们先进入了我们在最外层拿到的匿名迭代器的next函数
由于knownProviders依然为空,因此,我们进入lookupIterator.next()中去
debug进入后,我们进入了LazyIterator的next函数
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);
}
}
由于acc为空,我们进入nextService函数
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 subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
在这里面主要做了几个事情
将nextName的值(实现类的源根路径)赋值给了cn
然后通过cn加反射将具体的实现类拿到:
c = Class.forName(cn, false, loader);
最后利用service创建一个子类的实例化对象(反射)c.newInstance(),并将实例化对象以(子类源根路径,对象)的方式存储在providers这个LinkedHashMap链表中。
这个链表的作用就是的在我们第一次调用相关的ServiceLoader.load并通过匿名迭代器遍历后,在后续的重复创建一个匿名迭代器去后去获取接口的服务对象时,可以直接从LinkedHashMap链表缓存里读取即可,无需再次去解析接口对应的配置文件,起到了查询优化的作用。
因为
返回创建的实例化对象,也就是我们拿到了子类的对象
总结
最后其实整个代码的流程无外乎在
通过 URL 工具类从 jar 包的
/META-INF/services
目录下面找到对应的文件,
读取这个文件目录,在目录中找到对应的 spi 接口的源根路径命名的文件,
通过
InputStream
流将文件资源里面,一行一行读取子类的源根路径名称
根据获取到的全类名,通过反射的机制构造对应的实例对象
将构造出来的实例对象添加到
Providers
的列表中,让后续如果重复建造ServiceLoader#iterator()进行使用
SPI机制在不同框架中的应用
JDBC:加载数据库驱动,不需要手动
Class.forName
;
日志框架:
java.util.logging
、SLF4J 的适配;
JDK 内置工具:如
java.util.ServiceLoader
本身就是为扩展机制设计的;
Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口
Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
SpringBoot 中使用(历史上
META-INF/spring.factories
,新版使用基于 Import 的自动配置清单),这与 JDK SPI 不同。