AutoService 解析
一、概述
SPI 全称为Service Provider Interface
,是JDK内置的一种服务提供发现机制。简单来说,它是一种动态替换发现机制。例如,在设计组件化路由框架的时候就会使用到 SPI 设计思想。
场景:
假设有 A,B 两个业务组件,由于业务组件不存在相互依赖的问题,因此 A 组件就无法调用 B 组件的服务(API) 。但是我们可以使用 SPI 的思想实现服务(SPI) 的发现。这样就可以在 A 组件中调用 B 组件提供的功能了。
版本: Android 29
关联文章:
二、原理
系统提供了一个 java.util.ServiceLoader
类用于在指定报名路径下的发现服务,那这个服务又是从哪里获取的呢?这里需要结合 Google 提供的 AutoService
库来一起分析一下。
AutoService 的依赖:
annotationProcessor 'com.google.auto.service:auto-service-annotations:1.0-rc7'
implementation 'com.google.auto.service:auto-service:1.0-rc7'
原理的实现步骤:
- 定义一个接口类 IFly。
- 创建一个实现类 FlyImpl 实现借口 IFly,并用
@AutoService
注解修饰。- 在编译期会通过 AutoServiceProcessor 对被
@AutoService
注解修饰过的类进行处理,将该实现类(FlyImpl)的全路径类名信息写入一个文件中(该文件名为 IFly 全路径类名信息)。如果该接口有多个实现类,那么这些子类都会被写入统一个文件的不同行中。- 步骤3生成的文件被存储在
META-INF/services
文件夹中。- 在调用的时候,会使用
java.util.ServiceLoader
这个类的load(Class<S> service)
方法进行接口实现类的加载。
小结:
java.util.ServiceLoader
是用于加载指定路径下的文件。@AutoService
及其注解解析器是为了在指定的加载路径下生成相应的被加载文件。
三、使用流程
接下来我们按照上面的几个步骤进行分析。
1. 步骤1和步骤2 (定义接口 IFly、ISpeak,然后实现子类)
类的继承关系如下图所示:
IFly
定义一个接口 IFly
,并实现两个子类 FlyImpl1 、FlyImpl2。
public interface IFly {
String fly();
}
@AutoService(IFly.class)
public class FlyImpl1 implements IFly {
@Override
public String fly() {
return "FlyImpl1 fly";
}
}
@AutoService({IFly.class, ISpeak.class})
public class FlyImpl2 implements IFly, ISpeak {
@Override
public String fly() {
return "FlyImpl2 fly";
}
@Override
public String speak() {
return "FlyImpl2 speak";
}
}
ISpeak
定义一个接口 ISpeak
,并实现两个子类 ISpeakImpl1 、FlyImpl2 (实现了两个接口)。
public interface ISpeak {
String speak();
}
@AutoService(ISpeak.class)
public class SpeakImpl1 implements ISpeak {
@Override
public String speak() {
return "ISpeakImpl1 speak";
}
}
@AutoService({IFly.class, ISpeak.class})
public class FlyImpl2 implements IFly, ISpeak {
@Override
public String fly() {
return "FlyImpl2 fly";
}
@Override
public String speak() {
return "FlyImpl2 speak";
}
}
2. 步骤3和步骤4
在 Android 打包编译后,我们来看下 Apk 的包结构。
- 生成了以
ISpeak、IFly
接口的全路径类名信息的两个文件。
- 每个文件里面存储的是当前文件名对应接口类的所有子类。
3. 步骤5 (加载流程)
// 1.根绝传入的接口名称,构建一个ServiceLoader。
ServiceLoader<IFly> load = ServiceLoader.load(IFly.class);
// 2.获取该接口的所有子类。
load.forEach(fly -> {
String fly1 = fly.fly();
System.out.println(fly1);
});
小结:
- 根据传入的接口名称,构建一个ServiceLoader。
- 获取该接口的所有子类。
四、ServiceLoader 源码解析
分析了整个加载流程的步骤,那下面我们就来具体分析一下 ServiceLoader.load()
是如何加载的。
ServiceLoader.load()
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;
reload();
}
小结:
- 加载的时候,调用
ServiceLoader.load()
方法,传入指定接口Class。- 传入的指定接口会赋值给 ServiceLoader 的成员变量 service 。
- 构建完成之后会返回一个
ServiceLoader
对象 (每个 ServiceLoader 对象都对应一个要加载的接口类) 。
下面看一下 reload
方法。
ServiceLoader.reload()
// providers 的key存储的是实现类的全路径信息,value是接口的实现类。
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
public void reload() {
providers.clear();
// 一个用于子类对象的迭代器。
lookupIterator = new LazyIterator(service, loader);
}
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
小结:
执行完
ServiceLoader.load()
之后会返回一个 ServiceLoader 对象。由于 ServiceLoader 实现了 Iterable 接口,所以接下来我们会
调用ServiceLoader.iterator()
来获取该接口有多少实现类。
ServiceLoader.iterator()
public Iterator<S> iterator() {
return new Iterator<S>() {
// 将 providers 的数据赋值给 knownProviders,相当于之前已经加载过的。
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();
}
};
}
小结:
几个参数的含义:
knownProviders
存储之前加载过的实现类,避免再次解析文件。providers
存储所有的实现类,包括即将要从文件中查找的子类。lookupIterator
是 LazyIterator类型的,用来执行从文件中查找接口子类的迭代器。
我们知道,迭代器进行迭代操作时,会先执行 Iterator.hasNext()
方法判断是否有下一个数据,如果存在下一个数据,才会执行 Iterator.next()
方法。
接下来我们先来看一下 LazyIterator.hasNext()
方法。
LazyIterator.hasNext() 调用链
// LazyIterator.class
public boolean hasNext() {
return hasNextService();
}
private static final String PREFIX = "META-INF/services/"
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 1.service是我们传入进来要查找的接口,这里就是在指定的"META-INF/services/"文件夹下查找指定文件名的文件。
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;
}
// 2.解析文件,从里面读取所有子类的全路径名称,返回一个集合(迭代器)。
pending = parse(service, configs.nextElement());
}
// 3.一次进行子类名称的获取,赋值给nextName,后面会通过反射构造 nextName 对象。
nextName = pending.next();
return true;
}
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;
// 解析指定文件的每一行数据(每一行都是一个子类),如果没有数据了就返回-1。
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
// ...省略代码...
}
// names 包含的指定接口的所有子类名称。
return names.iterator();
}
// 对指定接口类的文件进行解析,获取里面的子类信息(每个子类都占用一行)。
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names)
throws IOException, ServiceConfigurationError{
// 1.读取文件的每一行。
String ln = r.readLine();
// 2.读取不到就返回-1。
if (ln == null) {
return -1;
}
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
// ...校验内容的合法性...
// 3.判断是否加载过,没加载过就添加到集合中。
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
// 4.lc自增,进行下一行的读取操作。
return lc + 1;
}
小结:
- 调用
LazyIterator.hasNext()
会去"META-INF/services/"
文件夹下查找指定 service 文件名的文件。- 解析该文件的每一行信息 (每一行都存储了一个子类信息),并存储在 ArrayList 中。
- 从 ArrayList 中获取子类信息,并通过反射构造指定接口的子类对象。
- 将构造的对象保存到 providers 集合中,提升查找性能。
调用完 LazyIterator.hasNext()
方法后会执行 LazyIterator.next()
方法。
LazyIterator.next()
public S next() {
return nextService();
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
// 1.nextName 会在 LazyIterator.hasNext() 方法中进行赋值,nextName 就是子类的全路径名称。
String cn = nextName;
nextName = null;
// 2. 通过反射,构造一个给定 nextName 名称的对象实例。
Class<?> c = Class.forName(cn, false, loader);
// ...省略...
try {
// 3.进行类型转换,转化成为我们指定的接口类型。
S p = service.cast(c.newInstance());
// 4.将已经加载起来的实例对象保存到内存当中,避免下次访问需要重新从文件进行解析加载。
providers.put(cn, p);
return p;
} catch (Throwable x) {
// ...省略...
}
throw new Error(); // This cannot happen
}
小结:
- 获取要构造的类的信息nextName (nextName 会在 LazyIterator.hasNext() 方法中进行赋值,nextName 就是子类的全路径名称)。
- 通过反射,构造一个给定 nextName 名称的对象实例。
- 进行类型转换,转化成为我们指定的接口类型。
- 将已经加载起来的实例对象保存到内存当中,避免下次访问需要重新从文件进行解析加载。
五、ServiceLoader 的缺点
通过对 ServiceLoader 的解析,我们也看出了 ServiceLoader 的一些缺陷。
- 会批量实例化指定的接口的所有子类,造成了内存的浪费。
- 获取单个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
- 实例不是单例。