目前業界已經有不少成熟的路由框架,最著名的應該是 ARouter,那麼咱們今天爲何還要從新造輪子呢? 我我的以爲有如下緣由:html
進入正題前,咱們先預告一下接下來會涉及到的知識點java
使用註解處理器,通常須要3個 Module:android
新建 Java Modulegit
建立 Router 註解github
/**
* 標記路由信息,僅支持 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 中的連接形式,不過要注意對於特殊字符的轉義api
舉個栗子,要支持以下連接bash
https://www.wanandroid.com/blog/show/2657
複製代碼
參數文章 ID 是 2657,那麼 path 就能夠寫爲app
/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)
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 代碼的身影(不對,好像前面就出現了,算了,我也懶得找了😓),不是說好用 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
方法中讀取參數 官方文檔傳送門
android {
}
kapt {
arguments {
arg("moduleName", project.name)
}
}
複製代碼
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 {
@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 框架(上):路由攔截機制」
本文是 手擼一個 Router 框架 的上篇,主要講了 APT 在 Kotlin 環境下的使用,並實現了一個完整的 APT 框架。小弟資歷有限,若是那哪裏說得不對,還望各位大哥指出🙏
若是以爲本文對你有幫助,還請不吝賜贊😄