Kotlin KSP聚合输出文件生成的正确实现策略
解决KSP多处理器依赖与聚合生成的正确策略
针对你遇到的KSP处理器聚合输出、跨处理器依赖以及多轮处理的问题,以下是可靠的解决方案,替代硬编码轮次的不可靠实现:
1. 核心思路:基于符号稳定状态的多轮触发
KSP的处理器通过返回未处理的符号(或返回false)告知框架需要重新运行处理,直到所有处理器返回空列表表示处理完成。我们可以利用这一机制,让处理器在依赖的符号稳定后再完成生成,无需硬编码轮次。
2. 各处理器的具体实现方案
AutoModuleProcessor(聚合@AutoModule类生成GeneratedModule)
解决重复生成、停止条件的问题:
- 维护集合跟踪已收集的@AutoModule类全限定名,仅在集合稳定(无新类加入)时生成文件
- 每轮对比当前收集的类与上一轮集合,判断是否需要继续触发下一轮
class AutoModuleProcessor(private val codeGenerator: CodeGenerator) : SymbolProcessor { private val collectedAutoModules = mutableSetOf<String>() override fun process(resolver: Resolver): List<KSAnnotated> { // 收集当前所有可解析的@AutoModule标注类 val autoModuleSymbols = resolver.getSymbolsWithAnnotation(AutoModule::class.qualifiedName!!) .filterIsInstance<KSClassDeclaration>() .filter { !it.isError } val currentModules = autoModuleSymbols.map { it.qualifiedName!!.asString() }.toSet() // 检查集合是否稳定:当前收集与上一轮完全一致 if (currentModules == collectedAutoModules) { // 集合稳定,仅生成一次GeneratedModule generateGeneratedModule(currentModules) return emptyList() // 空列表表示处理完成 } else { // 集合有更新,更新跟踪集合,触发下一轮处理 collectedAutoModules.clear() collectedAutoModules.addAll(currentModules) return autoModuleSymbols.toList() // 返回符号告知KSP重新处理 } } private fun generateGeneratedModule(moduleNames: Set<String>) { val file = codeGenerator.createNewFile( dependencies = Dependencies.ALL_FILES, packageName = "com.your.package", fileName = "GeneratedModule" ) file.bufferedWriter().use { writer -> writer.write(""" package com.your.package; import dagger.Module; @Module(includes = {${moduleNames.joinToString(", ") { "$it.class" }}}) public class GeneratedModule { } """.trimIndent()) } } }
GenerateAutoModuleProcessor(生成带@AutoModule的类)
确保生成的类能被AutoModuleProcessor收集:
- 处理所有@GenerateAutoModule标注的类,生成对应的@AutoModule类
- 若有文件生成,返回未处理符号触发下一轮,让AutoModuleProcessor能收集新生成的类
class GenerateAutoModuleProcessor(private val codeGenerator: CodeGenerator) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { val generateSymbols = resolver.getSymbolsWithAnnotation(GenerateAutoModule::class.qualifiedName!!) .filterIsInstance<KSClassDeclaration>() .filter { !it.isError } var hasGeneratedFiles = false generateSymbols.forEach { symbol -> val className = symbol.simpleName.asString() val packageName = symbol.packageName.asString() // 生成带@AutoModule的类 val file = codeGenerator.createNewFile( dependencies = Dependencies(aggregating = true, sources = arrayOf(symbol.containingFile!!)), packageName = packageName, fileName = "${className}AutoModule" ) file.bufferedWriter().use { writer -> writer.write(""" package $packageName; import com.your.package.AutoModule; @AutoModule public class ${className}AutoModule { } """.trimIndent()) } hasGeneratedFiles = true } // 有文件生成则返回符号触发下一轮,否则结束处理 return if (hasGeneratedFiles) generateSymbols.toList() else emptyList() } }
ComponentProcessor(依赖GeneratedModule生成代码)
解决无法解析GeneratedModule的问题:
- 推迟@Component的处理,直到GeneratedModule可被解析
- 若依赖类不存在,返回未处理符号触发下一轮
class ComponentProcessor(private val codeGenerator: CodeGenerator) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { val componentSymbols = resolver.getSymbolsWithAnnotation(Component::class.qualifiedName!!) .filterIsInstance<KSClassDeclaration>() .filter { !it.isError } val unprocessedSymbols = mutableListOf<KSAnnotated>() componentSymbols.forEach { symbol -> // 获取@Component的modules参数,检查GeneratedModule是否可解析 val componentAnnotation = symbol.annotations.find { it.shortName.asString() == "Component" } ?: return@forEach val modulesArg = componentAnnotation.arguments.find { it.name?.asString() == "modules" }?.value as? List<KSClassDeclaration> ?: return@forEach // 校验所有modules是否可解析(重点是GeneratedModule) val allModulesResolvable = modulesArg.all { module -> resolver.getClassDeclarationByName(module.qualifiedName!!) != null } if (!allModulesResolvable) { // 依赖未就绪,推迟处理 unprocessedSymbols.add(symbol) } else { // 依赖就绪,生成目标类 val className = symbol.simpleName.asString() val packageName = symbol.packageName.asString() val file = codeGenerator.createNewFile( dependencies = Dependencies.ALL_FILES, packageName = packageName, fileName = "Generated${className}" ) file.bufferedWriter().use { writer -> writer.write(""" package $packageName; import dagger.Component; @Component(modules = GeneratedModule.class) public interface Generated${className} { // 生成对应业务方法 } """.trimIndent()) } } } // 返回未处理符号,触发下一轮直到依赖满足 return unprocessedSymbols } }
3. 关键注意事项
- 不在finish()中生成文件:finish()是一轮处理结束后调用,此时依赖的处理器可能还未完成生成,导致后续处理器无法解析符号,应在process()中判断条件成熟时生成。
- 正确指定Dependencies:生成文件时设置合理的依赖参数,让KSP能准确跟踪文件依赖,避免不必要的重处理。
- 过滤错误符号:处理前过滤
isError的符号,避免无效处理引发异常。
内容的提问来源于stack exchange,提问作者Mikhail
相关产品推荐
相关产品推荐

