一、AutoService 介绍
1、AutoService 简介
AutoService 是谷歌提供的一个组件,使用简单但是功能却是非常强大。AutoService 会自动在 META-INF 文件夹下生成 Processor 配置信息文件,该文件包含实现该服务接口的具体实现类。当外部程序加载这个模块时,可以通过该 jar 包 META-INF/services/ 里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
AutoService 其实是基于 SPI 机制,这样我们可以直接跨模块查找到想要的接口实现类,避免不必要的模块间依赖,降低模块之间的耦合性。
优点:
- 简化 Java SPI 使用:不需要手动创建和维护 META-INF/services 目录下的配置文件,只需要在对应的实现类上添加 @AutoService 注解即可。
- 提高代码可读性:通过注解明确标记出哪些类是服务的提供者,使得代码更易于理解。
- 提高开发效率:自动化的过程减少了人为错误,提高了开发效率。
缺点:
- 依赖注解处理:AutoService 会在构建时创建需要的依赖配置,所以会增加项目编译时间。
- 局限性:AutoService 只能用于生成 Java SPI 的配置文件,对于其他类型的文件生成没有支持。
2、SPI 简介
SPI 全称是(Service Provider Interface),简单理解就是服务提供接口者。将接口的实现类的全限定名配置在文件中(文件名是接口的全限定名),由服务加载器读取配置文件,并加载实现类,实现了运行时动态的将接口替换对应的实现类。
二、AutoService 使用
大家看完上述对 AutoService 和 SPI 描述之后,是不是处在云里雾里的状态,嘴里嘀咕楼主讲的啥玩意。我第一次接触 AutoService 也是比较懵逼的状态,看文档根本没办法理解。所以,实践才是唯一的真理,接下来我会举例说明,并对其原理进行剖析。请大家跟着我的思路往下走,如有不对或者疑惑的地方,欢迎交流!
1、业务需求
现在有这种需求,library 需要读取 app 的配置参数来完成具体业务。我们知道 library 是无法直接调用 app 的类和方法,所以,并不能通过常规方式来实现。这时轮到 AutoService 出场了,我们可以借助它的能力来实现这种业务场景。
2、功能实现
1)app 模块 build.gradle
在项目中 app 模块的 build.gradle 进行集成。
implementation 'com.google.auto.service:auto-service-annotations:1.0.1'
annotationProcessor 'com.google.auto.service:auto-service:1.0.1'
2)创建 library
在 library 模块创建 IBuildConfig 类,定义 config 方法。
public interface IBuildConfig {
String config();
}
3)创建 BuildConfigImp
app 模块创建接口实现类 BuildConfigImp,需要注意的是实现类要添加 @AutoService 注解,不然是无法动态生成代码的。
@AutoService(IBuildConfig.class)
public class BuildConfigImpl implements IBuildConfig {
@Override
public String config() {
return "debug mode";
}
}
4)library 功能调用
library 调用 ServiceLoader.load 方法,拿到 app 的参数,实现了业务功能。
public class InitManager {
public void init() {
ServiceLoader<IBuildConfig> load = ServiceLoader.load(IBuildConfig.class);
for (IBuildConfig config : load) {
String cf = config.config();
Log.d("InitManager", "config ===> " + cf);
}
}
}
上面的简单示例向大家展示了 AutoService 是如何使用,其实在实际业务场景中 AutoService 有着更广泛的应用。包括插件化开发、模块化开发、跨模块通信,AutoService 都可以胜任。作为一种组件化实现方案,它主要用于实现服务接口的自动发现和加载,降低模块间的耦合性,提高应用的灵活性和可扩展性。
3、项目中遇到的问题
在实际项目使用中,当我调用 ServiceLoader.load 拿到 ServiceLoader 去迭代遍历时,发现返回的数据是空的,也就是 ServiceLoader 里面的数据并没有对应接口实现类,ServiceLoader 迭代器是空的。这下犯了嘀咕,我是按照 demo 的实现方式去应用到实际项目中,为啥项目中却出现了问题。
首先我们看一下 ServiceLoader 提供的两个重载的方法,如下所示:
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);
}
第一个方法需要指定 ClassLoader,第二个方法不需要指定(默认使用ClassLoader.getSystemClassLoader())。
正常使用采用第二个方法就行,可偏偏在实际项目中却遇到了问题。所以为了解决该问题,请大家跟着我来解析一下 AutoService 实现原理。
三、AutoService 实现原理
1、原理分析
当我们调用如下代码,返回一个ServiceLoader对象,让我们看一下load方法做了什么。
ServiceLoader<IBuildConfig> load = ServiceLoader.load(IBuildConfig.class);
我们看一下 Thread.currentThread().getContextClassLoader() 方法,这个方法是获取当前线程类加载器。拿到当前类加载器,我们能做什么呢?我们都知道 Java 加载类时,通过双亲委派机制。当我们需要加载自定义类时,可以通过这种方式来打破 Java 类加载双亲委派机制。
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
我们接着往下看 load 方法,创建并返回了一个 ServiceLoader 对象。
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;
// Android-changed: Do not use legacy security code.
// On Android, System.getSecurityManager() is always null.
// acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
这里创建 LazyIterator 类,这个类实现了 Iterator,我们继续往下看。
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
当我们使用迭代器 Iterator 去遍历时,会调用 hasNext 方法。在 Android 源码中,hasNext 的实现其实是通过 hasNextService。
public boolean hasNext() {
// Android-changed: do not use legacy security code
/* if (acc == null) { */
return hasNextService();
/*
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
*/
}
我们看一下 hasNextService 方法,关键地方做了注释。
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//固定路径(META-INF/services/)+接口全限定名
String fullName = PREFIX + service.getName();
if (loader == null)
//如果当前类加载为null,则使用父类加载器
configs = ClassLoader.getSystemResources(fullName);
else
//使用当前类加载器,返回路径URL
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;
}
我们看一下 parse 方法,主要是在指定 URL 中读取对应接口实现类全限定名称,为后续实例化做准备。
//将给定URL的内容解析为提供程序配置文件
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();
}
我们看一下 parseLine 方法,一行一行读取接口实现类的全限定名称。
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
String ln = r.readLine();
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) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
我们接着看 nextService 方法,主要是通过类加载方式,实例化接口对应的实现类。
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//创建接口实现类class对象,通过类加载方式
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
// Android-changed: Let the ServiceConfigurationError have a cause.
"Provider " + cn + " not found", x);
// "Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
// Android-changed: Let the ServiceConfigurationError have a cause.
ClassCastException cce = new ClassCastException(
service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
fail(service,
"Provider " + cn + " not a subtype", cce);
// 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
}
通过上述源码,我们知道 AutoService 实现原理,主要就是去加载 META-INF/services/ 路径下的接口全限定名称的文件。然后通过 IO 读取文件,找到实现类的类路径。然后通过类加载方式将其实例化,供使用者调用。
2、解决问题
其实看完源码之后,问题的原因已经找到了。在 hasNextService() 方法中,如下所示:
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
当我们不指定 ClassLoader 时,会使用系统默认的 ClassLoader。我们知道最终生成的文件是放置在 META-INF/services/ 目录下,我们可以看一下打包的 apk,如下图所示:
回过头来分析项目中的问题,当我们使用 AutoService 组件生成的配置文件,是属于应用内部文件。如果不指定对应的 ClassLoader ,就会使用系统默认的,当然是无法找到的。所以,要解决项目中的问题,我们需要指定当前类的 ClassLoader,通过 getClass().getClassLoader() 方式。