Understanding apt, kapt, and ksp: Annotation Processing and Kotlin Symbol Processing for Code Generation
This article explains the differences between Java's Annotation Processing Tool (apt), Kotlin's annotation processor (kapt), and the newer Kotlin Symbol Processing (ksp) framework, illustrating their architectures, usage patterns, and code‑generation capabilities with practical examples for Android development.
When developers first encounter meta‑programming, they often start with APT (Annotation Processing Tool), a javac utility that scans and processes annotations at compile time, commonly used in frameworks like ARouter and Dagger to generate template code.
With Kotlin’s rise, KAPT was introduced to provide similar annotation‑processing capabilities for Kotlin, but its compilation speed is usually lower than APT because Kotlin source is first compiled into Java stubs before being processed.
Kotlin Compiler Plugin (KCP) adds a compilation‑stage hook for modifying Kotlin symbols, though it remains complex and beta. Building on KCP, KSP (Kotlin Symbol Processing) streamlines code generation, reducing compilation time by over 20%.
A typical KSP plugin consists of three parts: SymbolProcessorProvider , SymbolProcessor , and custom processing logic.
SymbolProcessorProvider
/**
* [SymbolProcessorProvider] is the interface used by plugins to integrate into Kotlin Symbol Processing.
*/
fun interface SymbolProcessorProvider {
/**
* Called by Kotlin Symbol Processing to create the processor.
*/
fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}The provider supplies a SymbolProcessorEnvironment containing options, Kotlin version, a CodeGenerator , a logger, and platform information.
class SymbolProcessorEnvironment(
val options: Map
,
val kotlinVersion: KotlinVersion,
val codeGenerator: CodeGenerator,
val logger: KSPLogger,
val apiVersion: KotlinVersion,
val compilerVersion: KotlinVersion,
val platforms: List
,
) {
// Compatibility constructor for older KSP versions
constructor(
options: Map
,
kotlinVersion: KotlinVersion,
codeGenerator: CodeGenerator,
logger: KSPLogger
) : this(
options,
kotlinVersion,
codeGenerator,
logger,
kotlinVersion,
kotlinVersion,
emptyList()
)
}Implementations register the provider via the SPI mechanism by creating a file com.google.devtools.ksp.processing.SymbolProcessorProvider under resources/META-INF/services .
SymbolProcessor
interface SymbolProcessor {
/**
* Called by Kotlin Symbol Processing to run the processing task.
* @param resolver provides access to compiler symbols.
* @return A list of deferred symbols that cannot be processed in this round.
*/
fun process(resolver: Resolver): List
/** Called to finalize processing of a compilation. */
fun finish() {}
/** Called to handle errors after a processing round. */
fun onError() {}
}The process method receives a Resolver that can query symbols, e.g., getSymbolsWithAnnotation or getClassDeclarationByName . An example processor searches for methods annotated with @TestFind and generates a Kotlin file using KotlinPoet.
annotation class TestFind()
class MyProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver): List
{
val mySymbol = resolver.getSymbolsWithAnnotation(TestFind::class.qualifiedName!!)
val ret = mySymbol.filter { !it.validate() }.toList()
val list = mySymbol.filter { it is KSFunctionDeclaration }
.map { it as KSFunctionDeclaration }
.toList()
logger.warn("list is ${list.toList()}")
MyFuncHandler.generate(codeGenerator, logger, list)
return ret
}
}The helper MyFuncHandler creates a file MyFindLocation.FuncLocation with a method that logs the file path of each annotated function.
object MyFuncHandler {
var isInit = false
@OptIn(KotlinPoetKspPreview::class)
fun generate(
codeGenerator: CodeGenerator,
logger: KSPLogger,
list: List
) {
if (isInit) return
isInit = true
val file = FileSpec.builder("com.test.find.location", "MyFindLocation")
val classBuilder = TypeSpec.classBuilder("FuncLocation")
val fun2 = FunSpec.builder("myFunc")
list.forEach {
logger.warn("parent is " + it.parent.toString())
val location = it.location
if (location is FileLocation) {
fun2.addStatement("Log.e(%S,%S)", "hello", location.filePath)
}
}
classBuilder.addFunction(fun2.build())
file.addImport("android.util", "Log")
file.addType(classBuilder.build())
file.build().writeTo(codeGenerator, false)
}
}Because process may run multiple rounds, the isInit flag prevents generating duplicate files. The generated file contains the full path of each annotated method, useful for locating code across modules.
Summary
After reading this article, you should have a solid understanding of KSP: it can accomplish everything APT does, but with better performance for Kotlin‑centric Android projects. Migrating from KAPT to KSP is becoming mainstream, and developers are encouraged to adopt KSP for efficient code generation.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.