市面上的组件化通信框架可谓是眼花缭乱,参差不齐。像阿里的ARouter, 美团的WMRouter, 还有个人开发者提供的CC框架,他们各有优缺点。今天我们介绍一款轻量级的组件化通信框架,谷歌的亲儿子 - AutoService。
使用篇
项目结构
在Android项目架构中,最底层一个Base层,然后是Common,所有的业务模块都依赖Common层,app被称为壳工程,它依赖所有的业务模块。
1.png图很简陋,但整体就是这样一个结构了。我们就以一个简单的demo举例,创建一个kotlin项目:
2.pngcommon为通用模块,app和weblibrary为业务模块。我们要做的就是从app的MainActivity跳转到weblibrary的WebViewActivity。
依赖
首先需要在公共模块进行依赖:
api "com.google.auto.service:auto-service:1.0-rc7"
需要注意的是,如果某个业务模块需要对外开放,也就是需要其他模块进行访问,那么必须添加注解处理器,我们在weblibrary的build.gradle添加以下依赖项:
kapt "com.google.auto.service:auto-service:1.0-rc7"
HowUse
1. 接口下沉
在公共模块定义需要开放的接口,这个接口用于app模块的访问,从而实现跳转,我们叫做接口下沉。
interface AutoServiceInterface {
fun toWebActivity(context: Context, url: String, title: String)
}
2. 接口实现
因为要访问的是 web模块,所以要在web模块进行接口的实现
@AutoService(AutoServiceInterface::class)
class AutoServiceImpl : AutoServiceInterface {
override fun toWebActivity(context: Context, url: String, title: String?) {
context.startActivity(Intent(context, WebViewActivity::class.java).apply {
putExtra("url", url)
putExtra("title", title)
})
}
}
需要注意的是,接口实现类需要使用AutoService
注解,值为接口的类对象。这里通过context跳转到WebViewActivity,并传入url参数和title参数。
来看看WebViewActivity,接收到url参数和title参数,并进行展示和加载
class WebViewActivity : AppCompatActivity() {
private lateinit var mUrl: String
private lateinit var mTitle: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view)
mWebView.settings.javaScriptEnabled = true
mUrl = intent?.getStringExtra("url") ?: ""
mTitle = intent?.getStringExtra("title") ?: ""
if (mTitle.isEmpty()) {
mHeaderBar.visibility = View.GONE
} else {
mHeaderBar.visibility = View.VISIBLE
mHeaderBar.setTitle(mTitle)
}
mWebView.loadUrl(mUrl)
}
}
3. 访问
接下来就可以直接在app模块的MainActivity访问了
mBtnToWeb.setOnClickListener {
val load = ServiceLoader.load(AutoServiceInterface::class.java)?.toList()
if (load.isNullOrEmpty()) {
Log.i("kangf", "load == null")
return@setOnClickListener
}
load[0].toWebActivity(this, "https://www.baidu.com", "百度")
load.forEach {
Log.i("kangf", it.javaClass.canonicalName ?: it.javaClass.name)
}
}
通过ServiceLoader的load方法,传入接口类类型,返回的是一个Itrator对象,因为接口的实现类可能不止一个。我们这里为了方便,获取第1个实现类,直接调用其方法即可。
我们先点击按钮,打印出来的是实现类的全名:
[图片上传失败...(image-8a891f-1701756859877)]
实际运行效果:
<img src="https://img-blog.csdnimg.cn/20200810164309640.gif#pic_center" alt="在这里插入图片描述" style="zoom:50%;" />
6不6,就是这么简单,模块间所有的交互都可以通过接口来实现。
原理篇
至于原理,也是非常简单,AutoService源码:https://github.com/google/auto
SPI
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。这么说可能有些抽象,我们在源码中去理解。
注解
从AutoService注解来入手:
@Documented
@Retention(CLASS)
@Target(TYPE)
public @interface AutoService {
/** Returns the interfaces implemented by this service provider. */
Class<?>[] value();
}
这个注解的参数是一个Class, 看注释可以知道,这个注解使用在接口的实现类上面。对于注解不了解的同学,可以看一下这篇文章哦:
https://blog.csdn.net/qq_22090073/article/details/104476822
注解处理器
这个注解规定被标记在一个类上面,且在jvm加载class时被抛弃,所以这是一个在编译时处理的注解,必定是有注解处理器了。
注解处理器就是在编译时检测所有的注解,在处理器中查找我们需要的住进进行处理 (通过注解找到类元信息生成java代码或其他文件),然后同时被打包到jar文件中
@SupportedOptions({ "debug", "verify" })
public class AutoServiceProcessor extends AbstractProcessor {
// 省略不重要的东东。。。。。。。。。。。。。。。。。。。。
/**
* <ol>
* <li> 遍历所有带有AutoService注解的类<ul>
* <li> 验证AutoService的值是否正确
* <li> 按服务接口对class进行分类
* </ul>
*
* <li> For each {@link AutoService} interface <ul>
* <li> 创建一个文件 在META-INF/services/路径下
* <li> 遍历所有带有AutoService注解的类 <ul>
* <li>在文件里面创建一个实体(也就是类的全路径)
* </ul>
* </ul>
* </ol>
*/
/**
* 相当于main函数,开始处理注解
* 注解处理器的核心方法,处理具体的注解,生成Java文件
*
* @param set 使用了支持处理注解的节点集合
* @param roundEnvironment 当前或是之前的运行环境,可以通过该对象查找的注解。
* @return true 表示后续处理器不会再处理(已经处理完成)
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
// 调用了processImpl方法
return processImpl(annotations, roundEnv);
} catch (Exception e) {
// 如果在写入的过程中发生了错误,就把错误写入到文件中
StringWriter writer = new StringWriter();
e.printStackTrace(new PrintWriter(writer));
fatalError(writer.toString());
return true;
}
}
private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 查找所有的带注解的class,生成配置文件
// 1. 如果注解处理完成,生成文件
if (roundEnv.processingOver()) {
generateConfigFiles();
} else { // 2. 否则处理注解
processAnnotations(annotations, roundEnv);
}
return true;
}
// 处理注解
private void processAnnotations(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
// 找到所有带注解的节点 elements,每个带AutoService注解的class就是一个节点
// 其实就是把class封装成了一个Element对象
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);
log(annotations.toString());
log(elements.toString());
// 遍历节点
for (Element e : elements) {
// TODO(gak): check for error trees?
// 类节点就是TypeElement
TypeElement providerImplementer = (TypeElement) e;
// 将找到的实现了放到providers中,因为可能有多
AnnotationMirror annotationMirror = getAnnotationMirror(e, AutoService.class).get();
Set<DeclaredType> providerInterfaces = getValueFieldOfClasses(annotationMirror);
if (providerInterfaces.isEmpty()) {
error(MISSING_SERVICES_ERROR, e, annotationMirror);
continue;
}
for (DeclaredType providerInterface : providerInterfaces) {
TypeElement providerType = MoreTypes.asTypeElement(providerInterface);
log("provider interface: " + providerType.getQualifiedName());
log("provider implementer: " + providerImplementer.getQualifiedName());
if (checkImplementer(providerImplementer, providerType)) {
providers.put(getBinaryName(providerType), getBinaryName(providerImplementer));
} else {
String message = "ServiceProviders must implement their service provider interface. "
+ providerImplementer.getQualifiedName() + " does not implement "
+ providerType.getQualifiedName();
error(message, e, annotationMirror);
}
}
}
}
// 生成配置文件
private void generateConfigFiles() {
// 使用Filer对象生成文件,所以先创建出来
Filer filer = processingEnv.getFiler();
// 遍历找到的接口
for (String providerInterface : providers.keySet()) {
String resourceFile = "META-INF/services/" + providerInterface;
log("Working on resource file: " + resourceFile);
try {
// 1. 创建一个set集合
SortedSet<String> allServices = Sets.newTreeSet();
try {
// would like to be able to print the full path
// before we attempt to get the resource in case the behavior
// of filer.getResource does change to match the spec, but there's
// no good way to resolve CLASS_OUTPUT without first getting a resource.
FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
resourceFile);
log("Looking for existing resource file at " + existingFile.toUri());
// 2. 找到原来文件中存在的实现类,
Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
log("Existing service entries: " + oldServices);
// 3. 放到新的集合中,保证allServices中数据是文件中的最新数据
allServices.addAll(oldServices);
} catch (IOException e) {
// According to the javadoc, Filer.getResource throws an exception
// if the file doesn't already exist. In practice this doesn't
// appear to be the case. Filer.getResource will happily return a
// FileObject that refers to a non-existent file but will throw
// IOException if you try to open an input stream for it.
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;
}
}
}
/**
* Verifies {@link ServiceProvider} constraints on the concrete provider class.
* Note that these constraints are enforced at runtime via the ServiceLoader,
* we're just checking them at compile time to be extra nice to our users.
*/
private boolean checkImplementer(TypeElement providerImplementer, TypeElement providerType) {
String verify = processingEnv.getOptions().get("verify");
if (verify == null || !Boolean.valueOf(verify)) {
return true;
}
// TODO: We're currently only enforcing the subtype relationship
// constraint. It would be nice to enforce them all.
Types types = processingEnv.getTypeUtils();
return types.isSubtype(providerImplementer.asType(), providerType.asType());
}
/**
* Returns the binary name of a reference type. For example,
* {@code com.google.Foo$Bar}, instead of {@code com.google.Foo.Bar}.
*
*/
private String getBinaryName(TypeElement element) {
return getBinaryNameImpl(element, element.getSimpleName().toString());
}
private String getBinaryNameImpl(TypeElement element, String className) {
Element enclosingElement = element.getEnclosingElement();
if (enclosingElement instanceof PackageElement) {
PackageElement pkg = (PackageElement) enclosingElement;
if (pkg.isUnnamed()) {
return className;
}
return pkg.getQualifiedName() + "." + className;
}
TypeElement typeElement = (TypeElement) enclosingElement;
return getBinaryNameImpl(typeElement, typeElement.getSimpleName() + "$" + className);
}
/**
* Returns the contents of a {@code Class[]}-typed "value" field in a given {@code annotationMirror}.
*/
private ImmutableSet<DeclaredType> getValueFieldOfClasses(AnnotationMirror annotationMirror) {
return getAnnotationValue(annotationMirror, "value")
.accept(
new SimpleAnnotationValueVisitor8<ImmutableSet<DeclaredType>, Void>() {
@Override
public ImmutableSet<DeclaredType> visitType(TypeMirror typeMirror, Void v) {
// TODO(ronshapiro): class literals may not always be declared types, i.e. int.class,
// int[].class
return ImmutableSet.of(MoreTypes.asDeclared(typeMirror));
}
@Override
public ImmutableSet<DeclaredType> visitArray(
List<? extends AnnotationValue> values, Void v) {
return values
.stream()
.flatMap(value -> value.accept(this, null).stream())
.collect(toImmutableSet());
}
},
null);
}
private void log(String msg) {
if (processingEnv.getOptions().containsKey("debug")) {
processingEnv.getMessager().printMessage(Kind.NOTE, msg);
}
}
private void error(String msg, Element element, AnnotationMirror annotation) {
processingEnv.getMessager().printMessage(Kind.ERROR, msg, element, annotation);
}
private void fatalError(String msg) {
processingEnv.getMessager().printMessage(Kind.ERROR, "FATAL ERROR: " + msg);
}
}
final class ServicesFiles {
public static final String SERVICES_PATH = "META-INF/services";
private ServicesFiles() { }
static String getPath(String serviceName) {
return SERVICES_PATH + "/" + serviceName;
}
/**
* 读取文件
*/
static Set<String> readServiceFile(InputStream input) throws IOException {
HashSet<String> serviceClasses = new HashSet<String>();
Closer closer = Closer.create();
try {
// TODO(gak): use CharStreams
BufferedReader r = closer.register(new BufferedReader(new InputStreamReader(input, UTF_8)));
String line;
while ((line = r.readLine()) != null) {
int commentStart = line.indexOf('#');
if (commentStart >= 0) {
line = line.substring(0, commentStart);
}
line = line.trim();
if (!line.isEmpty()) {
serviceClasses.add(line);
}
}
return serviceClasses;
} catch (Throwable t) {
throw closer.rethrow(t);
} finally {
closer.close();
}
}
/**
* 按接口实现类全名和接口全名写入文件
*
* @param output not {@code null}. Not closed after use.
* @param services a not {@code null Collection} of service class names.
* @throws IOException
*/
static void writeServiceFile(Collection<String> services, OutputStream output)
throws IOException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, UTF_8));
for (String service : services) {
writer.write(service);
writer.newLine();
}
writer.flush();
}
}
以上就是注解处理器的全部关键代码,总共分为以下几步:
- 1.遍历找到所有带有AutoService注解的类
- 2.验证AutoService注解的值是否正确
- 3.遍历所有的下沉接口
- 3.在META-INF/services/路径下创建文件,文件名以类的接口类全路径命名
- 4.在文件里写入内容,实现类(当前注解类)的全路径
第三、四步可能大家不太明白,拿我们上面的例子来说:
其实就是在META-INF/services/路径下生成了一个名字为com.kagnf.webview.autoservice.AutoServiceInterface的文件
文件内容是com.kangf.webview.autoservice.AutoServiceImpl,大家打包后可以在apk里面看到:
<img src="https://img-blog.csdnimg.cn/20200825160553694.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIyMDkwMDcz,size_16,color_FFFFFF,t_70#pic_center" alt="在这里插入图片描述" style="zoom:50%;" />
如果有多个下沉接口,肯定就会有多个文件了。源码看起来复杂,但是处理器具体就这几步的操作。
ServiceLoader
注解处理器只生成了这样一个文件,可是怎么用呢?这时候Java API的作用就体现出来了,在上面的demo中,我们通过ServiceLoader.load(AutoServiceInterface::class.java)?.toList()
实现了接口的实现类的查找,因为可能不止一个实现类,所以这里返回了一个Iterator对象,使用toList方法转成List。
那么ServiceLoader是怎么实现具体查找的?走进去看看吧
// ServiceLoader.java
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
创建了一个类加载器,调用自己的load方法():
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
然后直接通过new的方式,创建了一个ServiceLoader对象,并返回,上面我们知道它返回是一个Iterator,所以它是实现了Iteratable接口的:
public final class ServiceLoader<S>
implements Iterable<S>
{
// 。。。。。。。。。。。
}
那么不用想,它的主要逻辑就在构造方法里面了:
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(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改变了:不要使用旧的安全代码。
// 在Android中, System.getSecurityManager() 永远为null
// acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
构造方法有两个参数,svc是我们传进来的接口的class对象,cl是内部创建的一个ClassLoader。首先给全局的service变量赋值为class对象,然后调用了reload()
方法。
reload()方法中,创建了一个懒加载的迭代器:lookupIterator = new LazyIterator(service, loader);
,并传入了class对象和类加载器,这里其实相当于一个门面模式,当调用ServiceLoader.iterator()时创建迭代器时,会使用到lookupIterator
的hasNext()和next()
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
// 返回懒加载迭代器的hasNext
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
// 返回懒加载迭代器的next
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
接下来我们看看这个懒加载迭代器怎么创建的:
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
// 当前接口对应的文件节点
Enumeration<URL> configs = null;
// 文件中的具体实现类的全路径,因为可能不止一个实现类,所以使用Iterator
Iterator<String> pending = null;
// 下一个实现类的文件名
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
// 第一次进入,nextName为Null
if (nextName != null) {
return true;
}
// configs为null,进入if语句
if (configs == null) {
try {
// private static final String PREFIX = "META-INF/services/";
// fullname就是找到META-INF/services/目录下的,以接口名全名命名的文件
// demo举例就是 META-INF/services/com.kagnf.webview.autoservice.AutoServiceInterface
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else // classloader加载这个资源文件,返回一个Enumeration节点对象
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赋值
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) {
// 。。。。。。。。。。。。。
}
if (!service.isAssignableFrom(c)) {
// 抛异常,这里不用管 。。。。。。。。。。
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
//。。。。。。。。。。。。。。。。。。。。。。。。。
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
return hasNextService();
}
public S next() {
return nextService();
}
public void remove() {
throw new UnsupportedOperationException();
}
}
以上代码并不难, 重写hasNext(),和next()方法,分别调用了hasNextService()
和nextService()
,顾名思义,nextService()方法就是返回具体的实现类
nextService()主要分为以下几步:
- 首先调用hasNextService()方法,判断有没有更多的接口对应的文件
- hasNextService中,找到下一个文件,并解析内容,把文件的class名放到Iterator中,给nextName赋值
- 使用Class.forName加载文件中的类,并使用c.newInstance()创建并返回类的实体
这样就完成了实现类的创建!
有同学有疑问,我们并没有用到ServiceLoader的next方法啊,因为kotlin的语法糖可以直接toList, 在java中正确的用法应该是:
val load = ServiceLoader.load(AutoServiceInterface::class.java)?.iterator().next();
这样是不是就一目了然了?这里获取的就是com.kagnf.webview.autoservice.AutoServiceInterface文件中的第一个实现类
总结
是不是很简单,一句话就能总价,先通过注解处理器在META-INF/services/目录下生成对应的文件,再通过ServiceLoader加载文件中的类并返回。这就是java中大名鼎鼎的SPI机制。
对于注解处理器不太了解的同学可以看下这篇文章:
手写ARouter
网友评论