Kotlin Symbol Processing (KSP) для Kotlin: полное руководство с примерами
1. Что это и зачем нужна
Kotlin Symbol Processing (KSP) — это библиотека и компиляторный плагин для языка Kotlin, разработанная компанией JetBrains. Её основная задача — предоставить разработчикам простой, быстрый и надёжный API для анализа исходного кода Kotlin на этапе компиляции и генерации на его основе нового кода (кодогенерация). KSP является прямой альтернативой старому механизму javax.annotation.processing (Java Annotation Processing), который был создан для Java и имеет ряд ограничений при работе с Kotlin.
Зачем нужна KSP? В современной разработке на Kotlin часто требуется автоматически создавать шаблонный код: DTO-классы, мапперы, Parcelable-реализации, Room-сущности, Moshi-адаптеры и многое другое. KSP позволяет делать это на этапе компиляции, без рефлексии в рантайме, что повышает производительность приложения. В отличие от KAPT (Kotlin Annotation Processing Tool), KSP работает напрямую с AST-деревом Kotlin, минуя промежуточную Java-стадию, что даёт прирост скорости в 2–4 раза. KSP также лучше поддерживает новые языковые конструкции Kotlin (suspend-функции, inline-классы, value-классы) и не требует генерации Java-заглушек.
KSP идеально подходит для создания собственных библиотек кодогенерации, фреймворков (аналогов Dagger, Room, Glide) и инструментов метапрограммирования. Она активно используется в экосистеме Kotlin: Compose, Room, Moshi, Koin, Apollo GraphQL и многих других популярных библиотеках.
2. Установка
KSP устанавливается не через pip или npm, а через систему сборки Gradle. Для Kotlin-проекта потребуется добавить плагин KSP и его версию, соответствующую версии Kotlin.
Точная команда для добавления в build.gradle.kts (корневой проект):
// build.gradle.kts (корневой уровень)
plugins {
id("org.jetbrains.kotlin.jvm") version "1.9.22" apply false
id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false
}
// build.gradle.kts (модуль приложения)
plugins {
id("org.jetbrains.kotlin.jvm")
id("com.google.devtools.ksp")
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.22")
implementation("com.squareup:kotlinpoet:1.16.0") // для генерации кода
ksp("com.example:my-processor:1.0.0") // ваш процессор
}
Для Gradle Groovy (build.gradle):
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.9.22'
id 'com.google.devtools.ksp' version '1.9.22-1.0.17'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.22"
implementation "com.squareup:kotlinpoet:1.16.0"
ksp "com.example:my-processor:1.0.0"
}
Важно: Версия плагина KSP должна строго соответствовать версии Kotlin. Например, для Kotlin 1.9.22 используется KSP 1.9.22-1.0.17. Актуальные версии смотрите на GitHub-релизах KSP.
3. Быстрый старт — минимальный рабочий пример
Создадим простейший процессор, который находит все классы с аннотацией @Builder и генерирует для них Builder-класс.
Шаг 1: Создайте модуль процессора (например, processor)
// processor/build.gradle.kts
plugins {
id("org.jetbrains.kotlin.jvm")
id("com.google.devtools.ksp")
}
dependencies {
implementation("com.squareup:kotlinpoet:1.16.0")
implementation("com.squareup:kotlinpoet-ksp:1.16.0")
implementation("com.google.devtools.ksp:symbol-processing-api:1.9.22-1.0.17")
}
// processor/src/main/kotlin/com/example/BuilderProcessor.kt
package com.example
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ksp.toClassName
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Builder
class BuilderProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return BuilderProcessor(environment.codeGenerator)
}
}
class BuilderProcessor(private val codeGenerator: CodeGenerator) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation("com.example.Builder")
symbols.filterIsInstance<KSClassDeclaration>().forEach { classDecl ->
generateBuilder(classDecl)
}
return emptyList()
}
private fun generateBuilder(classDecl: KSClassDeclaration) {
val className = classDecl.simpleName.asString()
val packageName = classDecl.packageName.asString()
val builderClassName = "${className}Builder"
val props = classDecl.getAllProperties().map { prop ->
prop.name to prop.type.resolve().toClassName()
}
val builderClass = TypeSpec.classBuilder(builderClassName)
.primaryConstructor(
FunSpec.constructorBuilder()
.addParameters(props.map { (name, type) ->
ParameterSpec.builder(name, type).build()
})
.build()
)
.addProperties(props.map { (name, type) ->
PropertySpec.builder(name, type)
.initializer(name)
.build()
})
.addFunction(
FunSpec.builder("build")
.returns(classDecl.toClassName())
.addStatement("return %T(%L)", classDecl.toClassName(), props.joinToString(", ") { it.first })
.build()
)
.build()
val file = FileSpec.builder(packageName, builderClassName)
.addType(builderClass)
.build()
file.writeTo(codeGenerator, Dependencies(false))
}
}
Шаг 2: Используйте процессор в основном модуле
// app/src/main/kotlin/com/example/User.kt
package com.example
@Builder
data class User(val name: String, val age: Int)
// После компиляции будет сгенерирован:
// public class UserBuilder(val name: String, val age: Int) {
// fun build(): User = User(name, age)
// }
fun main() {
val user = UserBuilder("Alice", 30).build()
println(user) // User(name=Alice, age=30)
}
4. Основные методы/функции/классы — минимум 8 штук
4.1 SymbolProcessor.process(resolver: Resolver): List<KSAnnotated>
Сигнатура: fun process(resolver: Resolver): List<KSAnnotated>
Что делает: Главный метод процессора. Вызывается компилятором для каждого раунда обработки. Получает объект Resolver для доступа к символам исходного кода. Возвращает список символов, которые не удалось обработать (для повторной обработки в следующем раунде).
Пример:
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation("com.example.MyAnnotation")
val deferred = mutableListOf<KSAnnotated>()
symbols.forEach { symbol ->
if (symbol is KSClassDeclaration) {
try {
processClass(symbol)
} catch (e: Exception) {
deferred.add(symbol) // откладываем на следующий раунд
}
}
}
return deferred
}
4.2 Resolver.getSymbolsWithAnnotation(annotationName: String): Sequence<KSAnnotated>
Сигнатура: fun getSymbolsWithAnnotation(annotationName: String): Sequence<KSAnnotated>
Что делает: Возвращает последовательность всех символов (классов, функций, свойств), аннотированных указанной аннотацией. Это основной способ найти цели для кодогенерации.
Пример:
val symbols = resolver.getSymbolsWithAnnotation("com.example.JsonSerializable")
symbols.filterIsInstance<KSClassDeclaration>().forEach { classDecl ->
println("Found class: ${classDecl.simpleName.asString()}")
}
4.3 KSClassDeclaration.getAllProperties(): Sequence<KSPropertyDeclaration>
Сигнатура: fun getAllProperties(): Sequence<KSPropertyDeclaration>
Что делает: Возвращает все свойства (поля) класса, включая унаследованные. Полезно для генерации Builder'ов, DTO-мапперов и т.д.
Пример:
val classDecl = resolver.getClassDeclarationByName("com.example.User")
classDecl?.getAllProperties()?.forEach { prop ->
println("Property: ${prop.simpleName.asString()} : ${prop.type.resolve().toClassName()}")
}
4.4 KSPropertyDeclaration.type: KSTypeReference
Сигнатура: val type: KSTypeReference
Что делает: Возвращает ссылку на тип свойства. Через resolve() можно получить KSType, а затем преобразовать в ClassName из KotlinPoet.
Пример:
val typeRef = prop.type
val resolvedType = typeRef.resolve()
val className = resolvedType.toClassName() // для KotlinPoet
println("Type: $className")
4.5 KSFunctionDeclaration.parameters: List<KSValueParameter>
Сигнатура: val parameters: List<KSValueParameter>
Что делает: Возвращает список параметров функции. Полезно для генерации фабрик, делегатов или обёрток.
Пример:
val functionDecl = resolver.getFunctionDeclarations().first()
functionDecl.parameters.forEach { param ->
println("Param: ${param.name?.asString()} : ${param.type.resolve().toClassName()}")
}
4.6 CodeGenerator.createNewFile(dependencies: Dependencies, packageName: String, fileName: String, extensionName: String): OutputStream
Сигнатура: fun createNewFile(dependencies: Dependencies, packageName: String, fileName: String, extensionName: String): OutputStream
Что делает: Создаёт новый файл для сгенерированного кода. Обычно используется вместе с KotlinPoet для записи .kt-файлов.
Пример:
val file = codeGenerator.createNewFile(
dependencies = Dependencies(false),
packageName = "com.example.generated",
fileName = "MyGeneratedClass",
extensionName = "kt"
)
file.bufferedWriter().use { writer ->
writer.write("package com.example.generated\nclass MyGeneratedClass")
}
4.7 KSAnnotation.annotationType: KSTypeReference
Сигнатура: val annotationType: KSTypeReference
Что делает: Возвращает тип аннотации. Позволяет получить аргументы аннотации через arguments.
Пример:
val annotation = classDecl.annotations.first { it.shortName.asString() == "Builder" }
val annotationType = annotation.annotationType.resolve().toClassName()
annotation.arguments.forEach { arg ->
println("Argument: ${arg.name?.asString()} = ${arg.value}")