Kotlin Symbol Processing (KSP) für Kotlin: Komplettguide mit Beispielen
1. Was ist KSP und warum wird es benötigt?
Kotlin Symbol Processing (KSP) ist eine Bibliothek und ein Compiler-Plugin für die Programmiersprache Kotlin, entwickelt von JetBrains. Seine Hauptaufgabe besteht darin, Entwicklern eine einfache, schnelle und zuverlässige API zur Analyse von Kotlin-Quellcode während der Kompilierung und zur Generierung von neuem Code (Codegenerierung) auf dieser Basis bereitzustellen. KSP ist eine direkte Alternative zum älteren Mechanismus javax.annotation.processing (Java Annotation Processing), der für Java entwickelt wurde und bei der Arbeit mit Kotlin einige Einschränkungen aufweist.
Warum wird KSP benötigt? In der modernen Kotlin-Entwicklung müssen oft Boilerplate-Code automatisch erstellt werden: DTO-Klassen, Mapper, Parcelable-Implementierungen, Room-Entitäten, Moshi-Adapter und vieles mehr. KSP ermöglicht dies während der Kompilierung, ohne Reflektion zur Laufzeit, was die App-Leistung verbessert. Im Gegensatz zu KAPT (Kotlin Annotation Processing Tool) arbeitet KSP direkt mit dem AST-Baum von Kotlin, unter Umgehung der Java-Zwischenstufe, was eine 2- bis 4-fache Geschwindigkeitssteigerung bringt. KSP unterstützt auch neuere Kotlin-Sprachkonstrukte (suspend-Funktionen, inline-Klassen, value-Klassen) besser und erfordert keine Generierung von Java-Stubs.
KSP ist ideal für die Erstellung eigener Codegenerierungsbibliotheken, Frameworks (Analogien zu Dagger, Room, Glide) und Metaprogrammierungs-Tools. Es wird aktiv im Kotlin-Ökosystem eingesetzt: Compose, Room, Moshi, Koin, Apollo GraphQL und vielen anderen beliebten Bibliotheken.
2. Installation
KSP wird nicht über pip oder npm installiert, sondern über das Gradle-Build-System. Für ein Kotlin-Projekt muss das KSP-Plugin und seine Version, die der Kotlin-Version entspricht, hinzugefügt werden.
Der genaue Befehl zum Hinzufügen in build.gradle.kts (Root-Projekt):
// build.gradle.kts (Root-Ebene)
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 (App-Modul)
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") // für Codegenerierung
ksp("com.example:my-processor:1.0.0") // Ihr Prozessor
}
Für Gradle Groovy (build.gradle):
// build.gradle (Root-Ebene)
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 (App-Modul)
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' // für Codegenerierung
ksp 'com.example:my-processor:1.0.0' // Ihr Prozessor
}
3. Grundlegende Methoden und API
Die KSP-API dreht sich um das Konzept der Symbole und Resolver. Ein Symbol repräsentiert ein deklariertes Element im Code (Klasse, Funktion, Eigenschaft, etc.). Der Resolver ermöglicht den Zugriff auf alle Symbole des Projekts und der Abhängigkeiten.
Die wichtigsten Schnittstellen und Methoden sind:
SymbolProcessor– Die Hauptschnittstelle, die Sie implementieren müssen. Sie enthält die Methodeprocess(resolver: Resolver), die bei jeder Kompilierungsrunde aufgerufen wird.Resolver– Bietet Methoden zum Abfragen von Symbolen:getSymbolsWithAnnotation(annotationName: String)– Gibt alle Symbole zurück, die mit einer bestimmten Annotation versehen sind.getAllFiles()– Gibt alle Code-Dateien im Projekt zurück.getClassDeclarationByName(name: ClassName)– Findet eine Klassendeklaration anhand ihres Namens.
KSFile– Repräsentiert eine Kotlin-Quelldatei.KSClassDeclaration– Repräsentiert eine Klassen-, Interface- oder Objektdeklaration.KSFunctionDeclaration– Repräsentiert eine Funktionsdeklaration.KSPropertyDeclaration– Repräsentiert eine Eigenschaftsdeklaration (Variable).KSAnnotation– Repräsentiert eine Annotation.
Beispiel: Ein einfacher Prozessor
Hier ist ein minimales Beispiel eines KSP-Prozessors, der alle mit @AutoGenerate annotierten Klassen findet und deren Namen ausgibt:
// AutoGenerate.kt
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class AutoGenerate
// AutoGenerateProcessor.kt
class AutoGenerateProcessor : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation("com.example.AutoGenerate")
val ret = mutableListOf<KSAnnotated>()
symbols.filterIsInstance<KSClassDeclaration>().forEach { symbol ->
println("Gefundene Klasse: ${symbol.simpleName.asString()}")
// Hier könnte die Codegenerierung erfolgen
}
return ret // Leere Liste = keine Fehler
}
}
// AutoGenerateProcessorProvider.kt
class AutoGenerateProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return AutoGenerateProcessor()
}
}
SymbolProcessorProvider zu registrieren. Erstellen Sie dazu die Datei META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider und fügen Sie den vollqualifizierten Klassennamen Ihres Providers ein (z.B. com.example.AutoGenerateProcessorProvider).
4. Codegenerierung mit KotlinPoet
Für die eigentliche Codegenerierung wird in der Regel die Bibliothek KotlinPoet von Square verwendet. Sie bietet eine typsichere und lesbare API zur Erstellung von Kotlin-Quellcode.
Hier ist ein Beispiel, wie Sie mit KotlinPoet eine einfache Klasse generieren können:
import com.squareup.kotlinpoet.*
val fileSpec = FileSpec.builder("com.example.generated", "GeneratedClass")
.addType(
TypeSpec.classBuilder("GeneratedClass")
.addProperty(
PropertySpec.builder("name", String::class)
.initializer("\"Standardname\"")
.build()
)
.addFunction(
FunSpec.builder("greet")
.addParameter("greeting", String::class)
.returns(String::class)
.addStatement("return \"\$greeting, \$name!\"")
.build()
)
.build()
)
.build()
fileSpec.writeTo(System.out) // Gibt den generierten Code auf der Konsole aus
Dieser Code generiert die folgende Kotlin-Datei:
package com.example.generated
class GeneratedClass {
val name: String = "Standardname"
fun greet(greeting: String): String {
return "$greeting, $name!"
}
}
5. Praktisches Beispiel: DTO-Generator
Lassen Sie uns einen vollständigen Prozessor erstellen, der aus einer Datenklasse mit einer speziellen Annotation automatisch eine DTO-Klasse (Data Transfer Object) generiert. Dies ist ein häufiges Szenario in der Android-Entwicklung mit Retrofit oder ähnlichen Bibliotheken.
Schritt 1: Annotation definieren
// annotations/Dto.kt
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Dto(val suffix: String = "Dto")
Schritt 2: Prozessor implementieren
// processor/DtoProcessor.kt
class DtoProcessor(private val codeGenerator: CodeGenerator) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation("com.example.annotations.Dto")
symbols.filterIsInstance<KSClassDeclaration>().forEach { classDeclaration ->
generateDto(classDeclaration)
}
return emptyList()
}
private fun generateDto(classDeclaration: KSClassDeclaration) {
val packageName = classDeclaration.packageName.asString()
val className = classDeclaration.simpleName.asString()
val dtoAnnotation = classDeclaration.annotations.find {
it.annotationType.resolve().declaration.qualifiedName?.asString() == "com.example.annotations.Dto"
}
val suffix = dtoAnnotation?.arguments?.find { it.name?.asString() == "suffix" }?.value as? String ?: "Dto"
val dtoClassName = "$className$suffix"
val properties = classDeclaration.getAllProperties().map { property ->
val propertyName = property.simpleName.asString()
val propertyType = property.type.resolve().toClassName()
PropertySpec.builder(propertyName, propertyType).build()
}
val dtoClass = TypeSpec.classBuilder(dtoClassName)
.addProperties(properties)
.addModifiers(KModifier.DATA)
.build()
val file = FileSpec.builder(packageName, dtoClassName)
.addType(dtoClass)
.build()
// Code in die generierte Quelle schreiben
codeGenerator.createNewFile(
Dependencies(false, classDeclaration.containingFile!!),
packageName,
dtoClassName
).use { outputStream ->
outputStream.write(file.toString().toByteArray())
}
}
}
// processor/DtoProcessorProvider.kt
class DtoProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return DtoProcessor(environment.codeGenerator)
}
}
Schritt 3: Verwendung im Projekt
// models/User.kt
@Dto
data class User(
val id: Int,
val name: String,
val email: String
)
// Nach der Kompilierung wird automatisch generiert:
// models/UserDto.kt
data class UserDto(
val id: Int,
val name: String,
val email: String
)
6. Fehlerbehandlung und Debugging
Bei der Arbeit mit KSP können verschiedene Probleme auftreten. Hier sind die häufigsten und ihre Lösungen:
| Problem | Ursache | Lösung |
|---|---|---|
| Prozessor wird nicht ausgeführt | Falsche Registrierung in META-INF/services |
Überprüfen Sie den Dateipfad und den Inhalt der Service-Datei |
| Kompilierungsfehler: "Unresolved reference: ksp" | KSP-Plugin nicht korrekt angewendet | Stellen Sie sicher, dass das Plugin in der build.gradle.kts des Moduls angewendet wird |
| Generierte Dateien werden nicht gefunden | Falscher Ausgabepfad oder fehlende Abhängigkeit | Überprüfen Sie die build/generated/ksp/ Verzeichnisse und fügen Sie ksp(project(":processor")) hinzu |
| Langsame Kompilierung | Zu viele Symbole in einer Runde verarbeitet | Verwenden Sie Dependencies(true, ...) für inkrementelle Verarbeitung |
logger.warn("Nachricht") in Ihren Prozessor ein. Die Logs erscheinen während der Kompilierung in der Gradle-Konsole. Für detailliertere Einblicke verwenden Sie --info oder --debug beim Gradle-Build.
7. Fazit
Kotlin Symbol Processing (KSP) ist ein mächtiges Werkzeug für die Kotlin-Entwicklung, das die Codegenerierung auf Compiler-Ebene ermöglicht. Es ist schneller, flexibler und besser an Kotlin angepasst als der ältere KAPT-Ansatz. Mit KSP können Sie:
- Boilerplate-Code automatisch generieren
- Eigene Annotation-basierte Frameworks erstellen
- Die Leistung Ihrer Builds verbessern (2-4x schneller als KAPT)
- Moderne Kotlin-Features wie suspend-Funktionen und value-Klassen nutzen
Die Kombination von KSP mit KotlinPoet bietet eine elegante und typsichere Möglichkeit, Kotlin-Code zu generieren. Ob Sie nun DTOs, Mapper, Parcelable-Implementierungen oder ganze API-Clients generieren möchten – KSP ist die richtige Wahl für Ihr nächstes Kotlin-Projekt.
Beginnen Sie noch heute mit der Integration von KSP in Ihr Projekt und erleben Sie die Vorteile der Compiler-basierten Codegenerierung!
Auch in der Bibliothek
Datenbankmigrationen in Node.js: Überblick über Tools und Beispiele
Symfony/Cache für PHP: Komplette Anleitung mit Beispielen
Firebase Android SDK für Kotlin: Komplettguide mit Beispielen
CLI-Framework für Swift: Erstellung von Konsolen-Dienstprogrammen für iOS/macOS