Stratify
Stratify 使您能够比以往任何时候都更容易地构建 Kotlin Symbol Processing (KSP) 插件。Stratify 抽象了 编写 KSP 插件时几乎所有的样板代码,并将 Kotlin 协程集成到您的 KSP 代码中,以 最大化您的 Symbol Processors 的效率。
Overview
使用 Stratify,您只需要设置一个 Strategy 和一个 Processor,Stratify 将自动处理其余的样板代码
以高效地选择节点。一个 Strategy 定义了要访问哪些节点,而一个 Processor 定义了要在每个
这些节点上执行的操作。您可以拥有任意数量的策略,每个策略可以拥有任意数量的处理器。这是一种极其
强大的构建 KSP 插件的方式,因为您的代码将易于理解、易于修改,并且具有无限的灵活性。
Stratify 框架将使您的代码保持整洁,使您的架构可扩展,简化维护,并使实验
就像换入一个新的处理器一样简单。
特性
- 效率: 利用对协程(Coroutines)的内置支持来提升效率。
- 灵活性: 为给定的注解定义任意数量的
Processors,并控制它们的执行顺序。 - 简洁性: 简单定义
Processor,将其插入到Strategy中,Stratify 将完成其余工作! - 可扩展性: 专为处理不断增长的项目而设计,Stratify 的健壮框架鼓励可扩展和可持续的开发实践,使其既适合小型团队也适合大型企业。
- 策略模式: 利用策略模式实现灵活且可维护的代码生成。
优势
- 更少代码,更多功能: Stratify 抽象了复杂且繁琐的样板代码,使开发者能够专注于有趣的部分。
- 架构: 通过利用策略模式强制实施一致的架构,Stratify 有助于保持代码库整洁、模块化且易于管理。
- 协程: 借助 Stratify 的内置协程支持,您可以获得高效、非阻塞的操作,从而提升大型项目的性能。
- 快速原型设计与测试: 开发者可以快速实现并试验新的处理器,从而加速开发周期。
快速入门
1. 添加依赖
Stratify 框架还会传递性地提供您开发所需的 KSP 库,因此您只需要一个依赖项。
将以下内容添加到您的 build.gradle.kts
dependencies {
// Note that this will also provide the KSP libraries you need!
implementation("io.github.mattshoe.shoebox:Stratify:1.2.0")
// Provides a simple DSL to write compilation tests
testImplementation("io.github.mattshoe.shoebox:Stratify.Test:1.2.0")
}
2. 创建注解(可选)
如果您的 Processor 依赖于自定义注解,现在是时候了!
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class MyAnnotation
3. 实现一个 StratifySymbolProcessor
继承 StratifySymbolProcessor 并实现 buildStrategies 方法。
class MyProcessor: StratifySymbolProcessor() {
override suspend fun buildStrategies(resolver: Resolver) = listOf(
AnnotationStrategy(
annotation = MyAnnotation::class,
TODO("Add your processors here once you implement them")
)
)
}
4. 创建一个 SymbolProcessorProvider
Stratify 为您抽象了此步骤,您只需执行以下操作:
class MyProcessorProvider: SymbolProcessorProvider by stratifyProvider<MyProcessor>()
5. 添加您的 META-INF 文件
KSP 要求您拥有一个指向第 4 步中您的 provider 的元数据文件。
只需创建以下文件:
src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
并在文件中放入第 4 步中您的 SymbolProcessorProvider 的全限定名
com.foo.bar.MyProcessorProvider
7. 实现一个 Processor
采用下面简单的 Processor。该处理器检查任何类声明上的 KDoc,然后使用
KotlinPoet 生成一个返回 KDoc 字符串的扩展函数:
class DocReaderClassProcessor: Processor<KSClassDeclaration> { // Specify we're only interested in KSClassDeclaration
override val targetClass = KSClassDeclaration::class // The class of your generic type
override suspend fun process(node: KSClassDeclaration): Set<GeneratedFile> {
val packageName = node.packageName.asString()
val className = node.simpleName.asString()
val fileName = "${className}_DocReader"
// Generate a file that defines an extension function `SomeClass.readDoc()`
val readDocFunction = FunSpec.builder("readDoc")
.receiver(ClassName(packageName, className))
.returns(String::class)
.addStatement("return %S", node.docString ?: "")
.build()
val file = FileSpec.builder(packageName, fileName)
.addFunction(readDocFunction)
.build()
// Return the set of files that we've generated for this node
return setOf(
GeneratedFile(
packageName = packageName,
fileName = fileName,
output = file.toString()
)
)
}
}
8. 选择一个 Strategy 并插入您的 Processor!
最后一步是选择您的 Strategy 并将其插入到您的 StratifySymbolProcessor 中!
class MyProcessor: StratifySymbolProcessor() {
override suspend fun buildStrategies(resolver: Resolver) = listOf(
AnnotationStrategy(
annotation = MyAnnotation::class,
DocReaderClassProcessor()
)
)
}
什么是策略?
在 Stratify 的上下文中,策略简单地定义了一个针对非常特定的 KSNode 实例子集运行的操作序列。
例如,你可能有一个策略,针对所有带有特定 Annotation 注释的源代码运行一系列操作。
或者,你可能需要针对所有以 "ViewModel" 为后缀的文件运行一组操作。又或者,你可能
需要针对所有以 "Async" 为后缀的函数运行一系列操作。你可能会遇到无数种可能的场景。
案例研究
最常见的用例是 AnnotationStrategy。该策略定义了一个针对所有由给定注解标注的 KSAnnotated 节点运行的 Processors 序列。
AnnotationStrategy(
annotation = MyAnnotation::class,
MyClassProcessor(),
MyFunctionProcessor()
)
在上面的示例中,AnnotationStrategy 就像一个"过滤器",只接受带有 MyAnnotation 注解的 KSNode 实例。这意味着它的上界是 KSAnnotated 类型。
该策略有 2 个处理器:MyClassProcessor 和 MyFunctionProcessor。
MyClassProcessor是一个Processor,仅处理KSClassDeclaration节点。由于该处理器用于MyAnnotation的AnnotationStrategy中,它只会处理同时带有MyAnnotation注解的KSClassDeclaration实例。MyFunctionProcessor的行为类似,但仅处理KSFunctionDeclaration节点。由于该处理器用于MyAnnotation的AnnotationStrategy中,它只会处理同时带有MyAnnotation注解的KSFunctionDeclaration实例。
什么是处理器?
在 Stratify 中,一个 Processor 定义了一个针对特定 KSNode 子类型执行的操作。
这通常用于生成新的代码文件,但你可以利用 Processor 来运行任何你
可能需要的操作类型。你可以用它来聚合数据,或者你遇到的其他用例。你不需要从你的处理器中返回
任何 GeneratedFile。
案例研究
考虑下面简单的 Processor。
该处理器检查类声明上的 KDoc,然后生成一个扩展函数,该函数使用 KotlinPoet 生成代码,以字符串形式返回 KDoc。
请注意,根据设计,Processor 的实现不绑定到任何特定的注解或其他过滤逻辑。它只是
针对你的 Strategy 解析的所有 KSNode 运行。这使得你的 Processor 实现保持高度可重用
并鼓励关注点分离。
例如,在 FilePatternStrategy 中使用下面的处理器意味着该处理器仅针对匹配指定文件模式的文件中包含的类
运行。或者在 FunctionNameStrategy 中使用它意味着该处理器
仅针对匹配 FunctionNameStrategy 中指定的函数名的函数运行。
class DocReaderClassProcessor: Processor<KSClassDeclaration> { // Specify we're only interested in KSClassDeclaration
override val targetClass = KSClassDeclaration::class // The class of your generic type
override suspend fun process(node: KSClassDeclaration): Set<GeneratedFile> {
val packageName = node.packageName.asString()
val className = node.simpleName.asString()
val fileName = "${className}_DocReader"
// Generate a file that defines an extension function `SomeClass.readDoc()`
val readDocFunction = FunSpec.builder("readDoc")
.receiver(ClassName(packageName, className))
.returns(String::class)
.addStatement("return %S", node.docString ?: "")
.build()
val file = FileSpec.builder(packageName, fileName)
.addFunction(readDocFunction)
.build()
// Return the set of files that we've generated for this node
return setOf(
GeneratedFile(
packageName = packageName,
fileName = fileName,
output = file.toString()
)
)
}
}
内置策略
AnnotationStrategy
定义一个 Strategy,其处理器将接收所有由指定注解标注的 KSAnnotated 节点实例。
AnnotationStrategy(
annotation = DocReader::class,
DocReaderClassProcessor(),
DocReaderFunctionProcessor()
)
FilePatternStrategy
定义一个 Strategy,其处理器将接收所有名称匹配给定模式的 KSFile 节点实例。
FilePatternStrategy(
pattern = ".*ViewModel",
DocReaderClassProcessor(),
DocReaderFunctionProcessor()
)
FileNameStrategy
定义一个 Strategy,其处理器将接收所有名称完全匹配给定名称的 KSFile 节点实例。
FileNameStrategy(
name = "SomeFileName",
DocReaderClassProcessor(),
DocReaderFunctionProcessor()
)
FunctionPatternStrategy
定义一个 Strategy,其处理器将接收所有名称匹配给定模式的 KSFunctionDeclaration 节点实例。
FunctionPatternStrategy(
pattern = ".*SomeFunction",
DocReaderFunctionProcessor()
)
FunctionNameStrategy
定义一个 Strategy,其处理器将接收所有名称完全匹配给定名称的
KSFunctionDeclaration 节点实例。
FunctionNameStrategy(
name = "someFunctionName",
DocReaderFunctionProcessor()
)
PropertyPatternStrategy
定义一个 Strategy,其处理器将接收所有名称匹配给定模式的 KSPropertyDeclaration 节点实例。
PropertyPatternStrategy(
pattern = ".*SomeProperty",
DocReaderPropertyProcessor()
)
PropertyNameStrategy
定义一个 Strategy,其处理器将接收所有名称完全匹配给定名称的
KSPropertyDeclaration 节点实例。
PropertyNameStrategy(
name = "somePropertyName",
DocReaderFunctionProcessor()
)
NewFilesStrategy
定义一个 Strategy,其处理器将接收所有 新 KSFile 实例。此处的“新”一词定义见 Resolver.getNewFiles。
NewFilesStrategy(
DocReaderClassProcessor()
)
测试
Stratify 为您提供了一个简化的测试 DSL,允许您在单元测试中编译您的 KSP 处理器, 并对其整体功能进行集成测试。这对于为您的处理器构建真正的测试套件非常有价值。
下面是一个使用编译 DSL 的示例测试。这是为 第 8 步 的 快速入门 章节构建的真实单元测试的快照:
@Test
fun `test annotation processor generates expected files`() = buildCompilation {
processors(MyProcessorProvider())
file("Test.kt") {
"""
package io.github.mattshoe.test
import test.stratify.annotation.DocReader
/**
* This is SomeInterface that fetches some data.
*/
@DocReader
interface SomeInterface {
/**
* This is a function that fetches some data
*/
@DocReader
suspend fun fetchData(param: String): String
}
""".trimIndent()
}
options {
assertionsMode = "always-enable"
javacArguments = mutableListOf("-parameters")
// etc, etc...
}
compile { compilation ->
val generatedClassDoc = compilation.generatedFiles.firstOrNull { it.name == "SomeInterface_DocReader.kt" }
Truth.assertThat(compilation.generatedFiles).hasSize(1)
Truth.assertThat(generatedClassDoc).isNotNull()
}
}
贡献
欢迎并鼓励贡献!请查阅 CONTRIBUTING 文档中的指南