作为前端开发者,你一定使用过 GraphQL Code Generator 从 Schema 自动生成 TypeScript 类型,或者用过 Prisma 从数据库 Schema 生成客户端代码。这些工具的核心思想就是编译时代码生成——在构建过程中分析源码,然后自动生成样板代码。今天,我们将深入 Kotlin 的 KSP (Kotlin Symbol Processing) 技术,看看如何在 Kotlin 中实现类似的编译时代码生成功能。通过一个完整的页面注册系统实战项目,你将掌握从注解定义到代码生成的完整流程。
在前面的学习中,我们已经掌握了:
文章地址: https://cloud.tencent.com/developer/article/2554449
文章地址: https://cloud.tencent.com/developer/article/2558876
文章地址: https://cloud.tencent.com/developer/article/2560699
现在,让我们将这些知识整合起来,构建一个真正的编译时代码生成系统。
KSP (Kotlin Symbol Processing) 是 Google 为 Kotlin 开发的轻量级编译器插件 API。它允许我们在编译时分析 Kotlin 代码的符号信息,并基于这些信息生成新的代码文件。
如果你熟悉前端工具链,可以这样理解 KSP:
// 类似 GraphQL Code Generator
// 输入:GraphQL Schema
type User {
id: ID!
name: String!
email: String!
}
// 输出:自动生成的 TypeScript 类型
export interface User {
id: string;
name: string;
email: string;
}
// KSP 的工作方式
// 输入:带注解的 Kotlin 类
@Page(name = "home", route = "/")
class HomePage {
fun show() = println("首页")
}
// 输出:自动生成的注册器
object PageRegistry {
fun createPage(name: String): Any? = when(name) {
"home" -> HomePage()
else -> null
}
}
特性 | KSP | KAPT | 反射 | 前端对比 |
---|---|---|---|---|
处理时机 | 编译时 | 编译时 | 运行时 | Babel 插件 vs 运行时 Proxy |
性能 | 高 | 中等 | 低 | 构建时 vs 运行时开销 |
Kotlin 支持 | 原生 | Java 兼容层 | 完整 | TypeScript vs JavaScript |
增量编译 | ✅ | ❌ | N/A | Webpack 增量编译 |
假设我们正在开发一个多页面应用,需要一个页面注册系统来管理所有页面的创建和路由。传统做法需要手动维护一个注册表,每次添加新页面都要记得更新注册代码。
使用 KSP 实现自动化的页面注册系统:
@Page
用于标记页面类@Page
注解,生成注册器编译时:
源码 → KSP 扫描 → 代码生成 → 编译
运行时:
应用代码 → 调用生成的工厂 → 创建页面实例
让我们从定义注解开始,这就像在前端定义组件的 props 接口一样:
// src/main/kotlin/annotations/Page.kt
package annotations
/**
* 页面注解 - 用于标记页面类
* 类似前端框架中的组件注册装饰器
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME) // 支持运行时访问(便于调试)
annotation class Page(
/**
* 页面名称 - 用作唯一标识符
* 类似 React 组件的 displayName
*/
val name: String = "",
/**
* 页面路由 - URL 路径
* 类似 Vue Router 的 path
*/
val route: String = "",
/**
* 页面描述 - 用于文档和调试
*/
val description: String = "",
/**
* 优先级 - 用于排序显示
* 数值越大优先级越高
*/
val priority: Int = 0
)
@Target(AnnotationTarget.CLASS)
:只能用于类,避免误用@Retention(RUNTIME)
:支持运行时反射,便于调试和测试现在让我们创建几个示例页面,就像在前端创建不同的组件一样:
// src/main/kotlin/pages/HomePage.kt
package pages
import annotations.Page
/**
* 首页 - 应用的主入口页面
* 类似前端的 Home 组件
*/
@Page(
name = "home",
route = "/",
description = "应用首页",
priority = 100 // 最高优先级
)
class HomePage {
fun showHome() {
println("=== 欢迎来到首页 ===")
println("这里是应用的主页面")
println("提供应用的主要功能入口")
}
fun getPageInfo(): String = "HomePage - 应用首页"
}
// src/main/kotlin/pages/AboutPage.kt
package pages
import annotations.Page
/**
* 关于页面 - 展示应用信息
*/
@Page(
name = "about",
route = "/about",
description = "关于我们页面",
priority = 50
)
class AboutPage {
fun showAbout() {
println("=== 关于我们 ===")
println("这是一个 Kotlin 元编程学习项目")
println("展示 KSP 代码生成的强大功能")
}
fun getPageInfo(): String = "AboutPage - 关于我们"
}
// src/main/kotlin/pages/ContactPage.kt
package pages
import annotations.Page
/**
* 联系页面 - 提供联系方式
*/
@Page(
name = "contact",
route = "/contact",
description = "联系我们页面",
priority = 30
)
class ContactPage {
fun showContact() {
println("=== 联系我们 ===")
println("邮箱: contact@example.com")
println("电话: 123-456-7890")
}
fun getPageInfo(): String = "ContactPage - 联系我们"
}
@Page
标记getPageInfo()
方法这是整个系统的核心部分,类似前端的 Babel 插件或 Webpack loader:
首先,我们需要创建一个独立的处理器子项目,避免循环依赖:
// processor/build.gradle.kts
plugins {
kotlin("jvm")
}
dependencies {
// KSP API - 用于编写处理器
implementation("com.google.devtools.ksp:symbol-processing-api:1.9.20-1.0.14")
}
// processor/src/main/kotlin/PageProcessor.kt
package processor
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
import com.google.devtools.ksp.validate
import java.io.OutputStream
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
/**
* 页面注解处理器
* 类似前端的代码生成器,扫描注解并生成代码
*/
class PageProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
logger.info("🚀 开始处理 @Page 注解...")
// 1. 查找所有带有 @Page 注解的类
val pageAnnotationName = "annotations.Page"
val symbols = resolver.getSymbolsWithAnnotation(pageAnnotationName)
val ret = symbols.filter { !it.validate() }.toList()
// 2. 过滤出有效的类声明
val pageClasses = symbols
.filterIsInstance<KSClassDeclaration>()
.filter { it.validate() }
.toList()
if (pageClasses.isEmpty()) {
logger.info("📝 没有找到带有 @Page 注解的类")
return ret
}
logger.info("✅ 找到 ${pageClasses.size} 个页面类")
pageClasses.forEach { clazz ->
logger.info(" - ${clazz.simpleName.asString()}")
}
// 3. 生成 PageRegistry
generatePageRegistry(pageClasses)
return ret
}
/**
* 生成页面注册器代码
*/
private fun generatePageRegistry(pageClasses: List<KSClassDeclaration>) {
logger.info("📦 开始生成 PageRegistry...")
// 创建生成文件
val file = codeGenerator.createNewFile(
dependencies = Dependencies(false, *pageClasses.map { it.containingFile!! }.toTypedArray()),
packageName = "generated",
fileName = "PageRegistry"
)
file.use { outputStream ->
outputStream.write(generateRegistryCode(pageClasses))
}
logger.info("🎉 成功生成 PageRegistry.kt")
}
/**
* 生成注册器代码内容
*/
private fun generateRegistryCode(pageClasses: List<KSClassDeclaration>): ByteArray {
val currentTime = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
// 提取页面信息
val pageInfos = pageClasses.map { clazz ->
val annotation = clazz.annotations.find {
it.shortName.asString() == "Page"
}
val name = annotation?.arguments?.find { it.name?.asString() == "name" }?.value as? String ?: ""
val route = annotation?.arguments?.find { it.name?.asString() == "route" }?.value as? String ?: ""
val description = annotation?.arguments?.find { it.name?.asString() == "description" }?.value as? String ?: ""
val priority = annotation?.arguments?.find { it.name?.asString() == "priority" }?.value as? Int ?: 0
PageInfo(
className = clazz.simpleName.asString(),
packageName = clazz.packageName.asString(),
name = name,
route = route,
description = description,
priority = priority
)
}
// 生成代码
val code = buildString {
appendLine("package generated")
appendLine()
// 导入语句
val packages = pageInfos.map { it.packageName }.distinct()
packages.forEach { pkg ->
appendLine("import $pkg.*")
}
appendLine()
// 文档注释
appendLine("/**")
appendLine(" * KSP 自动生成的页面注册器")
appendLine(" * 生成时间: $currentTime")
appendLine(" * 处理的页面数量: ${pageInfos.size}")
appendLine(" */")
// 注册器对象
appendLine("object PageRegistry {")
appendLine()
// 页面元数据类
appendLine(" data class PageMetadata(")
appendLine(" val name: String,")
appendLine(" val route: String,")
appendLine(" val description: String,")
appendLine(" val priority: Int,")
appendLine(" val className: String")
appendLine(" )")
appendLine()
// 页面工厂映射
appendLine(" private val pageFactories = mapOf<String, () -> Any>(")
pageInfos.sortedBy { it.name }.forEach { info ->
appendLine(" \"${info.name}\" to { ${info.className}() },")
}
appendLine(" )")
appendLine()
// 页面元数据映射
appendLine(" private val pageMetadata = mapOf<String, PageMetadata>(")
pageInfos.sortedBy { it.name }.forEach { info ->
appendLine(" \"${info.name}\" to PageMetadata(")
appendLine(" name = \"${info.name}\",")
appendLine(" route = \"${info.route}\",")
appendLine(" description = \"${info.description}\",")
appendLine(" priority = ${info.priority},")
appendLine(" className = \"${info.packageName}.${info.className}\"")
appendLine(" ),")
}
appendLine(" )")
appendLine()
// 公共 API
appendLine(" /**")
appendLine(" * 根据名称创建页面实例")
appendLine(" */")
appendLine(" fun createPage(name: String): Any? = pageFactories[name]?.invoke()")
appendLine()
appendLine(" /**")
appendLine(" * 获取所有页面名称")
appendLine(" */")
appendLine(" fun getAllPageNames(): Set<String> = pageFactories.keys")
appendLine()
appendLine(" /**")
appendLine(" * 获取页面元数据")
appendLine(" */")
appendLine(" fun getPageMetadata(name: String): PageMetadata? = pageMetadata[name]")
appendLine()
appendLine(" /**")
appendLine(" * 获取所有页面元数据")
appendLine(" */")
appendLine(" fun getAllPageMetadata(): Collection<PageMetadata> = pageMetadata.values")
appendLine()
appendLine(" /**")
appendLine(" * 根据路由查找页面")
appendLine(" */")
appendLine(" fun findPageByRoute(route: String): String? = ")
appendLine(" pageMetadata.values.find { it.route == route }?.name")
appendLine()
appendLine(" /**")
appendLine(" * 按优先级排序获取页面")
appendLine(" */")
appendLine(" fun getPagesByPriority(): List<PageMetadata> = ")
appendLine(" pageMetadata.values.sortedByDescending { it.priority }")
appendLine("}")
}
return code.toByteArray()
}
/**
* 页面信息数据类
*/
private data class PageInfo(
val className: String,
val packageName: String,
val name: String,
val route: String,
val description: String,
val priority: Int
)
}
/**
* KSP 处理器提供者
* 类似前端插件的入口点
*/
class PageProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return PageProcessor(environment.codeGenerator, environment.logger)
}
}
创建服务提供者配置文件:
// processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
processor.PageProcessorProvider
// build.gradle.kts
plugins {
kotlin("jvm")
id("com.google.devtools.ksp") version "1.9.20-1.0.14"
application
}
dependencies {
// KSP API
implementation("com.google.devtools.ksp:symbol-processing-api:1.9.20-1.0.14")
// Kotlin 反射
implementation("org.jetbrains.kotlin:kotlin-reflect")
// 使用处理器子项目
ksp(project(":modules:kotlin-metaprogramming:processor"))
}
// KSP 配置
ksp {
arg("generated.package", "generated")
}
// 源码目录配置 - 包含生成的代码
kotlin {
sourceSets.main {
kotlin.srcDir("build/generated/ksp/main/kotlin")
}
}
// 应用配置
application {
mainClass.set("demo.KspGeneratedDemoKt")
}
// settings.gradle.kts
include(":modules:kotlin-metaprogramming:processor")
现在让我们创建一个演示程序,使用 KSP 生成的页面注册器:
// src/main/kotlin/demo/KspGeneratedDemo.kt
package demo
import generated.PageRegistry
import pages.*
/**
* KSP 代码生成演示
* 展示如何使用自动生成的页面注册器
*/
fun main() {
println("=== KSP 代码生成演示 ===")
println("这个演示使用了真正的 KSP 处理器生成的代码")
println()
// 演示基本功能
demonstrateBasicFeatures()
println()
// 演示高级功能
demonstrateAdvancedFeatures()
println()
println("✅ 成功使用了 KSP 生成的 PageRegistry!")
}
/**
* 演示基本功能
*/
private fun demonstrateBasicFeatures() {
println("=== KSP 生成的 PageRegistry 演示 ===")
println()
// 1. 获取所有页面名称
val pageNames = PageRegistry.getAllPageNames()
println("所有页面: ${pageNames.joinToString(", ")}")
println()
// 2. 按优先级排序显示页面
println("按优先级排序的页面:")
PageRegistry.getPagesByPriority().forEach { metadata ->
println(" - ${metadata.name}: ${metadata.description} (优先级: ${metadata.priority})")
}
println()
// 3. 创建和使用页面
val pageName = "home"
println("尝试创建页面: $pageName")
val page = PageRegistry.createPage(pageName)
when (page) {
is HomePage -> {
page.showHome()
println("页面信息: ${page.getPageInfo()}")
}
else -> println("未知页面类型: $page")
}
}
/**
* 演示高级功能
*/
private fun demonstrateAdvancedFeatures() {
println("=== 高级功能演示 ===")
println()
// 1. 路由查找
val route = "/about"
val pageNameByRoute = PageRegistry.findPageByRoute(route)
println("路由 '$route' 对应的页面: $pageNameByRoute")
if (pageNameByRoute != null) {
val page = PageRegistry.createPage(pageNameByRoute)
when (page) {
is AboutPage -> page.showAbout()
}
}
println()
// 2. 元数据查询
println("所有页面的详细信息:")
PageRegistry.getAllPageMetadata().forEach { metadata ->
println(" 页面: ${metadata.name}")
println(" 路由: ${metadata.route}")
println(" 描述: ${metadata.description}")
println(" 类名: ${metadata.className}")
println(" 优先级: ${metadata.priority}")
println()
}
// 3. 动态页面创建
println("动态创建所有页面:")
PageRegistry.getAllPageNames().forEach { name ->
val page = PageRegistry.createPage(name)
println(" 创建页面 '$name': ${page?.javaClass?.simpleName}")
}
}
# 1. 清理项目
./gradlew :modules:kotlin-metaprogramming:clean
# 2. 运行 KSP 处理器(会自动执行)
./gradlew :modules:kotlin-metaprogramming:kspKotlin --info
# 3. 编译并运行演示
./gradlew :modules:kotlin-metaprogramming:run
# 查看生成的 PageRegistry
cat modules/kotlin-metaprogramming/build/generated/ksp/main/kotlin/generated/PageRegistry.kt
=== KSP 代码生成演示 ===
这个演示使用了真正的 KSP 处理器生成的代码
=== KSP 生成的 PageRegistry 演示 ===
所有页面: about, contact, home
按优先级排序的页面:
- home: 应用首页 (优先级: 100)
- about: 关于我们页面 (优先级: 50)
- contact: 联系我们页面 (优先级: 30)
尝试创建页面: home
=== 欢迎来到首页 ===
这里是应用的主页面
提供应用的主要功能入口
页面信息: HomePage - 应用首页
=== 高级功能演示 ===
路由 '/about' 对应的页面: about
=== 关于我们 ===
这是一个 Kotlin 元编程学习项目
展示 KSP 代码生成的强大功能
所有页面的详细信息:
页面: home
路由: /
描述: 应用首页
类名: pages.HomePage
优先级: 100
页面: about
路由: /about
描述: 关于我们页面
类名: pages.AboutPage
优先级: 50
页面: contact
路由: /contact
描述: 联系我们页面
类名: pages.ContactPage
优先级: 30
动态创建所有页面:
创建页面 'about': AboutPage
创建页面 'contact': ContactPage
创建页面 'home': HomePage
✅ 成功使用了 KSP 生成的 PageRegistry!
1. 源码解析 → Kotlin 编译器解析源码,构建 AST
2. 符号处理 → KSP 扫描符号,查找注解
3. 代码生成 → 处理器生成新的 Kotlin 文件
4. 增量编译 → 编译器编译原始代码 + 生成代码
5. 字节码输出 → 生成最终的 .class 文件
// KSP 的核心处理逻辑
override fun process(resolver: Resolver): List<KSAnnotated> {
// 1. 查找符号 - 类似 AST 遍历
val symbols = resolver.getSymbolsWithAnnotation("annotations.Page")
// 2. 验证符号 - 确保符号有效
val validSymbols = symbols.filter { it.validate() }
// 3. 提取信息 - 从注解中获取元数据
val pageInfos = validSymbols.map { extractPageInfo(it) }
// 4. 生成代码 - 基于信息生成新文件
generateCode(pageInfos)
// 5. 返回无效符号 - 用于下一轮处理
return symbols.filter { !it.validate() }.toList()
}
KSP 支持增量编译,只处理变更的文件:
// 正确设置文件依赖关系
val file = codeGenerator.createNewFile(
dependencies = Dependencies(
false, // aggregating = false,支持增量编译
*pageClasses.map { it.containingFile!! }.toTypedArray()
),
packageName = "generated",
fileName = "PageRegistry"
)
@Route("/api/users")
class UserController {
@GET("/")
fun getUsers(): List<User> = userService.findAll()
@POST("/")
fun createUser(@Body user: User): User = userService.save(user)
}
// KSP 生成路由注册器
object RouteRegistry {
val routes = mapOf(
"/api/users" to UserController::class,
// ... 其他路由
)
}
@Service
class UserService(private val userRepository: UserRepository)
@Repository
class UserRepository
// KSP 生成依赖注入容器
object DIContainer {
fun <T : Any> getInstance(clazz: KClass<T>): T {
// 自动生成的实例创建逻辑
}
}
@Entity("users")
data class User(
@Id val id: Long,
@Column("user_name") val name: String,
@Column val email: String
)
// KSP 生成 SQL 查询构建器
object UserQueries {
fun findById(id: Long): String = "SELECT * FROM users WHERE id = ?"
fun findByName(name: String): String = "SELECT * FROM users WHERE user_name = ?"
}
class OptimizedPageProcessor : SymbolProcessor {
// 缓存已处理的符号,避免重复处理
private val processedSymbols = mutableSetOf<String>()
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation("annotations.Page")
// 只处理新的符号
val newSymbols = symbols.filter { symbol ->
val key = "${symbol.packageName}.${symbol.simpleName}"
!processedSymbols.contains(key)
}
if (newSymbols.isEmpty()) {
return emptyList()
}
// 处理新符号
processSymbols(newSymbols)
// 记录已处理的符号
newSymbols.forEach { symbol ->
val key = "${symbol.packageName}.${symbol.simpleName}"
processedSymbols.add(key)
}
return emptyList()
}
}
private fun generateOptimizedCode(pageInfos: List<PageInfo>): String {
return buildString {
// 使用 StringBuilder 而不是字符串拼接
// 预分配合适的容量
ensureCapacity(pageInfos.size * 200)
// 批量生成,减少 I/O 操作
appendLine("object PageRegistry {")
// 使用更高效的数据结构
appendLine(" private val pageFactories = hashMapOf<String, () -> Any>(")
pageInfos.forEach { info ->
appendLine(" \"${info.name}\" to { ${info.className}() },")
}
appendLine(" )")
appendLine("}")
}
}
class RobustPageProcessor : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
return try {
processInternal(resolver)
} catch (e: Exception) {
logger.error("处理器执行失败: ${e.message}", e)
// 返回空列表,避免编译失败
emptyList()
}
}
private fun processInternal(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation("annotations.Page")
symbols.forEach { symbol ->
try {
validateSymbol(symbol)
} catch (e: Exception) {
logger.warn("符号验证失败: ${symbol.simpleName}, 错误: ${e.message}")
}
}
// ... 处理逻辑
return emptyList()
}
private fun validateSymbol(symbol: KSAnnotated) {
if (symbol !is KSClassDeclaration) {
throw IllegalArgumentException("@Page 只能用于类")
}
if (symbol.isAbstract()) {
throw IllegalArgumentException("@Page 不能用于抽象类")
}
// 检查是否有无参构造函数
val hasNoArgConstructor = symbol.primaryConstructor?.parameters?.isEmpty() == true
if (!hasNoArgConstructor) {
logger.warn("${symbol.simpleName} 没有无参构造函数,可能无法正确实例化")
}
}
}
@Page(
name = "user-profile",
route = "/user/{id}",
permissions = ["USER_READ", "PROFILE_VIEW"],
cacheStrategy = CacheStrategy.MEMORY,
dependencies = [UserService::class, ProfileService::class]
)
class UserProfilePage
class AdvancedPageProcessor : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val pageClasses = getPageClasses(resolver)
// 生成多个文件
generatePageRegistry(pageClasses) // 页面注册器
generateRouteMapping(pageClasses) // 路由映射
generatePermissionCheck(pageClasses) // 权限检查
generateDocumentation(pageClasses) // API 文档
return emptyList()
}
}
// 自定义 Gradle 插件
class PageGeneratorPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.extensions.create("pageGenerator", PageGeneratorExtension::class.java)
project.tasks.register("generatePages") { task ->
task.doLast {
// 执行页面生成逻辑
}
}
}
}
通过最近四篇文章的学习,我们已经掌握了从注解定义到代码生成的完整技术栈,可以在实际项目中应用这些强大的技术来提升开发效率和代码质量。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。