SPI
即 Service Provider Interface
,该方案是为某个接口动态寻找服务的机制,类似IOC的思想。
SPI的使用
先通过一个简单的例子来对SPI机制有一个初步的认识
定义接口
在Android Studio中新建一个module,新增一个接口Machine
, 接口定义如下:
public interface Machine {
void powerOn();
}
实现类
新增两个实现类,分别是TV
和Computer
, 如下:
public class TV implements Machine {
@Override
public void powerOn() {
System.out.println("TV power on");
}
}
public class Computer implements Machine {
@Override
public void powerOn() {
System.out.println("Computer power on");
}
}
定义类关系
- 在
main
目录下定义一个resources.META-INF.services
目录, 在该目录下添加一个名为com.fred.spi.Machine
文件,需要注意该文件名必须是和上面的Machine
对应上。 - 在
com.fred.spi.Machine
文件中添加两行com.fred.spi.impl.Computer com.fred.spi.impl.TV
测试
- 我们定义一个
MachineFactory
类,用一个工厂来管理。
public class MachineFactory {
private static MachineFactory mInstance;
private Iterator<Machine> mIterator;
private MachineFactory() {
ServiceLoader<Machine> loader = ServiceLoader.load(Machine.class);
mIterator = loader.iterator();
}
static MachineFactory getInstance() {
if (null == mInstance) {
synchronized (MachineFactory.class) {
if (null == mInstance) {
mInstance = new MachineFactory();
}
}
}
return mInstance;
}
Machine getMachine() {
return mIterator.next();
}
boolean hasNextMachine() {
return mIterator.hasNext();
}
}
- 测试入口文件
public static void main(String[] args) {
MachineFactory factory = MachineFactory.getInstance();
while (factory.hasNextMachine()) {
factory.getMachine().powerOn();
}
}
执行上面的代码会输出
Computer power on
TV power on
从java代码的层面上看,我们并没有任何地方new Computer, TV; 更没有执行其powerOn()
方法,只是在一个配置文件里面加了Computer
, TV
对应的类名。
SPI机制原理
在MachineFactory
中我们可以看到,加载Machine
接口的实现类只依赖于一行代码:
ServiceLoader<Machine> loader = ServiceLoader.load(Machine.class);
接着只需要对ServiceLoader
进行遍历,就可以找到所有有实现类。在上面的例子中,因为我们在com.fred.spi.Machine
文件中配了两个,所以能找到两个实现类。
ServiceLoader
的源码
我们来从ServiceLoader
的源码角度看看是如何完成类加载的。由于ServiceLoader
是在rt.jar
包中, 我们在安装jdk的时候是可以下载一个src.zip
文件,可以将该文件导入到IDE中去关联源码,但rt.jar
并不在src.zip
中,从这里可以拿到相关的代码。
其核心代码如下:
public final class ServiceLoader<S>
implements Iterable<S>
{
private static final String PREFIX = "META-INF/services/";
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);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
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;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
}
}
}
可以看到,其思路就是:
- 先获得一个classloader
- 然后去加载
META-INF/services/
下面的文件,获取相关的配置,如代码:
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
- 获取对应的实现类名, 即
parse
方法 - 利用反射,根据类名去创建对应的实例, 即
nextService
方法
Android中的应用
SPI机制能够较好的解藕,便于代码的扩展,比如有这么个场景,我们需要从多个数据源获取数据,每一个数据源相关的操作都作为一个子module集成到app中,这个时候,我们可以定义一个META-INF/services/xxx
文件,来配置数据源。
如果开发者自己写过类似于ARouter
这种路由框架,肯定会了解com.google.auto.service:auto-service
, 该组件便是简化了SPI的使用,让开发者不需要去手动维护META-INF/services/xxx
。
在我们自己写一个路由框架时,会需要自己实现一个AbstractProcessor
, 用来生成路由相关的配置。于是会定义一个类似的文件:
@AutoService(Processor.class)
// 允许/支持的注解类型,让注解处理器处理
@SupportedAnnotationTypes({Constants.ROUTER_ANNOTATION_TYPES})
// 指定JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// 注解处理器接收的参数
@SupportedOptions({Constants.MODULE_NAME, Constants.APT_PACKAGE})
public class RouterProcessor extends AbstractProcessor {
private Elements elementsUtils;
....
}
手写注入
如果我们不采用自动注入的方式,我们需要自己去维护一个META-INF/services/xxx
文件(上面代码中的第一行就没有必要加了),如下;
我们需要将对应Processor配置到这个文件里面,而Google的com.google.auto.service:auto-service
组件便是简化了SPI的使用,让开发者不需要去手动维护META-INF/services/xxx
。只需要加一个注解,由框架在编译时自动生成这个配置文件
自动注入
再回到上面的第一行代码@AutoService(Processor.class)
, 这个注解是auto-service
这个库提供的。在编译阶段,会执行AutoServiceProcessor
的process
方法,在该方法中会先调用generateConfigFiles
生成配置文件,如下:
private void generateConfigFiles() {
Filer filer = processingEnv.getFiler();
for (String providerInterface : providers.keySet()) {
String resourceFile = "META-INF/services/" + providerInterface;
log("Working on resource file: " + resourceFile);
try {
SortedSet<String> allServices = Sets.newTreeSet();
try {
FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
resourceFile);
log("Looking for existing resource file at " + existingFile.toUri());
Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
log("Existing service entries: " + oldServices);
allServices.addAll(oldServices);
} catch (IOException e) {
log("Resource file did not already exist.");
}
Set<String> newServices = new HashSet<String>(providers.get(providerInterface));
if (allServices.containsAll(newServices)) {
log("No new service entries being added.");
return;
}
allServices.addAll(newServices);
log("New service file contents: " + allServices);
FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
resourceFile);
OutputStream out = fileObject.openOutputStream();
ServicesFiles.writeServiceFile(allServices, out);
out.close();
log("Wrote to: " + fileObject.toUri());
} catch (IOException e) {
fatalError("Unable to create " + resourceFile + ", " + e);
return;
}
}
}
最终生成了配置文件中的类便指向了我们自定义的Processor。
当这个模块在使用的时候,便可以通过该配置找到具体的实现类,并完成实例化。