1.spi是什么
spi的全称是Service provider interface,即服务提供商接口。
在平时的开发中,我们经常接触到概念是api(Application Programming Interface),比如 jdk中hashMap、ArrayList等是java提供给我们的api,我们可以通过读取相关的文档,来学习这些api的使用。业务开发的过程中,后端一般也会提供api给前端,用于数据交互,比较常用的方式就是提供http接口。
那么spi与api有什么关系呢?让我们看一下维基百科上的一段说明:
Service provider interface (SPI) is an API intended to be implemented or extended by a third party. It can be used to enable framework extension and replaceable components.
译文:
SPI是旨在由第三方实现或扩展的 API。 它可用于启用框架扩展和可替换组件。
我们可以理解为spi也是一种api,与上述的两种api的使用场景,最大的不同就是实现交给了第三方去做,对比关系如下图所示:
spi就是一种为某个接口寻找服务实现的机制。这有点类似IOC的思想,将装配的控制权移到了程序之外。
2.ServiceLoader的使用
java中的spi主要是通过ServiceLoader来使用的,使用步骤如下:
-
定义接口
-
实现接口
-
实现接口的模块中,resources目录下创建文件:META-INF/services/接口类全名
-
通过ServiceLoader.load(接口.class)来获取所有的接口实现类对象
下面通过一个例子来看一下java中原生spi的使用方式(spiDemo (github.com))。
首先定义接口IotDevice,来模拟一个iot设备
public interface IotDevice {
/**
* 获取iot设备名称
*
* @return 设备名
*/
String name();
/**
* 开机
*/
void on();
/**
* 关机
*/
void off();
/**
* 设备工作中
*/
void work();
}
然后在其他模块中,创建几个IotDevice的实现类,模拟具体的iot设备
public class XiaomiLight implements IotDevice {
@Override
public String name() {
return "小米米家LED智能台灯1S";
}
@Override
public void on() {
System.out.println("小爱开灯");
}
@Override
public void off() {
System.out.println("小爱关灯");
}
@Override
public void work() {
System.out.println("亮了。。。");
}
}
在实现类所在模块的resources目录下,创建META-INF/services/top.learningwang.iot.IotDevice文件,内容填写实现类的全名
top.learningwang.iot.device.light.XiaomiLight
然后我们就可以通过ServiceLoader来获取所有IotDevice实现类的对象
public class Run {
public static void main(String[] args) {
ServiceLoader<IotDevice> s = ServiceLoader.load(IotDevice.class);
for (IotDevice iotDevice : s) {
System.out.println("-------------------");
System.out.println(iotDevice.name());
iotDevice.on();
iotDevice.work();
iotDevice.off();
System.out.println("-------------------");
System.out.print("\n");
}
}
}
运行后,可以看到扫描到了所有的IotDevice实现类,并可以通过对象调用接口定义的方法
3.ServiceLoader源码解析
通过上面的案例,可以看出ServiceLoader是java原生spi实现的核心类,那么它的实现原理是什么呢?为什么必须定义配置文件到META-INF/services/文件夹下面呢,带着这些问题,首先来看一下入口方法 ServiceLoader.load(IotDevice.class)
// 入口,默认从当前线程中获取类加载器,作为接口实现类的加载器
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
// 初始化ServiceLoader对象
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
ServiceLoader.load做的只有两件事:
- 指定类加载器
- 初始化ServiceLoader对象 ServiceLoader的构造方法:
// 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;
// 校验class、classLoader,初始化迭代器
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();
}
// 清空类缓存,重新初始化迭代器
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
构造方法中,初始化了类加载器、对象缓存map、迭代器信息。
以上就是ServiceLoader.load方法执行的逻辑,构造迭代器之后,就可以通过循环的方式,来执行迭代逻辑,扫描实现类并创建实现类对象的逻辑就放到了迭代器的实现中。
LazyIterator是ServiceLoader类中的一个私有内部类,构造方法及属性如下
// 接口类
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;
}
迭代器比较核心的方法就是 hasNext、next方法
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);
}
}
具体的实现逻辑,封装到了hasNextService、nextService方法中,先来看一下hasNextService方法
// 待扫描文件夹前缀
private static final String PREFIX = "META-INF/services/";
// 扫描 META-INF/services/class
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
// 扫描jar包,初始化url迭代器
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;
}
hasNextService方法中扫描resources路径下所有的META-INF/services/类全名文件,并读取文件内容,每次调用遍历一行,并将该行的内容存到了nextName属性中。
下面再来看一下nextService方法做了那些工作
// 3. 通过反射创建对象,并放入缓存中
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
// 3.1 nextName在hasNextService已初始化
String cn = nextName;
nextName = null;
// 3.2 反射创建对象,放入缓存中
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();
}
}
nextService方法中,使用hasNextService中已赋值的变量nextName,来通过反射的方式来创建对象,并放入到了缓存中。
以上就是jdk中ServiceLoader的具体实现,主要实现思路是通过迭代器去循环读取jar包下指定文件的类全名,并通过反射来创建对象。
4.java源码中spi的应用
java源码中,也有很多用到了spi机制,例如jdbc,首先,回顾一下原生jdbc获取连接的代码写法,以mysql为例
String url = "jdbc:mysql://localhost:3306/db1";
String user = "user1";
String password = "123456";
// 1.加载驱动程序(jdk1.6之后,不需要再去显式加载JDBC驱动程序)
Class.forName("com.mysql.jdbc.Driver");
// 2. 获得数据库连接
Connection conn = DriverManager.getConnection(url, user, password);
jdk1.6之后,使用ServiceLoader改写了JDBC驱动程序的加载方式,不需要再通过 Class.forName() 去显式加载JDBC驱动程序
public class DriverManager {
static {
// 静态加载驱动器
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
// 加载所有lib下的Driver实现
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
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;
}
});
}
}
mysql-connector包结构如下
java.sql.Driver文件中的内容中声明了驱动类 com.mysql.cj.jdbc.Driver,DriverManager会通过spi机制自动加载所有的驱动类。
jdbc通过spi机制实现的好处:
-
易于扩展,各厂商按照接口规范实现Driver即可
-
扩展一种新型数据库支持,不需要去升级jdk版本
5.总结
spi机制将接口的定义与具体的业务实现分离,而不是和业务端全部耦合再一起,可以运行时根据业务实际场景启用或者替换具体组件,符合高内聚、低耦合的设计原则,能够很好的满足基础框架对外提供扩展能力的需要。
附: 参考资料
springboot-starter中的SPI 机制 - 掘金 (juejin.cn)
某厂面试:如何优雅使用 SPI 机制 |周末学习 - 掘金 (juejin.cn)