SPI机制代码分析
在之前的文章里提到Spring Boot的类加载中有一种是SPI机制。但为了不冲淡主题,没有写SPI机制的详细代码分析。于是打算重新写一篇文章,把SPI机制的源码分析一遍。于我而言,是思考的总结,也是防止遗忘的备忘录。
SPI(Service Provider Interface)是 Java 提供的一种服务发现机制,它允许程序在运行时动态地为某个接口找到具体的实现类,而无需在代码中硬编码实现类名。下面我们追随代码,看看它的工作原理。 如下代码是LoggerFactory.java 类通过ServiceLoader加载具体日志处理类的入口
java
LoggerFactory.java
private static ServiceLoader<SLF4JServiceProvider> getServiceLoader(final ClassLoader classLoaderOfLoggerFactory) {
ServiceLoader<SLF4JServiceProvider> serviceLoader;
SecurityManager securityManager = System.getSecurityManager();
if(securityManager == null) {
serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class, classLoaderOfLoggerFactory);
} else {
final PrivilegedAction<ServiceLoader<SLF4JServiceProvider>> action = () -> ServiceLoader.load(SLF4JServiceProvider.class, classLoaderOfLoggerFactory);
serviceLoader = AccessController.doPrivileged(action);
}
return serviceLoader;
}
第6行,ServiceLoader.load函数实际是ServiceLoader对象的初始化函数,代码如下
java
ServiceLoader.java
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(Reflection.getCallerClass(), service, loader);
}
得到ServiceLoader对象serviceLoader后,再获得serviceLoader的iterator对象(下文代码第18行)。通过iterator,循环实例化provider对象(下文代码第20行)
java
LoggerFactory.java
static List<SLF4JServiceProvider> findServiceProviders() {
List<SLF4JServiceProvider> providerList = new ArrayList<>();
// retain behaviour similar to that of 1.7 series and earlier. More specifically, use the class loader that
// loaded the present class to search for services
final ClassLoader classLoaderOfLoggerFactory = LoggerFactory.class.getClassLoader();
SLF4JServiceProvider explicitProvider = loadExplicitlySpecified(classLoaderOfLoggerFactory);
if(explicitProvider != null) {
providerList.add(explicitProvider);
return providerList;
}
ServiceLoader<SLF4JServiceProvider> serviceLoader = getServiceLoader(classLoaderOfLoggerFactory);
Iterator<SLF4JServiceProvider> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
safelyInstantiate(providerList, iterator);
}
return providerList;
}
下面来看看ServiceLoader的iterator函数
java
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;
}
};
}
在第5行,新建了一个newLookupIterator对象,赋值给lookupIterator1。它包含了两个Iterator,如下文代码第6行第7行所示
java
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();
}
}
};
}
}
所以,调用iterator.hasNext()时,会调用lookupIterator1.hasNext(),最后调用代码return (first.hasNext() || second.hasNext());
first这个iterator,会在module的各个layer中去查找provider。如果查找不到,就使用second这个iterator,在各个classLoader的classpath中查找provider。由于Spring Boot只是兼容模块化,但是它本身并不适用模块化,故在first中是查找不到provider的。之后second.hasNext()就会被调用。second对象的类是LazyClassPathLookupIterator,它的hasNext()方法如下
java
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
第3行,就是SPI查找及初始化查找出的provider的具体逻辑
java
private boolean hasNextService() {
while (nextProvider == null && nextError == null) {
try {
//查找provider类
Class<?> clazz = nextProviderClass();
if (clazz == null)
return false;
if (clazz.getModule().isNamed()) {
// ignore class if in named module
continue;
}
if (service.isAssignableFrom(clazz)) {
// 初始化provider对象
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;
} else {
fail(service, clazz.getName() + " not a subtype");
}
} catch (ServiceConfigurationError e) {
nextError = e;
}
}
return true;
}
第5行是调用nextProviderClass函数查找provider,第15行之后,是初始化provider对象。nextProviderClass函数的代码是:
java
static final String PREFIX = "META-INF/services/";
private Class<?> nextProviderClass() {
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null) {
configs = ClassLoader.getSystemResources(fullName);
} else if (loader == ClassLoaders.platformClassLoader()) {
// The platform classloader doesn't have a class path,
// but the boot loader might.
if (BootLoader.hasClassPath()) {
configs = BootLoader.findResources(fullName);
} else {
configs = Collections.emptyEnumeration();
}
} else {
configs = loader.getResources(fullName);
}
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return null;
}
pending = parse(configs.nextElement());
}
String cn = pending.next();
try {
return Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
return null;
}
}
nextProviderClass函数的逻辑是,首先拼接fullName,因为loader为LaunchedClassLoader,故代码会进入第17行。调用LaunchedClassLoader的getResources函数。LaunchedClassLoader的getResources函数继承自ClassLoader,代码如下
java
public Enumeration<URL> getResources(String name) throws IOException {
Objects.requireNonNull(name);
@SuppressWarnings("unchecked")
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
if (parent != null) {
tmp[0] = parent.getResources(name);
} else {
tmp[0] = BootLoader.findResources(name);
}
tmp[1] = findResources(name);
return new CompoundEnumeration<>(tmp);
}
从第五六行可以看出,会递归查找所有父类和当前类的module和classpath中是否有该文件。以日志系统为例,传入的类是SLF4JServiceProvider,即代码serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class, classLoaderOfLoggerFactory);那么查找的文件fullName就是META-INF/services/org.slf4j.spi.SLF4JServiceProvider,最终查找到的文件为nested:/磁盘路径/项目路径/项目名/target/firstWebProject-0.0.1-SNAPSHOT.jar/!BOOT-INF/lib/logback-classic-1.5.18.jar!/META-INF/services/org.slf4j.spi.SLF4JServiceProvider。再读取org.slf4j.spi.SLF4JServiceProvider文件中的值,根据这个值加载class。最终加载的provider就是ch.qos.logback.classic.spi.LogbackServiceProvider。 最后贴一张图,看看该文件在jar包中的样子。
当我们要更换provider时,只需要调整文件里的内容,就可以了。这样就做到了解耦。