手撸一个 Router 框架(上):熟悉 APT
前言
目前业界已经有很多成熟的路由框架,最著名的应该是 ARouter,那么我们今天为什么还要重新造轮子呢? 我个人觉得有以下原因:
- ARouter 过于强大,很多功能我们不一定用得上,而且不一定适合我们的项目,自己撸一个,可以在满足项目需求的情况下,功能上去繁就简。
- 实践出真知,我想这也是很多开发者重复造轮子的主要原因吧。我们经常阅读许多大牛对于优秀框架的剖析,但那也只是大牛的理解,我们自己的呢?
- 便于排查问题。使用开源框架遇到问题一般会耗费更多的排查时间,因为我们对源码“不够熟悉”,而自己撸的一般都可以快速定位问题。
准备
进入正题前,我们先预告一下接下来会涉及到的知识点
- Kotlin,本文代码主要基于 Kotlin 语言编写,相信大家都知道 Kotlin 的好处了吧?
- APT,即 Annotation Processing Tool,注解处理器,用于在编译时扫描和处理注解,即解析和保存路由信息。
- 拦截器机制,众所周知 OKHTTP 的拦截器机制是十分强大的,我们也将参考并沿用这套机制。
正文
使用注解处理器,一般需要3个 Module:
- annotation - 包含注解类,提供给 compiler、api 和 app 使用
- compiler - 编译器,即注解处理器,在打包时处理注解
- api - 提供路由的 api 接口
注解 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
)
复制代码
提供以下参数
- value: 路由路径,即 path,为了方便这里直接用 value,不用显式指定参数名
- scheme、host: 这两个即是字面意思,提供默认值,一般使用默认值即可
- needLogin: 用于登录拦截,拦截机制下篇会讲到
注意一点,这里为了便于匹配,这里 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() {......
}
复制代码
注解处理 Module
新建 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')
}
复制代码
- auto-service: Google 出品,用于自动注册注解处理器
- javapoet: square 大厂的杰作,用于便捷的生成 Java 文件
接下来新建 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: Messageroverride 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: Typesoverride fun init(processingEnv: ProcessingEnvironment) {super.init(processingEnv)filer = processingEnv.filerelementUtil = processingEnv.elementUtilstypeUtil = processingEnv.typeUtilsLog.setLogger(processingEnv.messager)
}
复制代码
这里对日志进行封装,方便使用
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean {val routerElements = roundEnv.getElementsAnnotatedWith(Router::class.java)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(): Stringfun 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@Overridepublic String url() {return url;}@NotNull@Overridepublic Class target() {return target;}@Overridepublic boolean needLogin() {return needLogin;}};}
}
复制代码
不知道泥萌有没有发现,这里出现了 Java 代码的身影(不对,好像前面就出现了,算了,我也懒得找了?),不是说好用 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
方法中读取参数 官方文档传送门
- 在使用 apt 的 Module 的 build.gradle 中添加
android {
}kapt {arguments {arg("moduleName", project.name)}
}
复制代码
- 在 Processor 的
init
方法中读取
override fun init(processingEnv: ProcessingEnvironment) {super.init(processingEnv)val moduleName = processingEnv.options["moduleName"]
}
复制代码
到这里,我们完成了路由信息解析和创建 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 {@Overridepublic 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 框架(上):路由拦截机制」
总结
本文是 手撸一个 Router 框架 的上篇,主要讲了 APT 在 Kotlin 环境下的使用,并实现了一个完整的 APT 框架。小弟资历有限,如果那哪里说得不对,还望各位大哥指出?
如果觉得本文对你有帮助,还请不吝赐赞?
转载于:https://juejin.im/post/5d611b505188257a78382701
手撸一个 Router 框架(上):熟悉 APT相关推荐
- 手撸一个RPC框架——傻瓜式教程(一)
RPC框架-傻瓜式教程(一) 前言,太久没写博客了,有点手生,总结一下自己对RPC框架的学习过程 首先我们知道RPC的全名是,全程服务调用,我们用它来做什么,简单地说就是客户端通过接口调用服务端的函数 ...
- 很多小伙伴不太了解ORM框架的底层原理,这不,冰河带你10分钟手撸一个极简版ORM框架(赶快收藏吧)
大家好,我是冰河~~ 最近很多小伙伴对ORM框架的实现很感兴趣,不少读者在冰河的微信上问:冰河,你知道ORM框架是如何实现的吗?比如像MyBatis和Hibernate这种ORM框架,它们是如何实现的 ...
- javascript实现图片轮播_手撸一个简易版轮播图(上)
手撸一个简易版轮播图 实现原理,通过控制 swiper-warpper 容器的定位来达到切换图片的效果. 页面布局 简易版轮播图 < > 页面样式 .container{width: 60 ...
- 五分钟,手撸一个Spring容器!
Spring是我们最常用的开源框架,经过多年发展,Spring已经发展成枝繁叶茂的大树,让我们难以窥其全貌. 这节,我们回归Spring的本质,五分钟手撸一个Spring容器,揭开Spring神秘的面 ...
- 五分钟,手撸一个Spring容器
大家好,我是老三,Spring是我们最常用的开源框架,经过多年发展,Spring已经发展成枝繁叶茂的大树,让我们难以窥其全貌. 这节,我们回归Spring的本质,五分钟手撸一个Spring容器,揭开S ...
- 面试官让我手写一个RPC框架
如今,分布式系统大行其道,RPC 有着举足轻重的地位.Dubbo.Thrift.gRpc 等框架各领风骚,学习RPC是新手也是老鸟的必修课.本文带你手撸一个rpc-spring-starter,深入学 ...
- 使用Node.js手撸一个建静态Web服务器,内部CV指南
文章里有全部代码,也可以积分下载 操作步骤如上图 文章结束 话说这个键盘真漂亮~~ 文章目录 使用Node.js手撸一个建静态Web服务器 一.动静态服务器的概念 1.1 静态Web服务器概念 1.2 ...
- 呆呆带你手撸一个思维导图-基础篇
希沃ENOW大前端 公司官网:CVTE(广州视源股份) 团队:CVTE旗下未来教育希沃软件平台中心enow团队 「本文作者:」 前言 你盼世界,我盼望你无bug.Hello 大家好,我是霖呆呆! 哈哈 ...
- 手撸一个在线学习在线教育小程序
最近有小伙伴找小孟开发了一个在线教育的小程序项目. 一,小程序介绍 微信小程序,它的简称是小程序,其英文名称叫做Mini Program,是一种不需要在手机应用商店里面下载就可以在微信平台当中立即使用 ...
- 手撸一个动态数据源的Starter 完整编写一个Starter及融合项目的过程 保姆级教程
手撸一个动态数据源的Starter! 文章目录 手撸一个动态数据源的Starter! 前言 一.准备工作 1,演示 2,项目目录结构 3,POM文件 二.思路 三.编写代码 1,定义核心注解 Ds 2 ...
最新文章
- vue中阻止冒泡事件
- python编程入门p-读书笔记 - 《Python编程:从入门到实践》
- 大林算法计算机控制实验报告,大林算法
- 学习方法之01高效学习方程式,你的学习到底是哪里出了问题
- 首个使用Blazor 技术实现的社区软件 BlazorCommunity 发布
- eclipse如何导入okhttp 2.x源码
- EC-JET喷码机报EC2.01偏转板电压故障
- ccs定义的函数不变色_ccs使用问题及解决办法
- word插入页眉图片
- Rmarkdown 报错:无法打开链接
- 高职计算机基础教案ppt,高职高专计算机基础幻灯片.ppt
- iOS端使用DSA加密
- 第一章:第1章 CRM核心业务介绍--概述,crm架构,公司组织结构,软件开发的生命周期,crm项目的核心业务介绍。...
- vue按照字母表排序
- 期货如何展期(期货合约展期)
- 基于Java科研项目申报管理系统
- html5建议使用,[HTML5]label标签使用以及建议
- 基于quartz开发企业级任务调度应用
- 软驱光碟安装linux系统,无光驱和软驱安装debian的方法
- HCIE(4)——UDP DOS攻击