目前业界已经有很多成熟的路由框架,最著名的应该是 ARouter ,那么我们今天为什么还要重新造轮子呢? 我个人觉得有以下原因:
进入正题前,我们先预告一下接下来会涉及到的知识点
使用注解处理器,一般需要3个 Module:
新建 Java Module
创建 Router 注解
/** * 标记路由信息,仅支持 Activity */ @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class Router( /** * URL path,可以为 "" 或者以 "/" 开头,例如 "/example//.html",支持正则表达式,注意转义 */ val value: String, /** * URL scheme,不包含 "://",例如 "http",支持正则表达式,注意转义 */ val scheme: String = "(http|https|native|domain)", /** * URL host,不包含 "/",例如 "www//.google//.com",支持正则表达式,注意转义 */ val host: String = "(//w+//.)*domain//.com", /** * 是否需要登录,默认不需要 * * 需要调用 [CRouter#setLoginProvider] 才能生效 */ val needLogin: Boolean = false ) 复制代码
提供以下参数
注意一点,这里为了便于匹配,这里 scheme、host、path 都支持正则表达式,这样一条规则可以匹配 N 多链接,也可以支持参数在 path 中的链接形式,不过要注意对于特殊字符的转义
举个栗子,要支持如下链接
https://www.wanandroid.com/blog/show/2657 复制代码
参数文章 ID 是 2657,那么 path 就可以写为
/bolg/show///d+ 复制代码
看一下在 Activity 中的使用
@Router("/home/rankList") class RankListActivity : BaseActivity() { ...... } 复制代码
新建 Java Module,和上一步类似,这里不再截图
在 Module build.gradle 中添加以下依赖
dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation 'com.google.auto.service:auto-service:1.0-rc6' implementation 'com.squareup:javapoet:1.11.1' implementation project(':crouter-annotation') } 复制代码
接下来新建 RouterProcessor
@AutoService(Processor::class) class RouterProcessor : AbstractProcessor() { override fun getSupportedAnnotationTypes(): MutableSet<String> { val supportAnnotationTypes = mutableSetOf<String>() supportAnnotationTypes.add(Router::class.java.canonicalName) return supportAnnotationTypes } override fun getSupportedSourceVersion(): SourceVersion { return SourceVersion.latestSupported() } override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean { return false } } 复制代码
继承自 AbstractProcessor,表明是一个注解处理器
添加 AutoService 注解,用于自动生成 META-INF 配置信息
这里遇到一个坑,我使用的是 Android Studio 3.1.4 和 Kotlin 1.2.60,无论如何也不会自动生成 META-INF,导致编译时无法识别 Processor,最后只能手动添加:
在 src/main 目录下新建 /resources/META-INF/services/javax.annotation.processing.Processor 目录和文件
文件内容是 Processor 的包名 + 类名
me.wcy.crouter.compiler.RouterProcessor 复制代码
重写 getSupportedAnnotationTypes
,指定支持的注解类型,即 Router::class
重写 getSupportedSourceVersion
,指定支持源码版本,这个是固定模板
主要在 process
中对注解进行处理
为了确认我们的注解已经创建成功了,我们在 app 中引入注解处理器
app build.gradle
apply plugin: 'kotlin-kapt' dependencies { implementation project(':crouter-annotation') kapt project(':crouter-compiler') } 复制代码
Kotlin 中使用 kapt
添加注解处理器
我们在 Processor 的 process
方法中输出一条日志
private lateinit var messager: Messager override fun init(processingEnv: ProcessingEnvironment) { super.init(processingEnv) // 保存 messager 对象 this.messager = processingEnv.messager } override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean { this.messager.printMessage(Diagnostic.Kind.WARNING, "=============> RouterProcessor 已经生效") return false } 复制代码
这里也遇到了一个坑,Kotlin 中 NOTE 及以下级别的日志不会在控制台打印,所以至少要使用 WARNING 级别以上的日志
不得不说 Kotlin 的坑还是不少的
不过据说在新版本都已经修复了,我还没有验证,大家可以试一下
尝试一下,Build -> Rebuild Project,然后观察 build 日志
正常情况下,我们已经可以看到 Processor 的日志了,激动
如果没有看到日志,需要回过头一步步排查下哪里没写对
我们已经验证 Processor 有效,下面开始解析路由注解
首先,在 init
中保存需要的对象
private lateinit var filer: Filer private lateinit var elementUtil: Elements private lateinit var typeUtil: Types override fun init(processingEnv: ProcessingEnvironment) { super.init(processingEnv) filer = processingEnv.filer elementUtil = processingEnv.elementUtils typeUtil = processingEnv.typeUtils Log.setLogger(processingEnv.messager) } 复制代码
这里对日志进行封装,方便使用
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean { val routerElements = roundEnv.getElementsAnnotatedWith(Router::class.java) Log.w("[CRouter] Found routers, size is " + routerElements.size) val activityType = elementUtil.getTypeElement("android.app.Activity") for (element in routerElements) { val typeMirror = element.asType() val router = element.getAnnotation(Router::class.java) if (typeUtil.isSubtype(typeMirror, activityType.asType())) { Log.w("[CRouter] Found activity router: $typeMirror") var routerUrl = ProcessorUtils.assembleRouterUrl(router) routerUrl = ProcessorUtils.escapeUrl(routerUrl) } } ...... } 复制代码
通过 roundEnv.getElementsAnnotatedWith(Router::class.java)
获取注解 Router 注解的 Class 信息
遍历 Class 信息,通过 element.getAnnotation(Router::class.java)
获取 Router 注解信息,即路由信息,根据路由信息拼装路由 URL
路由仅支持 Activity,因此需要排除掉不是 Activity 的 Class
路由信息已经收集完成,接下来要保存到 Java 文件中,那么问题来了,我们首先要先预想一下保存的 Java 文件的结构是什么样的?
首先我们要有一个实体保存路由信息,这里我们可以使用接口
/** * 真正的路由信息 */ interface Route { fun url(): String fun target(): Class<*> fun needLogin(): Boolean } 复制代码
路由信息最终需要汇总到一个列表中,提供一个接口,用于加载路由信息
/** * 路由加载器 */ public interface RouterLoader { void loadRouter(Set<Route> routeSet); } 复制代码
routeSet 由外部传入,用于保存路由信息
生成的 Java 文件可以实现该接口,将扫描到的路由信息保存起来
这时有请 javapoet
登场
/** * Method: @Override public void loadRouter(Set<Route> routerSet) */ val loadRouterMethodBuilder = MethodSpec.methodBuilder(ProcessorUtils.METHOD_NAME) .addAnnotation(Override::class.java) .addModifiers(Modifier.PUBLIC) .addParameter(groupParamSpec) for (element in routerElements) { val typeMirror = element.asType() val router = element.getAnnotation(Router::class.java) if (typeUtil.isSubtype(typeMirror, activityType.asType())) { Log.w("[CRouter] Found activity router: $typeMirror") val activityCn = ClassName.get(element as TypeElement) var routerUrl = ProcessorUtils.assembleRouterUrl(router) routerUrl = ProcessorUtils.escapeUrl(routerUrl) /** * Statement: routerSet.add(RouterBuilder.buildRouter(url, needLogin, target)); */ loadRouterMethodBuilder.addStatement("/$N.add(/$T.buildRouter(/$N, /$N, /$T.class))", ProcessorUtils.PARAM_NAME, routerBuilderCn, routerUrl, router.needLogin.toString(), activityCn) } } /** * Write to file */ JavaFile.builder("me.wcy.router.annotation.loader", TypeSpec.classBuilder(ProcessorUtils.getFileName()) .addJavadoc(ProcessorUtils.JAVADOC) .addSuperinterface(ClassName.get(RouterLoader::class.java)) .addModifiers(Modifier.PUBLIC) .addMethod(loadRouterMethodBuilder.build()) .build()) .build() .writeTo(filer) 复制代码
这里贴出了主要代码,主要是创建了一个 Java 类,实现上面的 RouterLoader
接口,添加 loadRouter
方法,保存路由信息,最后添加注释、修饰符等属性,写入文件, javapoet
的使用不属于本文范畴,因此不再展开讲解,完整代码可参考源码
为了方便生成代码,将构造路由信息封装为一个方法
public class RouterBuilder { public static Route buildRouter(String url, boolean needLogin, Class target) { return new Route() { @NotNull @Override public String url() { return url; } @NotNull @Override public Class target() { return target; } @Override public boolean needLogin() { return needLogin; } }; } } 复制代码
不知道泥萌有没有发现,这里出现了 Java 代码的身影(不对,好像前面就出现了,算了,我也懒得找了:sweat:),不是说好用 Kotlin 吗,欺骗感情?
少侠请息怒,真的不是我欺骗大家感情,我也想全程 Kotlin 啊,可是 javapoet
他不支持 Kotlin 啊...
生成的 Java 文件使用固定包名 me.wcy.router.annotation.loader
,生成类名的方法
fun getFileName(): String { return "RouterLoader" + "_" + UUID.randomUUID().toString().replace("-", "") } 复制代码
大家不妨思考一下,这里为什么使用 RouterLoader + UUID
的方式生成类名?
是因为对于多 Module 项目,每个 Module 都需要收集路由信息,使用随机命名防止被覆盖
这时有有些同学站起来了:随机类名看着太乱,如果我想以 Module 的名字命名怎么办?
好问题!
如果想要根据 Module 命名,可以利用 kapt 设置 Module 的参数,在 Processor 的 init
方法中读取参数
代码稍后奉上...
到这里,我们完成了路由信息解析和创建 Java 文件保存路由信息,下面让我们 Rebuild
一下
正常情况下,我们已经可以在 app/build/generated/source/kapt/debug/me/wcy/router/annotation/loader
下看到我们在编译器生成的 Java 文件了
打开看一下内容
/** * DO NOT EDIT THIS FILE! IT WAS GENERATED BY CROUTER. */ public class RouterLoader_52def16bb9fa438ca17fec7b3b3f6787 implements RouterLoader { @Override public void loadRouter(Set<Route> routerSet) { routerSet.add(RouterBuilder.buildRouter("(http|https|native|domain)://(//w+//.)*domain//.com", false, HomeActivity.class)); routerSet.add(RouterBuilder.buildRouter("(http|https|native|domain)://(//w+//.)*domain//.com/home/rankList", false, RankListActivity.class)); routerSet.add(RouterBuilder.buildRouter("(http|https|native|domain)://(//w+//.)*domain//.com/home/newTask", false, NewerTaskActivity.class)); } } 复制代码
大功告成!
文章篇幅所限,本文暂且讲到这里,敬请期待下篇 「手撸一个 Router 框架(上):路由拦截机制」