kotlin KSP小试牛刀

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类了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容