java:解决SPI机制遇到的非典型问题-ServiceLoad.load(Class<T> service)方法失效

发布时间:2023年12月18日

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)方法,大家都是这么用的,到我这里却出了毛病,很难向客户证明这不是我们的问题。
好在可以通过修改我们代码,增加容错性逻辑解决了问题。

文章来源:https://blog.csdn.net/10km/article/details/135010464
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。