在上一篇文章 一文了解 ksp 的使用 中,我们使用了 kotlinPoet 来为 ksp 生成对应的源代码。这篇文章将详细介绍包含 kotlinPoet 在内的为 kotlin 生成源代码的方法。
直接拼接字符串
直接拼接字符串的方式非常简单,如下所示。就是把需要生成的源代码内容
kotlin
val message = "\"hello world\""
val content = """
fun main() {
println($message)
}
""".trimIndent()
File("Hello.kt").apply {
createNewFile()
appendText(content)
}
接下来,我们就可以使用如下命令来运行我们生成的源代码
gradle
// 使用 Kotlin 编译器编译文件
kotlinc Hello.kt -include-runtime -d Hello.jar
// 运行
java -jar Hello.jar
这样我们就可以看到输出的 hello world 了,如下图所示:

实际上,直接拼接字符串生成源码的方式在 kotlin 源码中也很常见。下面是 Arrays.kt 的源代码,通过注释可以看到它实际上是由 arrays.kt 来生成的

查看arrays.kt的源码,如下图所示。可以看到 Arrays.kt 实际上也是通过字符串拼接的方式来生成的。

为什么 Kotlin 官方这样的方式来生成源代码呢?因为这样方式的运行效率高,但是坏处是生成的程序看上去会比较复杂。
模板引擎渲染
通过拼接Kotlin字符串构造目标代码的方式在应对简单场景时非常方便且高效,但在应对较复杂的场景时会使得程序与目标代码严重耦合,导致程序的理解成本和维护成本明显上升。
相比之下,使用模板引擎生成目标代码有着更好的可维护性和可扩展性。这里使用 Pebble Templates 来作为模板引擎,来实现把 Java 静态方法转化为 Kotlin 扩展函数的功能。首先我们先导入 Pebble Templates 的库。
gradle
// https://mvnrepository.com/artifact/io.pebbletemplates/pebble
implementation("io.pebbletemplates:pebble:3.2.4")
然后创建模板文件,如下所示。关于Pebble Templates模板的规则,具体可以看 Basic Usage
txt
package {{ packageName }}
{% for fun in functions %}
fun {{ fun.receiverType }}.{{ fun.functionName }}({{ fun.args }})
= {{ fun.className }}.{{ fun.functionName }}({{ fun.argNames }})
{% endfor %}
我们的目标文件是 StringUtils,其内容如下所示。我们的目标就是把该文件中的 reverse
和 capitalize
方法变成扩展方法。
java
public class StringUtils {
/**
* 反转字符串的静态方法
* @param input 待反转的字符串,可以为 null
* @return 反转后的字符串,如果输入为 null 则返回 null
*/
public static String reverse(String input) {
if (input == null) {
return null;
}
return new StringBuilder(input).reverse().toString();
}
/**
* 将字符串的首字母转换为大写
* @param input 待处理的字符串,可以为 null 或空字符串
* @return 首字母大写后的字符串,如果输入为 null 则返回 null,空字符串返回空字符串
*/
public static String capitalize(String input) {
if (input == null || input.isEmpty()) {
return input;
}
return Character.toUpperCase(input.charAt(0)) + input.substring(1);
}
}
做好上面的操作后,我们就可以来实现我们的逻辑了。代码如下所示:
java
/**
* 模型生成器 - 将Java静态方法转换为Kotlin扩展函数
*/
class ModelGenerate {
/**
* 类信息数据结构
* @param packageName 包名
* @param functions 函数信息列表
*/
data class ClassInfo(val packageName: String, val functions: List<FunctionInfo>)
/**
* 函数信息数据结构
* @param className 声明函数的类名
* @param functionName 函数名称
* @param args 参数类型列表(带占位符参数名)
* @param argNames 参数名列表(占位符形式)
* @param receiverType 返回值类型
*/
data class FunctionInfo(
val className: String,
val functionName: String,
val args: String,
val argNames: String,
val receiverType: String
)
/**
* 生成Kotlin扩展函数文件的主方法
*/
fun generateKt() {
// 获取Java类的信息
val classInfo = getJavaFileClassInfo(StringUtils::class.java)
// 初始化Pebble模板引擎
val engine = PebbleEngine.Builder().build()
val compiledTemplate = engine.getTemplate("model")
// 准备模板上下文数据
val context = HashMap<String?, Any?>()
context.put("packageName", classInfo.packageName)
context.put("functions", classInfo.functions)
// 渲染模板
val writer: Writer = StringWriter()
compiledTemplate.evaluate(writer, context)
// 将渲染结果写入文件
val output: String = writer.toString()
File("src/main/java/example/kotlin_test/Output.kt").apply {
createNewFile()
appendText(output)
}
}
/**
* 分析Java类获取静态方法信息,这里使用Java反射功能来获取
* @param clazz 要分析的Java类
* @return 类信息对象
*/
fun getJavaFileClassInfo(clazz: Class<*>): ClassInfo {
val packageName = clazz.`package`.name
// 过滤并映射静态方法信息
val functionList = clazz.methods
.filter { Modifier.isStatic(it.modifiers) } // 只处理静态方法
.map { method ->
// 构建参数列表(由于Java反射默认情况下不支持获取参数名,这里使用自定义参数名)
val args = method.parameterTypes.mapIndexed { index, klass ->
"arg${index}: ${klass.simpleName}"
}.joinToString(",")
// 构建参数名列表(仅自定义参数名)
val argNames = method.parameterTypes.mapIndexed { index, _ ->
"arg${index}"
}.joinToString(",")
// 构建函数信息对象
FunctionInfo(
className = method.declaringClass.simpleName,
functionName = method.name,
args = args,
argNames = argNames,
receiverType = method.returnType.simpleName
)
}
return ClassInfo(packageName = packageName, functions = functionList)
}
}
fun main() {
ModelGenerate().generateKt()
}
最终结果如下图所示:

JavaPoet 和 KotlinPoet
上文介绍过直接拼接字符串来输出目标代码、使用模板引擎生成目标代码两种方式。直接输出代码的方式在大多数场景下开发效率较低。使用模板引擎生成代码的方式虽然在开发效率上有所提升,但也受限于模板本身,灵活性不高,想要扩展样式就要增加模板;同时,模板本身是中立的,缺乏对生成的目标代码的针对性支持,需要我们额外对目标代码进行优化。本节将简单介绍业界应用非常广泛的代码生成框架------------JavaPoet和KotlinPoet。
JavaPoet
javapoet 是用于生成Java代码的框架,框架本身也是使用Java编写实现的。要使用 javapoet ,我们需要先引入依赖。
gradle
// https://mvnrepository.com/artifact/com.squareup/javapoet
implementation("com.squareup:javapoet:1.13.0")
然后就可以使用了。这里以生成输出 Hello world 的代码为例。代码如下所示:
kotlin
import com.squareup.javapoet.*;
import javax.lang.model.element.Modifier;
import java.io.File;
import java.io.IOException;
/**
* HelloWorld 代码生成器
* 使用 JavaPoet 库动态生成包含 main 方法的 HelloWorld 类
*/
public class HelloWorldGenerator {
public static void main(String[] args) {
try {
// 构建 HelloWorld 类
TypeSpec helloWorldClass = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC) // 设置类为 public
.addJavadoc("自动生成的 HelloWorld 示例类\n") // 添加类注释
.addJavadoc("@author JavaPoet 生成器\n")
.addJavadoc("@since $L\n", java.time.LocalDate.now())
// 添加 main 方法
.addMethod(MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC) // 设置方法为 public static
.returns(void.class) // 返回类型为 void
.addParameter(String[].class, "args") // 添加 String[] args 参数
.addJavadoc("程序入口点\n")
.addJavadoc("@param args 命令行参数\n")
.addStatement("$T.out.println($S)", System.class, "Hello, World!") // 添加打印语句
.build())
.build();
// 创建编译单元并指定包名
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorldClass)
.addFileComment("自动生成的 Java 源文件,请勿手动修改") // 添加文件注释
.indent(" ") // 使用 4 个空格缩进
.build();
// 写入文件系统
String outputDir = "src/main/java";
javaFile.writeTo(new File(outputDir));
System.out.println("HelloWorld.java 已生成到 " + outputDir);
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行后,就可以看到生成的java文件了。

KotlinPoet
kotlinpoet 与JavaPoet类似,是用来生成Kotlin源代码的框架。KotlinPoet的代码使用Kotlin编写,其设计思路与JavaPoet如出一辙,不过由于Kotlin的语法特性比Java更多、更灵活,因此KotlinPoet的功能也比JavaPoet更复杂一些。
首先我们需要引入依赖:
gradle
// https://mvnrepository.com/artifact/com.squareup/kotlinpoet-jvm
implementation("com.squareup:kotlinpoet-jvm:2.2.0")
然后我们就可以使用 kotlinpoet 来生成kotlin代码了。这里以生成输出 Hello world 的代码为例。代码如下所示:
kotlin
import com.squareup.kotlinpoet.*
import java.io.File
/**
* Kotlin 代码生成器 - 使用 KotlinPoet 生成 HelloWorld 程序
*/
class KotlinCodeGenerator {
/**
* 生成 Kotlin 版本的 HelloWorld 程序
* @param packageName 包名
* @param fileName 文件名(不含.kt后缀)
* @param outputDir 输出目录
*/
fun generateHelloWorld(packageName: String, fileName: String, outputDir: String) {
// 创建文件规范构建器,指定包名和文件名
val fileSpec = FileSpec.builder(packageName, fileName)
.addFileComment("自动生成的 Kotlin 代码,输出 Hello World")
.addFileComment("生成时间: ${java.time.LocalDateTime.now()}")
// 创建 main 函数规范
val mainFunction = FunSpec.builder("main")
.addModifiers(KModifier.PUBLIC)
.returns(Unit::class)
.addKdoc("程序入口点\n")
.addStatement("println(%S)", "Hello, World!") // Kotlin 风格的输出语句
// 将 main 函数添加到文件中
fileSpec.addFunction(mainFunction.build())
// 写入文件系统
try {
val outputDirectory = File(outputDir)
if (!outputDirectory.exists()) {
outputDirectory.mkdirs()
}
fileSpec.build().writeTo(outputDirectory)
println("Kotlin 文件已生成到: ${outputDirectory.absolutePath}/$fileName.kt")
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// 使用示例
fun main() {
val generator = KotlinCodeGenerator()
generator.generateHelloWorld(
packageName = "com.example",
fileName = "HelloWorld",
outputDir = "src/main/kotlin"
)
}
效果如下图所示:

总结
方法 | 优点 | 缺点 | 应用示例 |
---|---|---|---|
直接拼接字符串 | 运行效率高 | 生成的程序看上去会比较复杂;而且开发效率低 | Kotlin 中 Arrays.kt 的源代码生成 |
模板引擎渲染 | 相对于直接拼接字符串,开发效率更高 | 受限于模板本身,灵活性不高,想要扩展样式就要增加模板 | Anko库使用模板引擎动态生成 Android Layout,目前 Anko 库已经废弃 |
代码生成框架---JavaPoet和KotlinPoet | 开发效率高、比较灵活 | 相对于前面的方法来说,该方式比较复杂 | 广泛应用 |
参考
- 《深入实践 Kotlin 元编程》
- 《深入实践 Kotlin 元编程》源码
- KotlinPoet详细使用指南(上)