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

参考

相关推荐
Digitally3 小时前
如何将视频从安卓设备传输到Mac?
android·macos
alexhilton5 小时前
Compose Unstyled:Compose UI中失传的设计系统层
android·kotlin·android jetpack
刘龙超6 小时前
如何应对 Android 面试官 -> 玩转 RxJava (基础使用)
android·rxjava
柿蒂7 小时前
从动态缩放自定义View,聊聊为什么不要把问题复杂化
android·ai编程·android jetpack
kerli8 小时前
kotlin协程系列:callbackFlow
android·kotlin
没有了遇见9 小时前
Android 原生定位实现(替代融合定位收费,获取经纬度方案)
android·kotlin
一枚小小程序员哈9 小时前
基于Android的车位预售预租APP/基于Android的车位租赁系统APP/基于Android的车位管理系统APP
android·spring boot·后端·struts·spring·java-ee·maven
诸神黄昏EX9 小时前
Android SystemServer 系列专题【篇四:SystemServerInitThreadPool线程池管理】
android
用户20187928316710 小时前
pm path 和 dumpsys package 的区别
android
是店小二呀10 小时前
【C++】智能指针底层原理:引用计数与资源管理机制
android·java·c++