SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。实现机制通过final类ServiceLoader实现,该类实现了Iterable接口。
druid采用spi初始化filter的代码非常的精简,方法名为initFromSPIServiceLoader,方法代码如下所示:只要实现了druid的filter接口,不关实现类在哪个jar包,只要稍微配置一下就能实现动态加载,是不是非常的神奇?
private void initFromSPIServiceLoader() {
String property = System.getProperty("druid.load.spifilter.skip");
if (property != null) {
return;
}
ServiceLoader<Filter> druidAutoFilterLoader = ServiceLoader.load(Filter.class);
for (Filter autoFilter : druidAutoFilterLoader) {
AutoLoad autoLoad = autoFilter.getClass().getAnnotation(AutoLoad.class);
if (autoLoad != null && autoLoad.value()) {
if (LOG.isInfoEnabled()) {
LOG.info("load filter from spi :" + autoFilter.getClass().getName());
}
addFilter(autoFilter);
}
}
}
方法的代码也很容易就能看懂,通过ServiceLoader.load(Filter.class),创建了ServiceLoader对象druidAutoFilterLoader,然后foreach该对象就能拿到所有filter对象。所以,要搞清楚SPI的实现原理,就要弄明白ServiceLoader的工作流。
ServiceLoader.load(Filter.class)最终调用了如下构造方法创建了ServiceLoader对象。此处分别对成员service,loader及acc进行了赋值,并调用了reload()方法。
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();
}
reload方法的定义如下:方法中创建了LazyIterator的对象lookupIterator。
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
LazyIterator是ServiceLoader的内部类,构造lookupIterator的代码如下,只是完成了成员service和loader的赋值。
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
至此,load过程已经结束了,我们并没有看到filter实现类的创建过程。通过LazyIterator名称中包含的Lazy,我们可以猜测,filter对象的创建是一种延迟创建,并不在load过程中创建。
继续结合initFromSPIServiceLoader()方法,创建filter对象的时机只能是foreach遍历的时候了。通过查看ServiceLoader类的iterator方法的实现过程,发现hasNext()和next()方法,分别调用了LazyIterator的hasNext()和next()方法。所以说,实现的关键又回到了LazyIterator类。而LazyIterator的hasNext()和next()方法,分别调用了自己的hasNextService()和nextService()方法。
最后我们将LazyIterator类的nextService()方法的实现代码摘出来,如下所示:
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
}
终于找到了我们期望的Class.forName()方法。从而验证了,filter对象的创建时机确实延迟到foreach遍历druidAutoFilterLoader对象的时候。
既然整个流程我们已经了解清楚了,前面也提到过通过SPI只要简单的配置就能实现service的动态加载,那么到底如何配置呢?
我们发现ServiceLoader类中有一个关键成员PREFIX,成员值如下:
private static final String PREFIX = "META-INF/services/";
当我们看到META-INF的时候,是否已经猜到了配置文件应该存放的路径了?
我们继续查找,发现PREFIX被使用的方法是hasNextService()方法,详细代码如下:
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;
}
通过此方法我们能看出,配置文件需要存放在META-INF/services/目录下,文件必须以接口的名称(包含包路径)命名。而文件中保存的就是各式各样的实现了该接口的类名(包含包路径),至此,我们理清楚了SPI的实现原理。
mysql的驱动程序已经完成了SPI的文件配置,我们不妨用它的配置格式来验证一下我们的结论,配置截图如下所示:





