Java SPI 的实现原理并不复杂,它的实现基于 Java 类加载机制和反射机制。
当使用 ServiceLoader.load(Class<T> service)
方法加载服务时,会检查 META-INF/services
目录下是否存在以接口全限定名命名的文件。如果存在,则读取文件内容,获取实现该接口的类的全限定名,并通过 Class.forName()
方法加载对应的类。
以下是我的项目中基于SPI加载接口实例的代码:
/**
* SPI(Service Provider Interface)机制加载 {@link IRowMetaData}所有实例
* @return 表名和 {@link RowMetaData}实例的映射对象
*/
private static HashMap<String, RowMetaData> loadRowMetaData() {
ServiceLoader<IRowMetaData> providers = ServiceLoader.load(IRowMetaData.class);
Iterator<IRowMetaData> itor = providers.iterator();
IRowMetaData instance;
HashMap<String, RowMetaData> m = new HashMap<>();
while(itor.hasNext()){
try {
instance = itor.next();
} catch (ServiceConfigurationError e) {
// 实例初始化失败输出错误日志后继续循环
SimpleLog.log(e.getMessage());
continue;
}
if(instance instanceof RowMetaData){
RowMetaData rowMetaData = (RowMetaData)instance;
m.put(rowMetaData.tablename, rowMetaData);
}
}
return m;
}
这个代码写了几年了,在Windows,Linux,Android平台都没有任何问题。但最近在客户的设备上(Android)出了问题。
我们提供的DEMO程序在客户的设备上也能正常执行,但放在客户的项目代码中,就不行。
通过跟踪发现itor.hasNext()
返回false
,就导致没有执行循环,应该是没有找到META-INF/services
目录下接口IRowMetaData的全限定名命名的文件。
进一步跟踪,找到itor.hasNext()
【java.util.ServiceLoader.LayIterator.hasNext()】最终调用的实现代码
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;
}
可以看到最终是通过调用ClassLoader.getResources(String)
来读取META-INF/services
目录下接口类的全限定名命名的文件,根据ClassLoader.getResources(String)
的方法说明,如果没有找到对应的文件,则返回空的对象。
所以itor.hasNext()
返回false
的直接原因应该就是没找到META-INF/services
目录下接口类的全限定名命名的文件。但是为什么会这样,没搞明白。
进一步看ServiceLoader的代码,才知道,ServiceLoader.load
其实有两个重载方法,如下:
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
常用的ServiceLoader.load(Class<T> service)
方法的实现其实是使用当前线程上下文的ClassLoader实例为参数调用了ServiceLoader.load(Class<T> service,ClassLoader loader)
方法
于是我尝试改为调用ServiceLoader.load(Class<T> service,ClassLoader loader)
方法,以接口类的ClassLoader实例作为loader参数,如下:
ServiceLoader<IRowMetaData> providers = ServiceLoader.load(IRowMetaData.class,IRowMetaData.class.getClassLoader());
居然可以正常执行了。
为什么会这样?太奇怪了,我无法解释,最有可能是的原因是客户的项目中的使用某种应用框架有自己的ClassLoader作为当前线程上下文的ClassLoader实例,.这个ClassLoader有BUG,找不到META-INF/services
目录下的资源文件。
不管怎样,为了安全起见,兼容两种加载方式,我将获取 实例迭代器(Iterator)的逻辑修改为如下:
即优先调用ServiceLoader.load(Class<T> service)
如果返回空则调用ServiceLoader.load(Class<T> service,ClassLoader loader)
方法
/**
* SPI(Service Provider Interface)机制加载 {@link IRowMetaData}所有实例
* @return 表名和 {@link RowMetaData}实例的映射对象
*/
private static HashMap<String, RowMetaData> loadRowMetaData() {
// 优先调用ServiceLoader.load(Class<T> service)
// 如果返回空则调用ServiceLoader.load(Class<T> service,ClassLoader loader)方法
Iterator<IRowMetaData> itor = ServiceLoader.load(IRowMetaData.class).iterator();
if(!itor.hasNext()){
itor = ServiceLoader.load(IRowMetaData.class,IRowMetaData.class.getClassLoader()).iterator();
}
IRowMetaData instance;
HashMap<String, RowMetaData> m = new HashMap<>();
while(itor.hasNext()){
try {
instance = itor.next();
} catch (ServiceConfigurationError e) {
// 实例初始化失败输出错误日志后继续循环
SimpleLog.log(e.getMessage());
continue;
}
if(instance instanceof RowMetaData){
RowMetaData rowMetaData = (RowMetaData)instance;
m.put(rowMetaData.tablename, rowMetaData);
}
}
return m;
}
这个问题解决了,但感觉是莫名奇妙,我查了很多关于第三方库及JDK的源码,关于SPI的使用方式都是调用ServiceLoader.load(Class<T> service)
,很少有特别强制要调用ServiceLoader.load(Class<T> service,ClassLoader loader)
方法,大家都是这么用的,到我这里却出了毛病,很难向客户证明这不是我们的问题。
好在可以通过修改我们代码,增加容错性逻辑解决了问题。