资料
组件化之AutoService使用与源码解析
资源目录
- router-annotations
这是一个java项目,主要是为了定义一个注解Destination - router-processor
使用AutoService功能,来处理生成的json文件 - router-runtime
Router用于 - buildSrc
实现插件功能,将每个模块自动生成的json文件,编译到Router类中
router-annotations
Destination.java
package com.imooc.router.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 说明当前注解可以修饰的元素,此处表示可以用于标记在类上面
*/
@Target({ElementType.TYPE})
/**
* 说明当前注解可以被保留的时间
*/
@Retention(RetentionPolicy.CLASS)
public @interface Destination {
/**
* 当前页面的URL,不能为空
* @return 页面URL
*/
String url();
/**
* 对于当前页面的中文描述
* @return 例如 "个人主页"
*/
String description();
}
router-runtime
Router.java
package com.imooc.gradle.router.runtime
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
object Router {
private const val TAG = "RouterTAG"
// 编译期间生成的总映射表
private const val GENERATED_MAPPING = "com.imooc.router.mapping.generated.RouterMapping"
// 存储所有映射表信息
private val mapping: HashMap<String, String> = HashMap()
fun init() {
try {
val clazz = Class.forName(GENERATED_MAPPING)
val method = clazz.getMethod("get")
val allMapping = method.invoke(null) as Map<String, String>
if (allMapping?.size > 0) {
Log.i(TAG, "init: get all mapping:")
allMapping.onEach {
Log.i(TAG, " ${it.key} -> ${it.value}")
}
mapping.putAll(allMapping)
}
} catch (e: Throwable) {
Log.e(TAG, "init: error while init router : $e")
}
}
fun go(context: Context, url: String) {
if (context == null || url == null) {
Log.i(TAG, "go: param error.")
return
}
// 匹配URL,找到目标页面
// router://imooc/profile?name=imooc&message=hello
val uri = Uri.parse(url)
val scheme = uri.scheme
val host = uri.host
val path = uri.path
var targetActivityClass = ""
mapping.onEach {
val ruri = Uri.parse(it.key)
val rscheme = ruri.scheme
val rhost = ruri.host
val rpath = ruri.path
if (rscheme == scheme && rhost == host && rpath == path) {
targetActivityClass = it.value
}
}
if (targetActivityClass == "") {
Log.e(TAG, "go: no destination found.")
return
}
// 解析URL里的参数,封装成为一个 Bundle
val bundle = Bundle()
val query = uri.query
query?.let {
if (it.length >= 3) {
val args = it.split("&")
args.onEach { arg ->
val splits = arg.split("=")
bundle.putString(splits[0], splits[1])
}
}
}
// 打开对应的Activity,并传入参数
try {
val activity = Class.forName(targetActivityClass)
val intent = Intent(context, activity)
intent.putExtras(bundle)
context.startActivity(intent)
} catch (e : Throwable) {
Log.e(TAG, "go: error while start activity: $targetActivityClass, " +
"e = $e")
}
}
}
router-processor
DestinationProcessor.java
package com.imooc.router.processor;
import com.google.auto.service.AutoService;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.imooc.router.annotations.Destination;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.Writer;
import java.util.Collections;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
@AutoService(Processor.class)
public class DestinationProcessor extends AbstractProcessor {
private static final String TAG = "DestinationProcessor";
/**
* 编译器找到我们关心的注解后,会回调这个方法
* @param set
* @param roundEnvironment
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> set,
RoundEnvironment roundEnvironment) {
// 避免多次调用 process
if (roundEnvironment.processingOver()) {
return false;
}
System.out.println(TAG + " >>> process start ...");
String rootDir = processingEnv.getOptions().get("root_project_dir");
// 获取这个模块中,所有的@Destination注解的类的信息
// 获取所有标记了 @Destination 注解的 类的信息
Set<Element> allDestinationElements = (Set<Element>)roundEnvironment.getElementsAnnotatedWith(Destination.class);
System.out.println(TAG + " >>> all Destination elements count = " + allDestinationElements.size());
// 当未收集到 @Destination 注解的时候,跳过后续流程
if (allDestinationElements.size() < 1) {
return false;
}
// 将要自动生成的类的类名
String className = "RouterMapping_" + System.currentTimeMillis();
StringBuilder builder = new StringBuilder();
builder.append("package com.imooc.router.mapping;\n\n");
builder.append("import java.util.HashMap;\n");
builder.append("import java.util.Map;\n\n");
builder.append("public class ").append(className).append(" {\n\n");
builder.append(" public static Map<String, String> get() {\n\n");
builder.append(" Map<String, String> mapping = new HashMap<>();\n\n");
final JsonArray destinationJsonArray = new JsonArray();
// 遍历所有 @Destination 注解信息,挨个获取详细信息
for (Element element : allDestinationElements) {
final TypeElement typeElement = (TypeElement) element;
// 尝试在当前类上,获取 @Destination 的信息
final Destination destination = typeElement.getAnnotation(Destination.class);
if (destination == null) {
continue;
}
final String url = destination.url();
final String description = destination.description();
final String realPath = typeElement.getQualifiedName().toString();
System.out.println(TAG + " >>> url = " + url);
System.out.println(TAG + " >>> description = " + description);
System.out.println(TAG + " >>> realPath = " + realPath);
// 将目标类放入生成的map文件中
builder.append(" ")
.append("mapping.put(")
.append("\"" + url + "\"")
.append(", ")
.append("\"" + realPath + "\"")
.append(");\n");
// 将目标路径放入到json文件中
JsonObject item = new JsonObject();
item.addProperty("url", url);
item.addProperty("description", description);
item.addProperty("realPath", realPath);
destinationJsonArray.add(item);
}
builder.append(" return mapping;\n");
builder.append(" }\n");
builder.append("}\n");
String mappingFullClassName = "com.imooc.router.mapping." + className;
System.out.println(TAG + " >>> mappingFullClassName = " + mappingFullClassName);
System.out.println(TAG + " >>> class content = \n" + builder);
// 写入自动生成的类到本地文件中
try {
JavaFileObject source = processingEnv.getFiler().createSourceFile(mappingFullClassName);
Writer writer = source.openWriter();
writer.write(builder.toString());
writer.flush();
writer.close();
} catch (Exception ex) {
throw new RuntimeException("Error while create file", ex);
}
// 写入JSON到本地文件中
// 检测父目录是否存在
File rootDirFile = new File(rootDir);
if (!rootDirFile.exists()) {
throw new RuntimeException("root_project_dir not exist!");
}
// 创建 router_mapping 子目录
File routerFileDir = new File(rootDirFile, "router_mapping");
if (!routerFileDir.exists()) {
routerFileDir.mkdir();
}
File mappingFile = new File(routerFileDir,"mapping_" + System.currentTimeMillis() + ".json");
// 写入json内容
try {
BufferedWriter out = new BufferedWriter(new FileWriter(mappingFile));
String jsonStr = destinationJsonArray.toString();
out.write(jsonStr);
out.flush();
out.close();
} catch (Throwable throwable) {
throw new RuntimeException("Error while writing json", throwable);
}
System.out.println(TAG + " >>> process finish.");
return false;
}
/**
* 告诉编译器,当前处理器支持的注解类型
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(Destination.class.getCanonicalName());
}
}
总结:
buildSrc
buildSrc
—> RouterExtension.groovy
—> RouterMappingByteCodeBuilder.groovy
—> RouterMappingController.groovy
—> RouterMappingTransform.groovy
—> RouterPlugin.groovy
注册RouterPlugin
resources/META-INF/gradle-plugins/com.imooc.router.properties
implementation-class=com.imooc.router.gradle.RouterPlugin
RouterPlugin.groovy
package com.imooc.router.gradle
import com.android.build.api.transform.Transform
import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import groovy.json.JsonSlurper
class RouterPlugin implements Plugin<Project> {
// 实现apply方法,注入插件的逻辑
void apply(Project project) {
// 注册 Transform
if (project.plugins.hasPlugin(AppPlugin)) {
AppExtension appExtension = project.extensions.getByType(AppExtension)
Transform transform = new RouterMappingTransform()
appExtension.registerTransform(transform)
}
// 1. 自动帮助用户传递路径参数到注解处理器中
if (project.extensions.findByName("kapt") != null) {
project.extensions.findByName("kapt").arguments {
arg("root_project_dir", project.rootProject.projectDir.absolutePath)
}
}
// 2. 实现旧的构建产物的自动清理
project.clean.doFirst {
// 删除 上一次构建生成的 router_mapping目录
File routerMappingDir = new File(project.rootProject.projectDir, "router_mapping")
if (routerMappingDir.exists()) {
routerMappingDir.deleteDir()
}
}
if (!project.plugins.hasPlugin(AppPlugin)) {
return
}
println("I am from RouterPlugin, apply from ${project.name}")
// 获取参数
project.getExtensions().create("router", RouterExtension)
project.afterEvaluate {
RouterExtension extension = project["router"]
println("用户设置的WIKI路径为 : ${extension.wikiDir}")
// 3. 在javac任务 (compileDebugJavaWithJavac) 后,汇总生成文档
project.tasks.findAll { task ->
task.name.startsWith('compile') && task.name.endsWith('JavaWithJavac')
}.each { task ->
task.doLast {
File routerMappingDir = new File(project.rootProject.projectDir, "router_mapping")
if (!routerMappingDir.exists()) {
return
}
File[] allChildFiles = routerMappingDir.listFiles()
if (allChildFiles.length < 1) {
return
}
// 生成页面文档
StringBuilder markdownBuilder = new StringBuilder()
markdownBuilder.append("# 页面文档\n\n")
allChildFiles.each { child ->
if (child.name.endsWith(".json")) {
JsonSlurper jsonSlurper = new JsonSlurper()
def content = jsonSlurper.parse(child)
content.each { innerContent ->
def url = innerContent['url']
def description = innerContent['description']
def realPath = innerContent['realPath']
markdownBuilder.append("## $description \n")
markdownBuilder.append("- url: $url \n")
markdownBuilder.append("- realPath: $realPath \n\n")
}
}
}
File wikiFileDir = new File(extension.wikiDir)
if (!wikiFileDir.exists()) {
wikiFileDir.mkdir()
}
File wikiFile = new File(wikiFileDir, "页面文档.md")
if (wikiFile.exists()) {
wikiFile.delete()
}
wikiFile.write(markdownBuilder.toString())
}
}
}
}
}
RouterMappingTransform.groovy
package com.imooc.router.gradle
import com.android.build.api.transform.Format
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
class RouterMappingTransform extends Transform {
/**
* 当前 Transform 的名称
* @return
*/
@Override
String getName() {
return "RouterMappingTransform"
}
/**
* 返回告知编译器,当前Transform需要消费的输入类型
* 在这里是CLASS类型
* @return
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* 告知编译器,当前Transform需要收集的范围
* @return
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
/**
* 是否支持增量
* 通常返回False
* @return
*/
@Override
boolean isIncremental() {
return false
}
/**
* 所有的class收集好以后,会被打包传入此方法
* @param transformInvocation
* @throws TransformException
* @throws InterruptedException
* @throws IOException
*/
@Override
void transform(TransformInvocation transformInvocation)
throws TransformException, InterruptedException, IOException {
// 1. 遍历所有的Input
// 2. 对Input进行二次处理
// 3. 将Input拷贝到目标目录
// 工具类
RouterMappingCollector collector = new RouterMappingCollector()
// 遍历所有的输入
transformInvocation.inputs.each {
// 把 文件夹 类型的输入,拷贝到目标目录
it.directoryInputs.each { directoryInput ->
def destDir = transformInvocation.outputProvider
.getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY)
collector.collect(directoryInput.file)
FileUtils.copyDirectory(directoryInput.file, destDir)
}
// 把 JAR 类型的输入,拷贝到目标目录
it.jarInputs.each { jarInput ->
def dest = transformInvocation.outputProvider
.getContentLocation(
jarInput.name,
jarInput.contentTypes,
jarInput.scopes, Format.JAR)
collector.collectFromJarFile(jarInput.file)
FileUtils.copyFile(jarInput.file, dest)
}
}
println("${getName()} all mapping class name = " + collector.mappingClassName)
File mappingJarFile = transformInvocation.outputProvider.
getContentLocation(
"router_mapping",
getOutputTypes(),
getScopes(),
Format.JAR)
println("${getName()} mappingJarFile = $mappingJarFile")
if (mappingJarFile.getParentFile().exists()) {
mappingJarFile.getParentFile().mkdirs()
}
if (mappingJarFile.exists()) {
mappingJarFile.delete()
}
// 将生成的字节码,写入本地文件
FileOutputStream fos = new FileOutputStream(mappingJarFile)
JarOutputStream jarOutputStream = new JarOutputStream(fos)
ZipEntry zipEntry = new ZipEntry(RouterMappingByteCodeBuilder.CLASS_NAME + ".class")
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(RouterMappingByteCodeBuilder.get(collector.mappingClassName))
jarOutputStream.closeEntry()
jarOutputStream.close()
fos.close()
}
}
RouterMappingController.groovy
package com.imooc.router.gradle
import java.util.jar.JarEntry
import java.util.jar.JarFile
class RouterMappingCollector {
private static final String PACKAGE_NAME = 'com/imooc/router/mapping'
private static final String CLASS_NAME_PREFIX = 'RouterMapping_'
private static final String CLASS_FILE_SUFFIX = '.class'
private final Set<String> mappingClassNames = new HashSet<>()
/**
* 获取收集好的映射表类名
* @return
*/
Set<String> getMappingClassName() {
return mappingClassNames;
}
/**
* 收集class文件或者class文件目录中的映射表类。
* @param classFile
*/
void collect(File classFile) {
if (classFile == null || !classFile.exists()) return
if (classFile.isFile()) {
if (classFile.absolutePath.contains(PACKAGE_NAME)
&& classFile.name.startsWith(CLASS_NAME_PREFIX)
&& classFile.name.endsWith(CLASS_FILE_SUFFIX)) {
String className =classFile.name.replace(CLASS_FILE_SUFFIX, "")
mappingClassNames.add(className)
}
} else {
classFile.listFiles().each {
collect(it)
}
}
}
/**
* 收集JAR包中的目标类
* @param jarFile
*/
void collectFromJarFile(File jarFile) {
Enumeration enumeration = new JarFile(jarFile).entries()
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry)enumeration.nextElement()
String entryName = jarEntry.getName()
if (entryName.contains(PACKAGE_NAME)
&& entryName.contains(CLASS_NAME_PREFIX)
&& entryName.contains(CLASS_FILE_SUFFIX)) {
String className = entryName
.replace(PACKAGE_NAME, "")
.replace("/", "")
.replace(CLASS_FILE_SUFFIX, "")
mappingClassNames.add(className)
}
}
}
}
RouterMappingByteCodeBuilder.groovy
package com.imooc.router.gradle
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
class RouterMappingByteCodeBuilder implements Opcodes {
public static final String CLASS_NAME = "com/imooc/router/mapping/generated/RouterMapping"
static byte[] get(Set<String> allMappingNames) {
// 1. 创建一个类
// 2. 创建构造方法
// 3. 创建get方法
// (1)创建一个Map
// (2)塞入所有映射表的内容
// (3)返回map
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS)
cw.visit(V1_7,
ACC_PUBLIC + ACC_SUPER,
CLASS_NAME,
null,
"java/lang/Object",
null
)
// 生成或者编辑方法
MethodVisitor mv
// 创建构造方法
mv = cw.visitMethod(Opcodes.ACC_PUBLIC,
"<init>",
"()V",
null,
null)
mv.visitCode()
mv.visitVarInsn(Opcodes.ALOAD, 0)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL,
"java/lang/Object", "<init>", "()V", false)
mv.visitInsn(Opcodes.RETURN)
mv.visitMaxs(1, 1)
mv.visitEnd()
// 创建get方法
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
"get",
"()Ljava/util/Map;",
"()Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>;",
null)
mv.visitCode()
mv.visitTypeInsn(NEW, "java/util/HashMap")
mv.visitInsn(DUP)
mv.visitMethodInsn(INVOKESPECIAL, "java/util/HashMap", "<init>", "()V", false)
mv.visitVarInsn(ASTORE, 0)
// 向Map中,逐个塞入所有映射表的内容
allMappingNames.each {
mv.visitVarInsn(ALOAD, 0)
mv.visitMethodInsn(INVOKESTATIC,
"com/imooc/router/mapping/$it",
"get", "()Ljava/util/Map;", false)
mv.visitMethodInsn(INVOKEINTERFACE,
"java/util/Map",
"putAll",
"(Ljava/util/Map;)V", true)
}
// 返回map
mv.visitVarInsn(ALOAD, 0)
mv.visitInsn(ARETURN)
mv.visitMaxs(2, 2)
mv.visitEnd()
return cw.toByteArray()
}
}
RouterExtension.groovy
在gradle文件中使用
package com.imooc.router.gradle
class RouterExtension {
String wikiDir
}
总结:
- 使用AutoService,将每个模块添加注解的类的信息获取出来, 共插件使用
1.1 生成RouterMapping_xxxx.java 类
1.2 生成routermapping_xxxx.json文件 - 在运行plugin的时候,注册Transform,生成页面文档
2.1 Transform中获取所有的class文件(包括jar中的),将RouterMapping_xxx.class获取出来的
2.2 通过ASM字节码插桩的形式,将所有的class文件放入RouterMapping.class文件中,并写入 - 在使用的时候,先调用Router.init()。也就是从RouterMapping.class文件中获取所有的类信息
- 调用Router.go()来进行Activity的跳转