SPI 是 JAVA 提供的一种服务提供发现接口,听起来有点别扭哈!其实就是一种面向接口的编程,为接口去匹配具体服务实现的机制,这一点上与 IOC 的思想类似,都是把装配的控制权放到了程序之外,下面具体看看什么是 SPI
什么是 SPI
SPI 全称为 Service Provider Interface,即服务提供发现接口,这里的服务指的不是我们经常听到的微服务服务发现,这里的一个服务 Service 指的是一个接口或抽象类,服务提供方则是对这个接口或抽象类的实现
SPI 是 ”基于接口的编程 + 策略模式 + 配置文件“ 组合实现的动态加载机制
为什么使用 SPI?
模块化设计中,模块之间基于接口编程,把装配的控制权放到程序之外,实现系统的解耦
使用场景
适用于调用方根据实际需求启用、扩展、替换服务的策略实现
许多开源框架中都使用了 Java 的 SPI 机制,如 JDBC 的 SPI 加载模式、日志框架 SLF4J 加载不同提供商的日志实现、Spring 中也大量适用了 SPI、Dubbo 的扩张机制、ServiceComb Java Chassis (CSE) 的 Filter、异常处理等扩展机制
SPI 的实现
SPI 的实现步骤
- 在类路径下的
META-INF/services
目录下,创建以服务接口的”全限定名“命名的文件,文件的内容为接口实现类的全限定名 - 实现类必须在当前程序的 classpath 下
- 使用
java.util.ServiceLoader
动态加载实现,会扫描 META-INF/services 下的配置文件加载实现类
一个简单的 demo
定义一个接口 SimpleSpiService.java
public interface SimpleSpiService {
void say();
}
两个简单的实现类
public class SimpleSpiServiceA implements SimpleSpiService {
@Override
public void say() {
System.out.println("Hi! A");
}
}
public class SimpleSpiServiceB implements SimpleSpiService {
@Override
public void say() {
System.out.println("Hi! B");
}
}
在 META_INF/services
目录下创建文件 com.snail.service.SimpleSpiService
,里面内容为实现类的全限定名
com.snail.service.prodiver.SimpleSpiServiceA
com.snail.service.prodiver.SimpleSpiServiceB
服务调用
public class ServiceInvoker {
public static void main(String[] args) {
ServiceLoader<SimpleSpiService> services = ServiceLoader.load(SimpleSpiService.class);
for (SimpleSpiService service : services) {
service.say();
}
}
}
运行结果
Hi! A
Hi! B
SPI 实现原理
首先我们可以先来看下 ServiceLoader 的部分成员变量,即内部结构
public final class ServiceLoader<S> implements Iterable<S>{
private static final String PREFIX = "META-INF/services/";
// 被加载的类或接口
private final Class<S> service;
// 用于定位、加载和实例化 providers 的类加载器
private final ClassLoader loader;
// 创建 ServiceLoader 时获取的访问控制上下文
private final AccessControlContext acc;
// 缓存 providers, 按照实例化的顺序
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 当前懒加载迭代器
private LazyIterator lookupIterator;
......
}
服务 providers 查找的迭代器
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;
}
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;
}
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
}
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);
}
}
}
实现的流程如下
- 调用方调用 ServiceLoader.load(),会先实例化一个 ServiceLoader 对象,并实例化多个成员变量
- loader ClassLoader - 用于定位、加载和实例化 providers 的类加载器
- service Class - 被加载的类或接口
- acc AccessControlContext - 创建 ServiceLoader 时获取的访问控制上下文
- providers LinkedHashMap - providers.clear() 清空prividers
- lookupIterator LazyIterator - 实例化懒加载迭代器
- 通过迭代器接口获取对象示例,ServiceLoader 会先去 providers 中去判断是否有缓存该实例对象,有就直接返回,没有执行类的加载
- 读取 META-INF/services/ 下配置文件,获取所有实现类全限定名
- 通过反射 Class.forName() 获取类并实例化对象
- 把实例对象添加到 providers (LinkedHashMap ) 缓存中
- 返回实例对象
使用 Java SPI 最大的优势就是实现模块之间的解耦,第三方的服务模块的装配加载和调用者方的代码分离,这样可以很方便的做到模块服务的扩展和替换
但是 SPI 还是有一定的缺点,虽然 ServiceLoader 是使用的延迟加载,但是每次获取都是通过遍历加载,接口的所有实现类都会被实例化,不能按需加载,如果没有用到其中的某些实现,就会造成资源的浪费