kotlin元编程(二)使用 Kotlin 来生成源代码

在上一篇文章 一文了解 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,其内容如下所示。我们的目标就是把该文件中的 reversecapitalize 方法变成扩展方法。

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 开发效率高、比较灵活 相对于前面的方法来说,该方式比较复杂 广泛应用

参考

相关推荐
fatiaozhang95272 小时前
创维智能融合终端SK-M424_S905L3芯片_2+8G_安卓9_线刷固件包
android·电视盒子·刷机固件·机顶盒刷机
来来走走3 小时前
Flutter开发 了解Scaffold
android·开发语言·flutter
哆啦A梦的口袋呀4 小时前
Android 底层实现基础
android
闻道且行之4 小时前
Android Studio下载及安装配置
android·ide·android studio
alexhilton5 小时前
初探Compose中的着色器RuntimeShader
android·kotlin·android jetpack
小墙程序员5 小时前
kotlin元编程(一)一文理解 Kotlin 反射
android·kotlin·android studio
fatiaozhang95276 小时前
创维智能融合终端DT741_移动版_S905L3芯片_安卓9_线刷固件包
android·电视盒子·刷机固件·机顶盒刷机
KotlinKUG贵州8 小时前
贪心算法:从“瞎蒙”到稳赚
算法·kotlin
小林学Android8 小时前
Android四大组件之Activity详解
android