SPI简介及源码简析
一,背景介绍
数据库连接驱动JDBC大家都知道,我们在初学JAVA的时候,加载数据库驱动的方式一定是:Class.forName(“com.mysql.jdbc.Driver”)。但是后来慢慢不这么写了,甚至于在后来的项目中,都已经不关注这它了,只要把mysql或者oracle的驱动放在maven文件的依赖中,就可以正常的加载到数据的连接驱动。项目可以正常运行了,随着紧凑的项目进度安排,可能后续因为时间紧任务重,也就不会再去关注它了。
但是我们是什么时候开始,不再去写Class.forName这样的代码了呢?不写的话,数据库驱动又是怎么被加载的呢?
这个分界点,准确来说,是JDK6的发布。
原来,在JDK6的时候,新增了一种特性,叫SPI机制。随着JDK6一起发布的,还有JDBC4.0。
同时,JDBC4.0也随着SPI机制的引入,增加了一个 *auto-loading of JDBC driver class
*的特性。数据库驱动就是通过这个特性实现自动加载的。是它们,解放了我们的双手,不用再写那些重复的代码。
在JDBC4.0的API特性介绍中有这么一句话:
意思是:支持驱动类的自动加载。接来重点是:JAR包里的META-INF/services文件夹,名为java.sql.Driver的文件,如果这个文件包含驱动类的名字,那JDBC驱动就支持自动加载。
疑问三连:
1,那这个自动加载JDBC驱动类的特性,又是怎么实现自动加载的呢?它和SPI机制有什么内在的联系吗?
2,这种自动加载的方式是否抛弃了Class.forName(),采用了新的方式呢?
3,具体又是怎么做的呢?
回答这些问题之前,我们先来了解一下SPI。
二,SPI概念
SPI是Service Provider Interface 的简称,字面翻译是服务提供者接口,但其实是一个服务发现机制。SPI是一种扩展机制,在对应的配置文件中定义好某个接口的实现类,然后再根据这个接口去这个配置文件中加载这个实例类并实例化,实现和接口不在一起。接口在调用方,而实现在其他地方,通过SPI机制,保证在程序运行时,动态为接口替换实现类。SPI机制可以为某个接口寻找服务实现。
有一个与它很像的名字,叫API,应用程序接口。但是二者并非同级同类,不可并论。SPI更像组织API的一种方式。如果接口和实现在同一个包,这是我们通常理解下的API,我们引用的非JDK包含的一些第三方工具类,如okhttp3,也都是这么做的。
那如果接口和实现分别属于独立的包,调用的时候,只需要知道接口就好,这样的接口和实现,本质上还是属于API。而这种情况下,把它们连接到一起,在程序运行时期二者就像普通的API一样对外提供服务的这个中间人,就是SPI,可以把SPI理解是连接接口规范和实现的桥梁。
而接口规范和实现分离的情况,在JDK中尤为常见。JDK定义了很多的接口规范,例如JDBC,servlet,javax.validation,日志等等。而我们日常使用的时候,只需要知道JDBC等这些接口就好了,而不用去关心它们是由谁实现的,在哪儿。而这些接口和实现的组合本质上还是属于API。
三,原理解析
概念都比较抽象,来点实际的吧。
先说回答问题:
1,那这个自动加载JDBC驱动类的特性,是通过JDK6发布的SPI机制实现了自动加载。
2,这种自动加载的方式的本质依旧是通过Class.forName()反射获取驱动实现类信息,进而实例化驱动实现类。
3,数据库驱动实现类厂商,只需要在驱动类的包中新建:META-INF/services包,在包下新建文件,命名为:java.sql.Driver,这样项目在引入对应的驱动的maven依赖的时候,就可以通过自动加载的方式加载到驱动了。
接下来,我们来验证一下,并且去弄清楚原理。
1,验证
DriverManager中源码如下:
//一下代码摘自java.sql.DriverManager#ensureDriversInitialized
public Void run() {
//通过ServiceLoader加载
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while (driversIterator.hasNext()) {
driversIterator.next();
}
手动模拟:
1 ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);
2 Iterator<Driver> iterator = load.iterator();
3 while (iterator.hasNext()){
4 Driver next = iterator.next();
5 System.out.println("已加载的驱动:"+next.getClass());
6 System.out.println();
7 }
运行结果:
已加载的驱动:class com.mysql.jdbc.Driver
已加载的驱动:class com.mysql.fabric.jdbc.FabricMySQLDriver
可以看到我们目前的应用应用在启动的时候,加载了两个Mysql的驱动。其余的驱动还有Oracle,H2等。
我们打开在mysql驱动的jar发现,符合自动加载的要求,反证成立。
2,分析
ServiceLoader的功能比较齐全,比如通过模块服务加载,通过类路径下的配置文件加载,通过制定类加载器的模块服务加载等诸多方式。本文只分析通过类路径下配置文件加载的方式,即Driver.class的实现类加载的过程,并且Driver.class的实现类仅限于com.mysql.jdbc.Driver,和com.mysql.fabric.jdbc.FabricMySQLDriver这两种。其他的驱动不做分析,原理是一样的。
主要流程如下:
注:与类路径懒加载的迭代器(LazyClassPathLookupIterator)同级的还有模块服务加载迭代器(ModuleServicesLookupIterator)迭代器,详情请见:https://zhuanlan.zhihu.com/p/30860041
本文只分析类路径懒加载的迭代器。
根据流程,截取的ServiceLoader部分关键代码:
//1,传入Driver.class
//2,获取当前线程的类加载器
//3,构造ServiceLoader实例
@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
//4,构造Iterator实例
//5,遍历Iterator
//11,并将最后实例化之后的实现类缓存起来
public Iterator<S> iterator() {
lookupIterator1 = newLookupIterator();
return new Iterator<S>() {
@Override
public boolean hasNext() {
return lookupIterator1.hasNext();
}
@Override
public S next() {
next = lookupIterator1.next().get();
instantiatedProviders.add(next);
return next;
}
};
}
//6,构造类路径懒加载Iterator
private Iterator<Provider<S>> newLookupIterator() {
Iterator<Provider<S>> second = new LazyClassPathLookupIterator<>();
return new Iterator<Provider<S>>() {
@Override
public boolean hasNext() {
return (second.hasNext());
}
@Override
public Provider<S> next() {
(second.hasNext()) {
return second.next();
} else{
throw new NoSuchElementException();
}
}
};
}
//7,遍历类路径懒加载Iterator
private final class LazyClassPathLookupIterator<T>
implements Iterator<Provider<T>> {
@Override
public boolean hasNext() {
return hasNextService();
}
@Override
public Provider<T> next() {
return nextService();
}
//10,获取实现类信息之后,再获取实现类的构造器
private boolean hasNextService() {
Class<?> clazz = nextProviderClass();
Class<? extends S> type = (Class<? extends S>) clazz;
Constructor<? extends S> ctor
= (Constructor<? extends S>) getConstructor(clazz);
ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc);
nextProvider = (ProviderImpl<T>) p;
}
//8,拼接全路径 fullName=META-INF/services/java.sql.Driver
//9,加载配置文件
//10,通过Class.forName()反射获取实现类信息
private Class<?> nextProviderClass() {
static final String PREFIX = "META-INF/services/"
String fullName = PREFIX + service.getName();
configs = loader.getResources(fullName);
pending = parse(configs.nextElement());
return Class.forName(cn, false, loader);
}
private Provider<T> nextService() {
if (!hasNextService())
throw new NoSuchElementException();
Provider<T> provider = nextProvider;
return provider;
}
}
private static class ProviderImpl<S> implements Provider<S> {
//11,实例化实现类
@Override
public S get() {
return newInstance();
}
}
//11,实例化实现类
private S newInstance() {
S p = null;
p = ctor.newInstance();
}
}
备注:
在ServiceLoader的属性中,有如下3个:
// The lazy-lookup iterator for iterator operations
private Iterator<Provider<S>> lookupIterator1;
private final List<S> instantiatedProviders = new ArrayList<>();
// The lazy-lookup iterator for stream operations
private Iterator<Provider<S>> lookupIterator2;
1,实例化之后,将实例放在 List<S> instantiatedProviders中缓存,这个改动是在JDK9时加进去的。
2,属性中有两个Iterator<Provider<S>>,lookupIterator1是我们以上分析iterator()用到的。lookupIterator2是stream()方法用到的。stream()方法的好处在于,可以不用实例化接口实现,对接口的实现进行一些类型检查等操作(通过java.util.ServiceLoader.Provider#type方法)。
四,“逆向”的双亲委派机制
细心的读者可能会有疑问,Driver.classs是位于rt包下,而我们上文中反复提到了类路径,这两个地方的类被加载时使用的类加载器明显不是同一个。也就是说,应用启动时,加载Drivder.class,使用的是PlatformClassLoader(以前叫ExtClassLoader ,JDK9之后改了),此时要加载类路径classpath中的诸如Mysql驱动,肯定是加载不到的。而加载类路径下的类,必须使用应用程序加载器,也就是AppClassLoader。
而通过“逆向”的双亲委派机制可以解决。
将应用程序类加载器设置进线程里面,即线程里面新定义一个类加载器的属性ContextClassLoader
,然后将线程的ContextClassLoader
这个属性设置为应用程序类加载器。然后启动类加载器去加载java.sql.Driver
和java.sql.DriverManager
等类时,同时也会从当前线程中取出这个ContextClassLoader
即应用程序类加载器去classpath
中加载外部厂商提供的JDBC驱动类。这个ContextClassLoader
叫线程上下文类加载器
@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {
//从当前线程获取应用程序加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
//以下代码摘自java.util.ServiceLoader.LazyClassPathLookupIterator#nextProviderClass
try {
//此时loader=jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
String fullName = PREFIX + service.getName();
if (loader == null) {
configs = ClassLoader.getSystemResources(fullName);
} else if (loader == ClassLoaders.platformClassLoader()) {
//略
} else {
//最终会走到这里,即,使用拼接之后的全路径来加载类路径中的配置信息
//fullName=META-INF/services/java.sql.Driver
configs = loader.getResources(fullName);
}
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
五,JAVA的SPI与Dubbo的SPI区别
Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,二者的区别: