手擼一個 Router 框架(上):熟悉 APT

前言

目前業界已經有不少成熟的路由框架,最著名的應該是 ARouter,那麼咱們今天爲何還要從新造輪子呢? 我我的以爲有如下緣由:html

  1. ARouter 過於強大,不少功能咱們不必定用得上,並且不必定適合咱們的項目,本身擼一個,能夠在知足項目需求的狀況下,功能上去繁就簡。
  2. 實踐出真知,我想這也是不少開發者重複造輪子的主要緣由吧。咱們常常閱讀許多大牛對於優秀框架的剖析,但那也只是大牛的理解,咱們本身的呢?
  3. 便於排查問題。使用開源框架遇到問題通常會耗費更多的排查時間,由於咱們對源碼「不夠熟悉」,而本身擼的通常均可以快速定位問題。

準備

進入正題前,咱們先預告一下接下來會涉及到的知識點java

  1. Kotlin,本文代碼主要基於 Kotlin 語言編寫,相信你們都知道 Kotlin 的好處了吧?
  2. APT,即 Annotation Processing Tool,註解處理器,用於在編譯時掃描和處理註解,即解析和保存路由信息。
  3. 攔截器機制,衆所周知 OKHTTP 的攔截器機制是十分強大的,咱們也將參考並沿用這套機制。

正文

使用註解處理器,通常須要3個 Module:android

  1. annotation - 包含註解類,提供給 compiler、api 和 app 使用
  2. compiler - 編譯器,即註解處理器,在打包時處理註解
  3. api - 提供路由的 api 接口

註解 Module

新建 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
)
複製代碼

提供如下參數正則表達式

  • value: 路由路徑,即 path,爲了方便這裏直接用 value,不用顯式指定參數名
  • scheme、host: 這兩個便是字面意思,提供默認值,通常使用默認值便可
  • needLogin: 用於登陸攔截,攔截機制下篇會講到

注意一點,這裏爲了便於匹配,這裏 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() {
    ......
}
複製代碼

註解處理 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: 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 方法中讀取參數 官方文檔傳送門

  • 在使用 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 {
  @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 框架。小弟資歷有限,若是那哪裏說得不對,還望各位大哥指出🙏

若是以爲本文對你有幫助,還請不吝賜贊😄

相關文章
相關標籤/搜索