概述
- 什么是spi
SPI (Service Provider Interface)属于动态加载接口实现类
的的一项技术,是JDK内置的一种服务提供发现机制,使用ServiceLoader去加载接口对应的实现,这样我们就不用关注实现类,ServiceLoader会告诉我们。官方文档描述为:为某个接口寻找服务的机制,类似IOC思想,将装配的控制权交给ServiceLoader。
- 解决问题
只提供服务接口,具体服务由其他组件实现,接口和具体实现分离(类似桥接),同时能够通过系统的ServiceLoader
拿到这些实现类的集合,统一处理,这样在组件化中往往会带来很多便利,SPI机制可以实现不同模块之间方便的面向接口编程,拒绝了硬编码的方式,解耦效果很好
即相当于制定标准,然后不同实现方用不同的方式实现标准供使用方使用,并且可以动态加载
在Android中如何使用
上面说的可能比较抽象,下面将结合例子说明下在Android中的运用。
这种机制在使用起来也比较简单,使用步骤如下:
定义接口和接口的实现类
创建resources/META-INF/services目录
在上述Service目录下,创建一个以接口名(类的全名) 命名的文件, 其内容是实现类的类名 (类的全名)。
在services目录下创建的文件是com.binglumeng.spidemo.IService 文件中的内容为Animal接口的实现类, 可能是com.binglumeng.spidemo.AService
- 在java代码中使用ServcieLoader来动态加载并调用内部方法.
主工程和组件之间一些“服务”的配置
定义接口
package com.example; public interface IDisplay { String display(); }
在主工程和bdisplay 模块中的实现该接口
创建spi描述文件
在工程的main目录下新建目录resources/META-INF/services,以服务接口名为文件名新建spi描述文件,内容为具体的服务实现类权限定名,可以有多个
文件结构如下
加载不同服务
通过ServiceLoader来加载接口的不同实现类,然后会得到迭代器,在迭代器中可以拿到不同实现类全限定名,然后通过反射动态加载实例就可以调用display方法了。
ServiceLoader<Display> loader = ServiceLoader.load(IDisplay.class); mIterator =loader.iterator(); while(mIterator.hasNext()){ mIterator.next().display(); }
源码分析
感觉有点很神奇
就可以拿到Display.class
接口的所有实现类了, amazing!(感觉这里跟Retrift使用有点类似)下面来分析一下这个背后到底隐藏了什么
ServiceLoader.java
先看下几个重要的成员变量
- PREFIX就是配置文件所在的包目录路径;
- service就是接口名称,在我们这个例子中就是Display;
- loader就是类加载器,其实最终都是通过反射加载实例;
- providers就是不同实现类的缓存,key就是实现类的全限定名,value就是实现类的实例
- lookupIterator就是内部类LazyIterator的实例。
private static final String PREFIX = "META-INF/services/"; // The class or interface representing the service being loaded private Class<S> service; // The class loader used to locate, load, and instantiate providers private ClassLoader loader; // Cached providers, in instantiation order private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // The current lazy-lookup iterator private LazyIterator lookupIterator;
之前spi加载的三个关键步骤
- mIterator =loader.iterator();
- while(mIterator.hasNext()){
mIterator.next().display();
}
获取实现接口集合
ServiceLoader提供了两个静态的load方法,如果我们没有传入类加载器,ServiceLoader会自动为我们获得一个当前线程的类加载器,最终都是调用构造函数。
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){ return new ServiceLoader<>(service, loader); }
构造函数中有一个重要的函数reload
public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); } private LazyIterator(Class<S> service, ClassLoader loader) { this.service = service; this.loader = loader; }
所以看到当我们load class之后并没有得到什么实现类,那么在何时加载的呢?
懒加载
那么service provider在什么地方进行加载?我们接着看第二个步骤loader.iterator(),
- 首先会到providers中去查找有没有存在的实例,有就直接返回,没有再到LazyIterator中查找
public Iterator<S> iterator() { return new Iterator<S>() { Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator(); public boolean hasNext() { if (knownProviders.hasNext()) return true; return lookupIterator.hasNext(); } public S next() { if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); } public void remove() { throw new UnsupportedOperationException(); } }; }
其实就是返回一个迭代器。我们看下官方文档的解释,这个就是懒加载实现的地方,
焦点聚焦在LazyIterator
上
- hasNext()
- 首先拿到配置文件名fullName,我们这个例子中是com.example.Display
public boolean hasNext() { if (nextName != null) { return true; } if (configs == null) { try { //首先拿到配置文件名fullName 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; } //依次扫描每个配置文件的内容,返回配置文件内容Iterator<String> pending pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; }
Tips
关于 ClassLoader.getSystemResources(fullName)可以查阅
- next()
在上面hasNext()方法中拿到的nextName就是实现类的全限定名,接下来我们去看看具体实例化工作的地方next():
- 1.首先根据nextName,Class.forName加载拿到具体实现类的class对象
- 2.Class.newInstance()实例化拿到具体实现类的实例对象
- 3.将实例对象转换service.cast为接口
- 4.将实例对象放到缓存中,providers.put(cn, p),key就是实现类的全限定名,value是实例对象。
- 5.返回实例对象
public S next() { if (!hasNext()) { throw new NoSuchElementException(); } String cn = nextName; nextName = null; Class<?> c = null; try { //首先根据nextName,Class.forName加载拿到具体实现类的class对象 c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found", x); } if (!service.isAssignableFrom(c)) { ClassCastException cce = new ClassCastException( service.getCanonicalName() + " is not assignable from " + c.getCanonicalName()); fail(service, "Provider " + cn + " not a subtype", cce); } try { //将实例对象转换service.cast为接口 S p = service.cast(c.newInstance()); //将实例对象放到缓存中,providers.put(cn, p),key就是实现类的全限定名,value是实例对象 providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated: " + x, x); } throw new Error(); // This cannot happen }
总结
Spi的优缺点
- 优点
只提供服务接口,具体服务由其他组件实现,接口和具体实现分离,同时能够通过系统的ServiceLoader拿到这些实现类的集合,统一处理。
- 缺点
- Java中SPI是随jar发布的,每个不同的jar都可以包含一系列的SPI配置,而Android平台上,应用在构建的时候最终会将所有的jar合并,这样很容易造成相同的SPI冲突,常见的问题是DuplicatedZipEntryException异常
- 读取SPI配置信息是在运行时从jar包中读取,由于apk是签过名的,在从jar中读取的时候,签名校验的耗时问题会造成性能损失
后续可以改进的点
Java中使用ServiceLoader去读取SPI配置信息是在程序运行时,我们可以将这个读取配置信息提前,在编译时候就搞定,通过gradle插件,去扫描class文件,找到具体的服务类(可以通过标注来确定),然后生成新的java文件,这个文件中包含了具体的实现类。这样程序在运行时,就已经知道了所有的具体服务类,缺点就是编译时间会加长,自己需要重新写一套读取SPI信息、生成java文件等逻辑。
经过优化后,SPI已经偏离了原本的初衷,但是可以做更多的事,可以将业务服务分离,通过SPI找到业务服务入口,业务组件化,抽成单独的aar,独立成工程。