概述
- 什么是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();
}
源码分析
感觉有点很神奇
ServiceLoader loader = ServiceLoader.load(Display.class);
就可以拿到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加载的三个关键步骤
- ServiceLoader loader = ServiceLoader.load(IDisplay.class);
- 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
- 通过类加载器获得所有模块的配置文件Enumeration configs configs
- 依次扫描每个配置文件的内容,返回配置文件内容Iterator pending,每个配置文件中可能有多个实现类的全限定名,所以pending也是个迭代器。
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,独立成工程。