1. SPI是什么?
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。主要是被框架开发人员使用的一种技术。例如,使用 Java 语言访问数据库时我们会使用到 java.sql.Driver 接口,不同数据库产品底层的协议不同,提供的 java.sql.Driver 实现也不同,在开发 java.sql.Driver 接口时,开发人员并不清楚用户最终会使用哪个数据库,在这种情况下就可以使用 Java SPI 机制在实际运行过程中,为 java.sql.Driver 接口寻找具体的实现。
2. JDK SPI的使用
首先定义一个接口在这里插入代码片:
public interface Log {
void log(String message);
}
然后定义多个实现接口:
public class MyLogA implements Log{
@Override
public void log(String message) {
System.out.println("MyLogA - log: " + message);
}
}
public class MyLogB implements Log{
@Override
public void log(String message) {
System.out.println("MyLogB - log: " + message);
}
}
在项目的 resources/META-INF/services 目录下添加一个名为 com.tdt.spi.Log的文件(没有什么后缀名),这是 JDK SPI 需要读取的配置文件,具体内容就是接口实现类的全路径,如下:
com.tdt.spi.MyLogA
com.tdt.spi.MyLogB
最后创建 main() 方法,加载上述配置文件并创建全部 Log 接口实现的实例,并执行其 log() 方法,如下所示:
public static void main(String[] args) {
ServiceLoader<Log> logServiceLoader = ServiceLoader.load(Log.class);
Iterator<Log> iterator = logServiceLoader.iterator();
while (iterator.hasNext()) {
Log log = iterator.next();
log.log(" execute log spi");
}
}
代码结构图如下:
3. 原理分析
我们可以看到 JDK SPI 的入口方法是 ServiceLoader.load() 方法,下面我们进入源码,
public static <S> ServiceLoader<S> load(Class<S> service) {
// 当前线程类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
private ServiceLoader(Class<?> caller, Class<S> svc, ClassLoader cl) {
// 省略
......
this.service = svc;
this.serviceName = svc.getName();
this.layer = null;
this.loader = cl;
this.acc = (System.getSecurityManager() != null)
? AccessController.getContext()
: null;
}
service : 表示正在加载的服务的接口
serviceName : 接口的名称
layer : 类加载器的提供者,初始化时为null
loader : 用于查找,加载和实例化提供程序的类加载器
acc : 创建ServiceLoader时采取的访问控制上下文
由上可以看到ServiceLoader构造函数为私有,只能通过静态方法进行实例化,有单例模式的影子,实例化ServiceLoader只对参数进行校验,设置一些基本属性的值,大部分实现逻辑在迭代器里面。
接着分析ServiceLoader遍历:
public Iterator<S> iterator() {
// create lookup iterator if needed
if (lookupIterator1 == null) {
// 创建迭代器
lookupIterator1 = newLookupIterator();
}
return new Iterator<S>() {
// record reload count
final int expectedReloadCount = ServiceLoader.this.reloadCount;
// index into the cached providers list
int index;
/**
* Throws ConcurrentModificationException if the list of cached
* providers has been cleared by reload.
*/
private void checkReloadCount() {
if (ServiceLoader.this.reloadCount != expectedReloadCount)
throw new ConcurrentModificationException();
}
@Override
public boolean hasNext() {
checkReloadCount();
if (index < instantiatedProviders.size())
return true;
return lookupIterator1.hasNext();
}
@Override
public S next() {
checkReloadCount();
S next;
if (index < instantiatedProviders.size()) {
next = instantiatedProviders.get(index);
} else {
next = lookupIterator1.next().get();
instantiatedProviders.add(next);
}
index++;
return next;
}
};
}
创建真实的迭代器:
private Iterator<Provider<S>> newLookupIterator() {
assert layer == null || loader == null;
if (layer != null) {
return new LayerLookupIterator<>();
} else {
Iterator<Provider<S>> first = new ModuleServicesLookupIterator<>();
Iterator<Provider<S>> second = new LazyClassPathLookupIterator<>();
return new Iterator<Provider<S>>() {
@Override
public boolean hasNext() {
return (first.hasNext() || second.hasNext());
}
@Override
public Provider<S> next() {
if (first.hasNext()) {
return first.next();
} else if (second.hasNext()) {
return second.next();
} else {
throw new NoSuchElementException();
}
}
};
}
}
从上面可以看出来ServiceLoad获取的迭代器是创建的一个匿名迭代器,同时该匿名迭代器再指向一个匿名迭代器,同时该迭代器包含真正执行hasNext和next方法进行遍历.
由LazyClassPathLookupIterator加载META-INF/services/指定文件的内容
迭代器之间的关系大概为:
4. jdk spi在jdbc中的运用
JDK 中只定义了一个 java.sql.Driver 接口,具体的实现是由不同数据库厂商来提供的。以 MySQL 提供的 JDBC 实现包为例进行分析。在 mysql-connector-java-*.jar 包中的 META-INF/services 目录下,有一个 java.sql.Driver 文件中只有一行内容,如下所示:
com.mysql.cj.jdbc.Driver
在使用 mysql-connector-java-*.jar 包连接 MySQL 数据库的时候,我们会用到如下语句创建数据库连接:
javaString url = "jdbc:xxx://xxx:xxx/xxx";
Connection conn = DriverManager.getConnection(url, username, pwd);
DriverManager 是 JDK 提供的数据库驱动管理器,在调用 getConnection() 方法的时候,DriverManager 类会被 Java 虚拟机加载、解析并触发 static 代码块的执行;在 loadInitialDrivers() 方法中通过 JDK SPI 扫描 Classpath 下 java.sql.Driver 接口实现类并实例化,核心实现如下所示:
private static void loadInitialDrivers() {
String drivers = System.getProperty("jdbc.drivers")
// 使用 JDK SPI机制加载所有 java.sql.Driver实现类
ServiceLoader<Driver> loadedDrivers =
ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
driversIterator.next();
}
String[] driversList = drivers.split(":");
for (String aDriver : driversList) { // 初始化Driver实现类
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
}
}
在 MySQL 提供的 com.mysql.cj.jdbc.Driver 实现类中,同样有一段 static 静态代码块,这段代码会创建一个 com.mysql.cj.jdbc.Driver 对象并注册到 DriverManager.registeredDrivers 集合中(CopyOnWriteArrayList 类型),如下所示:
static {
java.sql.DriverManager.registerDriver(new Driver());
}
在 getConnection() 方法中,DriverManager 从该 registeredDrivers 集合中获取对应的 Driver 对象创建 Connection.