kotlin KSP小试牛刀

作者: 魂狩 | 来源:发表于2022-07-22 11:02 被阅读0次

    KSP是什么

    KSP,全称Kotlin Symbol Processing,我的翻译是kotlin符号处理程序,是KAPT(Kotlin Annotation Processor Tool)的下一代替代品。功能和KAPT差不多,也是方便处理注解、生成代码的,但是性能会高很多。

    开始尝试KSP

    不管是官网还是内网,关于KSP的使用方法都很模糊。官网提供了各种API文档,也提供了最基本的一个示例,但是在示例里面只是单纯记录了信息,没有保存下来,没有什么用。内网也只是单纯翻译官方文档,没有价值。于是只能阅读别人的代码,摸索写下一个简单的组件。

    初始化项目

    创建一下项目,新开3个模块,分别是ann、web、ksp。
    在根目录下的build.gradle.kts里设置一下仓库地址:

    subprojects {
        repositories {
            maven("https://maven.aliyun.com/repository/central")
            maven("https://maven.aliyun.com/repository/spring")
        }
    }
    
    buildscript {
        repositories {
            maven("https://maven.aliyun.com/repository/central")
            maven("https://maven.aliyun.com/repository/spring")
        }
        dependencies {
            classpath(kotlin("gradle-plugin", version = "1.7.10"))
        }
    }
    

    先写ann,这是放注解的,在build.gradle.kts里指定使用kotlin即可:

    plugins {
        kotlin("jvm")
    }
    

    然后创建一下注解Woo:

    package com.small.ann
    
    annotation class Woo(val right:String)
    

    ann模块就完成了。

    ksp模块的build.gradle.kts如下:

    plugins {
        kotlin("jvm")
    }
    dependencies {
        implementation("com.google.devtools.ksp:symbol-processing-api:1.7.10-1.0.6")
        implementation("org.jetbrains.kotlin:kotlin-reflect")
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
        implementation("org.springframework:spring-web:5.3.21")
        implementation(project(":ann"))
    }
    tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
        kotlinOptions {
            freeCompilerArgs = listOf("-Xjsr305=strict")
            jvmTarget = "17"
        }
    }
    

    注意:我使用的kotlin版本为1.7.10,ksp版本为1.0.6,所以是com.google.devtools.ksp:symbol-processing-api:1.7.10-1.0.6。使用前需要确定ksp和kotlin的对应关系。

    web模块的build.gradle.kts如下:

    import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
    
    plugins {
        id("org.springframework.boot") version "2.7.1"
        id("io.spring.dependency-management") version "1.0.11.RELEASE"
        kotlin("jvm")
        kotlin("plugin.spring") version "1.7.10"
        id("com.google.devtools.ksp") version "1.7.10-1.0.6"
    }
    
    group = "com.example"
    version = "0.0.1-SNAPSHOT"
    java.sourceCompatibility = JavaVersion.VERSION_17
    
    dependencies {
        implementation("org.springframework.boot:spring-boot-starter-web")
        implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
        implementation("org.jetbrains.kotlin:kotlin-reflect")
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
        testImplementation("org.springframework.boot:spring-boot-starter-test")
        ksp(project(":kapt"))
        implementation(project(":ann"))
    }
    
    tasks.withType<KotlinCompile> {
        kotlinOptions {
            freeCompilerArgs = listOf("-Xjsr305=strict")
            jvmTarget = "17"
        }
    }
    kotlin {//将生成出来的文件夹加到代码源中,让IDE识别
        sourceSets.main {
            kotlin.srcDir("build/generated/ksp/main/kotlin")
        }
    }
    tasks.withType<Test> {
        useJUnitPlatform()
    }
    

    web模块是从模板创建的,关键点是使用id("com.google.devtools.ksp") version "1.7.10-1.0.6"插件,并指定ksp依赖ksp(project(":kapt")),然后将生成出来的文件夹加到代码源中,让IDE知道。

    完成web模块

    web模块除了SpringBootApplication外就一个控制器文件,如下:

    package com.example.demo.server
    
    import com.small.kapt.Xixi
    import com.small.ann.Woo
    import org.springframework.web.bind.annotation.GetMapping
    import org.springframework.web.bind.annotation.RestController
    
    @RestController
    class Blank {
        @GetMapping("/good")
        @Woo("nice")
        fun hello():String{
            println(Xixi.nice)
            return "hi"
        }
    }
    

    其中com.small.kapt.Xixi类是生成出来的代码,后面再说。

    完成ksp模块

    现在开始进入正题。KSP主体有两个类,SymbolProcessorProviderSymbolProcessor。前面是注册器,后面是具体执行的。注册器很简单,创建一个自己的SymbolProcessor即可:

    package com.small.kapt
    
    import com.google.devtools.ksp.processing.SymbolProcessor
    import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
    import com.google.devtools.ksp.processing.SymbolProcessorProvider
    
    class SmallProvider:SymbolProcessorProvider {
        override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
            return SmallProcessor(environment)
        }
    }
    

    然后需要在resources文件夹创建META-INF/services文件夹,并在services文件夹创建文件com.google.devtools.ksp.processing.SymbolProcessorProvider。内容为com.small.kapt.SmallProvider,即创建的SymbolProcessor完整路径。

    具体工作是在SymbolProcessor中执行的,代码如下:

    package com.small.kapt
    
    import com.google.devtools.ksp.KspExperimental
    import com.google.devtools.ksp.containingFile
    import com.google.devtools.ksp.getAnnotationsByType
    import com.google.devtools.ksp.isAnnotationPresent
    import com.google.devtools.ksp.processing.Dependencies
    import com.google.devtools.ksp.processing.Resolver
    import com.google.devtools.ksp.processing.SymbolProcessor
    import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
    import com.google.devtools.ksp.symbol.*
    import com.small.ann.Woo
    import org.springframework.web.bind.annotation.GetMapping
    import org.springframework.web.bind.annotation.RequestMapping
    
    class SmallProcessor(private val environment: SymbolProcessorEnvironment): SymbolProcessor {
        @OptIn(KspExperimental::class)
        override fun process(resolver: Resolver): List<KSAnnotated> {
            val path2right=HashMap<String,String>()
            val dep=ArrayList<KSFile>()
            resolver.getSymbolsWithAnnotation(Woo::class.java.name).forEach {
                dep.add(it.containingFile!!)
                val right=it.getAnnotationsByType(Woo::class).first().right
                val path=it.getAnnotationsByType(GetMapping::class).first().value[0]
                path2right[path] = right
            }
            if (path2right.isNotEmpty()){
                val sb=StringBuilder("package com.small.kapt\nobject Xixi{\n")
                path2right.forEach {(path,right) ->
                    sb.appendLine("const val $right=\"$path\"")
                }
                sb.append("}")
                environment.codeGenerator.createNewFile(Dependencies(false,*dep.toTypedArray()),"com.small.kapt","Xixi")
                    .write(sb.toString().encodeToByteArray())
            }
            return emptyList()
        }
    }
    

    采集到的数据都存在path2right中,同时把对应依赖文件存在dep中。遍历完成后,使用codeGenerator创建文件,传入依赖文件列表,这样方便缓存。当依赖文件列表中的文件没有改动的时候,就会跳过KSP过程,节约时间,出现的信息会是:web:kspKotlin UP-TO-DATE。只有文件有改动才会触发重新生成文件。可以把依赖列表改成Dependencies.ALL_FILES来强制每次生成。

    这里是直接通过拼接字符串的方式实现的,对于稍微复杂一点的内容,建议使用kotlinPoet,创建文件更方便。build web项目后,生成的代码如下:

    package com.small.kapt
    object Xixi{
    const val nice="/good"
    }
    

    位于build/generated/ksp/main/kotlin,这就是web模块中使用的Xixi类了。

    相关文章

      网友评论

        本文标题:kotlin KSP小试牛刀

        本文链接:https://www.haomeiwen.com/subject/dixpirtx.html