KSP注解处理器的输出目录与源码集配置问题
问题背景与疑问
我们在混编Java/Kotlin的原生Android项目中,用基于KSP的注解处理器收集带项目内部特定注解的类定义,生成顶层函数供构建流程使用,整体运行正常。
注解处理器实现于独立模块,和业务代码分离,只能通过字符串指定注解名称扫描目标类,无法依赖注解所在模块。
为避免注解重构(如重命名)但处理器内注解名称未同步导致生成空列表文件引发应用异常,我们加了单元测试检查生成文件的顶层函数是否存在且列表非空。
但执行单元测试时遇到问题:KSP再次触发,在build目录的不同子文件夹(当前构建变体为devDebug,对应devDebug与devDebugUnitTest)生成内容为空的文件,推测是KSP未扫描到目标业务类。
现提出两个问题:
- 如何让
environment.codeGenerator.createNewFile()生成的文件始终输出至同一目录,不受触发范围影响? - 如何配置让KSP扫描应用实际源码目录?找到的
srcDirs配置示例均针对Android插件模块,而该处理器模块仅为纯Kotlin模块,未使用Android插件。
处理器模块Gradle配置
plugins { id 'java-library' id 'org.jetbrains.kotlin.jvm' id 'com.google.devtools.ksp'} java { sourceCompatibility = versions.java.javaVersion targetCompatibility = versions.java.javaVersion } dependencies { implementation libs.googleDevtoolsKspSymbolProcessing }
处理器核心代码
class SomeAnnotationSymbolProcessor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor { override fun process(resolver: Resolver): List<KSClassDeclaration> { val annotatedClasses: Sequence<KSClassDeclaration> = resolver.getSymbolsWithAnnotation(SOME_ANNOTATION) .filterIsInstance<KSClassDeclaration>() .filter { it.annotations.any() } try { environment.logger.info("Creating file '$GENERATED_CLASS_NAME' based on found '$SOME_ANNOTATION_NAME' annotations.") annotatedClasses.mapNotNull { it.containingFile }.run { environment.codeGenerator.createNewFile( dependencies = Dependencies( false, *this.toList().toTypedArray(), ), packageName = SCAN_BARCODES_PACKAGE, fileName = GENERATED_CLASS_NAME, ).also { it.write(assembleClassImplementation(annotatedClasses.toList()).toByteArray()) } } } catch (_: FileAlreadyExistsException) { environment.logger.info("Existing file '$GENERATED_CLASS_NAME' has been overwritten silently.") } // Provide list of skipped symbols to the next round of symbol processing. // Those symbols will get processed in the next round, the processor automatically takes care of that based on the `Sequence` logic. return annotatedClasses.filterNot { it.validate() }.toList().also { environment.logger.info("Files not yet considered valid and held back for next round: ${it.joinToString(", ")}") } } }
解决方案
问题1:统一生成文件的输出目录
KSP默认会跟着构建任务(比如主编译、单元测试编译)把文件输出到对应变体的目录里,要统一路径可以用以下两种方法:
方法1:自定义生成目录(直接控制路径)
绕过codeGenerator.createNewFile()的默认逻辑,直接指定固定目录创建文件:
// 拿到KSP默认生成根目录,或自定义项目内路径 val baseDir = environment.options["ksp.generated.dir"] ?: environment.codeGenerator.generatedDir val unifiedOutputDir = File(baseDir, "shared-generated") unifiedOutputDir.mkdirs() // 构建包含包名结构的目标文件路径 val packageDir = File(unifiedOutputDir, SCAN_BARCODES_PACKAGE.replace(".", "/")) packageDir.mkdirs() val targetFile = File(packageDir, "$GENERATED_CLASS_NAME.kt") // 写入生成内容 targetFile.writeBytes(assembleClassImplementation(annotatedClasses.toList()).toByteArray())
注意:这种方式需要自己处理文件重复和增量编译逻辑,若要保留KSP的增量编译支持,用下面的方法。
方法2:通过Gradle传参统一配置目录
在应用模块的Gradle中添加KSP参数,统一指定输出目录:
ksp { arg("fixedOutputDir", "${project.buildDir}/generated/ksp/shared") }
然后在处理器代码中读取该参数,按自定义路径创建文件:
val fixedDir = environment.options["fixedOutputDir"] ?: error("请在应用模块配置fixedOutputDir参数") val packageDir = File(fixedDir, SCAN_BARCODES_PACKAGE.replace(".", "/")) packageDir.mkdirs() val targetFile = File(packageDir, "$GENERATED_CLASS_NAME.kt") // 写入生成内容...
问题2:让KSP扫描应用实际源码目录
处理器模块是纯Kotlin库,本身不需要扫描源码,问题出在单元测试任务触发时,KSP只扫描了测试源码,没覆盖主业务源码。解决方式如下:
- 在应用模块(Android模块)的Gradle中配置,让单元测试的KSP任务包含主源码:
tasks.withType(Test) { ksp { sourceSets project.sourceSets.main } }
或者直接让所有KSP任务扫描主+测试源码:
ksp { sourceSets project.sourceSets.main, project.sourceSets.test }
- 如果处理器模块自身单元测试需要扫描应用源码,在处理器模块的Gradle中添加依赖和配置:
// 处理器模块build.gradle dependencies { testImplementation project(":app") // 引入应用模块代码 } tasks.withType(Test) { ksp { // 添加应用模块的主源码路径 srcDirs project(":app").sourceSets.main.java.srcDirs srcDirs project(":app").sourceSets.main.kotlin.srcDirs } }
内容的提问来源于stack exchange,提问作者arkascha
相关产品推荐
相关产品推荐

