iT邦幫忙

2022 iThome 鐵人賽

DAY 26
0
Mobile Development

Kotlin 全面啟動 系列 第 26

[Kotlin 全面啟動] KSP II

  • 分享至 

  • xImage
  •  

有了昨天的 KSP 基礎結構後,今天就讓我們著重在於 Processor module 裡的邏輯!

如果還沒看過上一篇的話,請往這裡去:
https://ithelp.ithome.com.tw/articles/10306377

Processor

既然我們把這個 module 叫做 Processor module,那最重要的功能就是 process 這件事情了,KSP 所提供的 SymbolProcessor 就是處理這件事情的物件:

interface SymbolProcessor {
     fun process(resolver: Resolver): List<KSAnnotated> //main logic
     fun finish() {}
     fun onError() {} 
}

SymbolProcessor 作為統一的接口,除了 finishonError 二個不同 terminate 的 callback 之外,最重要的就是 process 這個 function 了,我們會在這個 callback 裡處理大部分的邏輯,而 Resolver 這個參數也是個 interface,它的實作是由系統提供所以我們不用煩惱,而它的 function 也很多所以我們就不一一列出來,總之可以把它想成是一個取得程式碼資訊的中介層。

雖然說 Resolver 有很多 function,但通常我還是會在幫它加上下面的 extension function,比較容易過濾出有我所關注的 annotation 的物件。

fun Resolver.getSymbols(cls: KClass<*>) =
    this.getSymbolsWithAnnotation(cls.qualifiedName.orEmpty())
        .filterIsInstance<KSClassDeclaration>()
        .filter(KSNode::validate)

官網上還列了很多常用的 extension,有興趣的話也可以參考:
https://kotlinlang.org/docs/ksp-examples.html

Kotlin Symbols

上面的範例很多 KS 開頭的物件對吧,有 KS 前綴的 class 通常代表著一個程式碼物件,以 KSFile 為首可以展開以下的結構:

KSFile
  packageName: KSName
  fileName: String
  annotations: List<KSAnnotation>  (File annotations)
  declarations: List<KSDeclaration>
    KSClassDeclaration // class, interface, object
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      classKind: ClassKind
      primaryConstructor: KSFunctionDeclaration
      superTypes: List<KSTypeReference>
      // contains inner classes, member functions, properties, etc.
      declarations: List<KSDeclaration>
    KSFunctionDeclaration // top level function
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      functionKind: FunctionKind
      extensionReceiver: KSTypeReference?
      returnType: KSTypeReference
      parameters: List<KSValueParameter>
      // contains local classes, local functions, local variables, etc.
      declarations: List<KSDeclaration>
    KSPropertyDeclaration // global variable
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      extensionReceiver: KSTypeReference?
      type: KSTypeReference
      getter: KSPropertyGetter
        returnType: KSTypeReference
      setter: KSPropertySetter
        parameter: KSValueParameter

藉由 Resolver 解析出的 Kotlin Symbol 所蘊含的資訊,我們就可以了解程式碼的結構,進而使用這些資訊建立我們想要自動產生的 kt 檔案,但現在問題來了,這些 SymbolProcessor 是誰會來呼叫呢?

SymbolProcessorProvider

KSP 裡負責建立 SymbolProcessor 的是 SymbolProcessorProvider,它唯一的任務就是建立這個 processor,以下是這個 SAM 的定義:

fun interface SymbolProcessorProvider {
    /**
     * Called by Kotlin Symbol Processing to create the processor.
     */
    fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}

SymbolProcessorEnvironment 裡有著各種 parse 時需要的資訊以及物件,原始碼如下:

class SymbolProcessorEnvironment(
    /**
     * passed from command line, Gradle, etc.
     */
    val options: Map<String, String>,

    /**
     * language version of compilation environment.
     */
    val kotlinVersion: KotlinVersion,

    /**
     * creates managed files.
     */
    val codeGenerator: CodeGenerator,

    /**
     * for logging to build output.
     */
    val logger: KSPLogger,

    /**
     * Kotlin API version of compilation environment.
     */
    val apiVersion: KotlinVersion,

    /**
     * Kotlin compiler version of compilation environment.
     */
    val compilerVersion: KotlinVersion,

    /**
     * Information of target platforms
     *
     * There can be multiple platforms in a metadata compilation.
     */
    val platforms: List<PlatformInfo>,
)

而如果你只需要 codeGeneratorlogger 的話,一個具體實作可能長得像這樣:

class BuilderProcessorProvider : SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return BuilderProcessor(environment.codeGenerator, environment.logger)
    }
}

但問題又出現了,系統怎麼知道該建立這個 SymbolProcessorProvider 來得到 SymbolProcessor 呢?

這個問題跟我們寫 android 的時候定義的 Activity 很像,Activity 作為一般的 class 可以在任意地方定義,但要想被系統建立就必須在 AndroidManifest 裡宣告,而這個邏輯不論是 Annotation Processing 或 KSP 也是一樣的,必須要在一個固定的地方宣告,以 KSP 來說就必須定義在 src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider 裡:

src
├── java 
│   └── ...
└── resources
    └── META-INF
        └── services
            └── com.google.devtools.ksp.processing.SymbolProcessorProvider

而在這個檔案裡呢,只要把一個一個的 SymbolProcessorProvider,包括 package 一行行列出來就可以了,範例如下:

com.jintin.kfactory.processor.BuilderProcessorProvider

KotlinPoet

介紹完 Processor 的結構後,讓我們再回頭看看產生檔案這塊,如果你有新增 KotlinPoet 以及 KotlinPoet 的 KSP extension 的話,那寫檔案這塊應該會蠻愉快的,如果沒有的話,可以先加上這些 dependency:

dependencies {
    implementation 'com.squareup:kotlinpoet:1.11.0'
    implementation 'com.squareup:kotlinpoet-ksp:1.12.0'
}

有了以上設定,我們只要建立 FileSpec 後呼叫 writeTo 就可以了:

fileSpec.writeTo(codeGenerator, Dependencies(true))

而且 KotlinPoet 也提供 Kotlin Symbol 直接轉換成 KotlinPoet 物件的能力,比如 toTypeNametoClassNametoKModifier 等都非常實用,尤其當你的物件有很多層的 generic type 的時候,你會非常感謝不用自己手刻遞迴來處理。

心得

KSP 提供了一套標準讓我們可以在 compile 的時候讀取程式碼資訊,並動態加入更多程式碼一起 compile,讓我們可以把重複性的程式碼自動產生,真的是非常棒的工具呢。寫個程式讓程式能自己寫程式真的是非常過癮的一件事情,期待大家也能仔細想想自己的 domain 有哪些應用場景,相信實際寫個一二次就會得心應手了!


上一篇
[Kotlin 全面啟動] KSP
下一篇
[Kotlin 全面啟動] KSP III
系列文
Kotlin 全面啟動 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言