本文参考资源:
什么是SPI
SPI全称为Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过SPI机制为我们的程序提供拓展功能。

应用场景:
- 数据库驱动加载接口实现类的加载
JDBC加载不同类型数据库的驱动 - 日志门面接口实现类加载
SLF4J加载不同提供商的日志实现类 - Spring
Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等 - Dubbo
Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口
Java中的SPI
用法
要使用Java SPI,需要遵循如下约定:
- 当服务提供者提供了接口的一种具体实现后,在jar包的
META-INF/services
目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名 - 接口实现类所在的jar包放在主程序的classpath中
- 主程序通过
java.util.ServiceLoader
动态装载实现模块,它通过扫描META-INF/services
目录下的配置文件找到实现类的全限定名,把类加载到JVM - SPI的实现类必须携带一个不带参数的构造方法
示例
创建接口Markable
和实现类RedMark
,BlackMark
,结构如下图所示:

public interface Markable {
void mark();
}
public class RedMark implements Markable {
public void mark() {
System.out.println("红色标记");
}
}
public class BlackMark implements Markable {
public void mark() {
System.out.println("黑色标记");
}
}
在resources下面建立文件夹/META-INF/services/
,添加文件com.rhett.service.Markable
,内容为
com.rhett.service.impl.BlackMark
com.rhett.service.impl.RedMark
进行测试:
public class SpiTest {
public static void main(String[] args) {
ServiceLoader<Markable> serviceLoader = ServiceLoader.load(Markable.class);
Iterator<Markable> iterator = serviceLoader.iterator();
while (iterator.hasNext()){
Markable markable = iterator.next();
markable.mark();
}
}
}
源码分析
load流程:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();//获得当前类加载器
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
ServiceLoader类:
public final class ServiceLoader<S>
implements Iterable<S>
{
private static final String PREFIX = "META-INF/services/";
// 代表传入的接口
private final Class<S> service;
// 用于定位、加载和初始化实现类
private final ClassLoader loader;
// 访问权限上下文
private final AccessControlContext acc;
// 缓存提供器
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 当前的懒查询的迭代器
private LazyIterator lookupIterator;
//...
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//传入的接口不为空
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//如果类加载器为null默认使用应用程序类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
//acc:AccessControlContext,访问权限相关
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
//跟进
reload();
}
public void reload() {
//清空缓存表
providers.clear();
//实例化一个LazyIterator
lookupIterator = new LazyIterator(service, loader);
}
//将接口、loader赋值给迭代器中的成员变量
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
LazyIterator类:
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
//...
}
获取iterator方法:
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() {
//如果缓存表中有,从缓存表(hashmap)中取
if (knownProviders.hasNext())
return knownProviders.next().getValue();
//否则使用懒查询迭代器的next方法
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
lookupIterator的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);
}
}
private S nextService() {
//是否有下一个服务,如果有这里就获取了nextName
if (!hasNextService())
throw new NoSuchElementException();
//cn得到了下一个要加载的实现类全限定名
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//使用Class.forName即通过反射使用类加载器加载类,赋值给了类中的c变量
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 {
//将C转换为指定类型的对象
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
}
获取下一个要加载的实现类全限定名的方法:
private boolean hasNextService() {
//如果nextName不为空
if (nextName != null) {
return true;
}
//configs为空,即还没有读取过资源
if (configs == null) {
try {
// PREFIX就是/META-INF/resources,拼接接口的全限定名
String fullName = PREFIX + service.getName();
//加载文件资源,会返回一个Enumeration<URL>对象
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
//给pending赋值,pending是一个Iterator<String>,这里就是实现类全限定名的迭代器
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
// 将nextName设置为迭代器中的下一个类全限定名
nextName = pending.next();
return true;
}
原生SPI的优缺点
优点:
使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
并且因为它会加载jar包中资源的特点,可以动态扩充接口的实现类,并使得源框架不必关心接口的实现类的路径。
缺点:
获取机制不够灵活: 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过Iterator获取,而在迭代器中,每次调用next方法都会对得到的实现类全限定名进行加载,也就是接口的实现类会全部加载并实例化一遍。如果你并不想用某些实现类,它也会被加载并实例化了,这就造成了浪费。
多个并发多线程使用ServiceLoader类的实例是不安全的。
原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/java%e4%b8%adspi%e6%9c%ba%e5%88%b6%e4%bb%8b%e7%bb%8d%e5%92%8c%e6%ba%90%e7%a0%81%e5%88%86%e6%9e%90/