在 KSP 中默认生成代码的方式是通过CodeGenerator
创建文件流后以字符串拼接的方式来生成代码,对于简单的demo还好,但是对于实际生产项目中要生成的代码可能会十分复杂,如果还是自己手动去拼接,可能非常的繁琐,累死人不说,还非常容易出错,比如说少拼接了一个标点符号,可能需要排查半天。实际生产项目中使用的最多的就是由 JakeWharton 大神所写的著名的开源库 JavaPoet(很有诗意的名字,翻译过来叫Java诗人)使用该库可通过方便的函数进行拼接,减少出错。
KotlinPoet 是对应 JavaPoet 的 Kotlin 版本,同样是由square开发的,它可以用来很方便的生成 Kotlin 代码。
本文介绍 KotlinPoet 的使用,包括但不限于其官网文档中的内容,你也可以直接参考其官方文档:https://square.github.io/kotlinpoet/
在ksp模块的build.gradle中添加KotlinPoet的依赖:
dependencies {implementation 'com.squareup:kotlinpoet:1.12.0'
}
对应版本可以在Github上的KotlinPoet官网上查找。
例如:
val greeterClass = ClassName("com.example.generated", "Greeter")
val fileSpec = FileSpec.builder("com.example.generated", "HelloWorld").addType(TypeSpec.classBuilder(greeterClass).primaryConstructor(FunSpec.constructorBuilder().addParameter("name", String::class).build()).addProperty(PropertySpec.builder("name", String::class).initializer("name").build()).addFunction(FunSpec.builder("greet").addStatement("println(%P)", "Hello, \$name").build()).build()).addFunction(FunSpec.builder("main").addParameter("args", String::class, KModifier.VARARG).addStatement("%T(args[0]).greet()", greeterClass).build()).build()fileSpec.writeTo(System.out)
这会生成一个包含如下代码的HelloWorld.kt文件:
package com.example.generatedimport kotlin.String
import kotlin.Unitpublic class Greeter(public val name: String,
) {public fun greet(): Unit {println("""Hello, $name""")}
}public fun main(vararg args: String): Unit {Greeter(args[0]).greet()
}
是不是很简单,跟直接拼接的方式相比,可读性很好,而且更加安全。
KotlinPoet 根据不同的使用用途提供了不同的开箱即用的类:
生成目标 | 使用对象 |
---|---|
Kotlin 文件 | FileSpec ,可以调用其addType 、addFunction 、addImport 、addCode 、addProperty 等来生成文件内容 |
类、接口和对象 | TypeSpec ,可以调用其addModifiers 、addFunctions 、addProperty 等来生成主体内容 |
函数和构造函数 | FunSpec ,可以调用其 addModifiers 、addParameters 、addStatement 、addCode 等来生成函数内容 |
参数 | ParameterSpec |
属性 | PropertySpec |
注解 | AnnotationSpec |
类型别名 | TypeAliasSpec |
但是方法和构造函数的主体在 KotlinPoet 中没有建模,没有表达式类、语句类或语法树节点。KotlinPoet 可通过调用 addCode
方法传入一个字符串模板作为代码块生成方式,可以利用 Kotlin 的多行字符串使它看起来更漂亮:
val main = FunSpec.builder("main").addCode("""|var total = 0|for (i in 0 until 10) {| total += i|}|""".trimMargin()).build()
这样会生成如下代码:
fun main() {var total = 0for (i in 0 until 10) {total += i}
}
通过 addStatement
配合 beginControlFlow
和endControlFlow
可以进行更加灵活的流程控制代码生成:
private fun computeRange(name: String, from: Int, to: Int, op: String): FunSpec {return FunSpec.builder(name).returns(Int::class).addStatement("var result = 1").beginControlFlow("for (i in $from until $to)").addStatement("result = result $op i").endControlFlow().addStatement("return result").build()
}
例如当调用 computeRange("computeRange", 1, 100, "*")
时,会生成以下代码:
public fun computeRange(): Int {var result = 1for (i in 1 until 100) {result = result * i}return result
}
当使用字符串模板的方式生成代码时,使用%S
代表一个String
,它会完成包装引号和转义,例如:
fun main(args: Array) {val helloWorld = TypeSpec.classBuilder("HelloWorld").addFunction(whatsMyNameYo("slimShady")).addFunction(whatsMyNameYo("eminem")).addFunction(whatsMyNameYo("marshallMathers")).build()val kotlinFile = FileSpec.builder("com.example.helloworld", "HelloWorld").addType(helloWorld).build()kotlinFile.writeTo(System.out)
}private fun whatsMyNameYo(name: String): FunSpec {return FunSpec.builder(name).returns(String::class).addStatement("return %S", name).build()
}
这会生成以下代码:
class HelloWorld {fun slimShady(): String = "slimShady"fun eminem(): String = "eminem"fun marshallMathers(): String = "marshallMathers"
}
使用%S
会自动加上双引号。
%S
还会自动处理美元符号 ( $
) 的转义,以避免无意中创建字符串模板导致无法在生成的代码中编译:
val stringWithADollar = "Your total is " + "$" + "50"
val funSpec = FunSpec.builder("printTotal").returns(String::class).addStatement("return %S", stringWithADollar).build()
这会产生:
fun printTotal(): String = "Your total is ${'$'}50"
如果调用printTotal()
函数,就会输出Your total is $50
,可以看到美元符号被自动转义了,这很好,但是如果需要在拼接字符串模板时, $
用于引用变量,而不转义美元符号,请使用 %P
:
private fun stringTemplate(): FunSpec{val stringWithADollar = "Your total is " + "\${amount}"return FunSpec.builder("printTotal").addParameter("amount", String::class).returns(String::class).addStatement("return %P", stringWithADollar).build()
}
这会产生:
public fun printTotal(amount: String): String = """Your total is ${amount}"""
这样就是动态输出amount
变量的值了。
还可以将CodeBlocks
用作 %P
的参数,这在需要在字符串模板中引用可导入类型或成员时非常方便:
val file = FileSpec.builder("com.example", "Digits").addFunction(FunSpec.builder("print").addParameter("digits", IntArray::class).addStatement("println(%P)", buildCodeBlock {val contentToString = MemberName("kotlin.collections", "contentToString")add("These are the digits: \${digits.%M()}", contentToString)}).build()).build()
println(file)
上面的代码片段将产生以下输出,会正确的处理导入:
package com.exampleimport kotlin.IntArray
import kotlin.collections.contentToStringfun print(digits: IntArray) {println("""These are the digits: ${digits.contentToString()}""")
}
KotlinPoet 对类型有丰富的内置支持,包括 import
语句的自动生成。仅用于%T
引用类型:
val today = FunSpec.builder("today").returns(Date::class).addStatement("return %T()", Date::class).build()val helloWorld = TypeSpec.classBuilder("HelloWorld").addFunction(today).build()val kotlinFile = FileSpec.builder("com.example.helloworld", "HelloWorld").addType(helloWorld).build()kotlinFile.writeTo(System.out)
这会生成以下.kt
文件,其中包含必要内容的import
:
package com.example.helloworldimport java.util.Dateclass HelloWorld {fun today(): Date = Date()
}
上面我们通过Date::class
引用了一个我们在编写生成代码时恰好可用的类。但是我们也可以引用一个在编写生成代码时还不存在的类:
val hoverboard = ClassName("com.mattel", "Hoverboard")val tomorrow = FunSpec.builder("tomorrow").returns(hoverboard).addStatement("return %T()", hoverboard).build()
这会生成以下代码,那个还不存在的类也被导入了:
package com.example.helloworldimport com.mattel.Hoverboardclass HelloWorld {fun tomorrow(): Hoverboard = Hoverboard()
}
由于类型非常重要,在使用 KotlinPoet 时会经常需要到 ClassName
。它可以识别任何声明的类。声明类型只是 Kotlin 丰富类型系统的开始:我们还有数组、参数化类型、通配符类型、lambda 类型和类型变量。KotlinPoet 具有用于构建以下各项的类:
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedByval hoverboard = ClassName("com.mattel", "Hoverboard")
val list = ClassName("kotlin.collections", "List")
val arrayList = ClassName("kotlin.collections", "ArrayList")
val listOfHoverboards = list.parameterizedBy(hoverboard)
val arrayListOfHoverboards = arrayList.parameterizedBy(hoverboard)val thing = ClassName("com.misc", "Thing")
val array = ClassName("kotlin", "Array")
val producerArrayOfThings = array.parameterizedBy(WildcardTypeName.producerOf(thing))val beyond = FunSpec.builder("beyond").returns(listOfHoverboards).addStatement("val result = %T()", arrayListOfHoverboards).addStatement("result += %T()", hoverboard).addStatement("result += %T()", hoverboard).addStatement("result += %T()", hoverboard).addStatement("return result").build()val printThings = FunSpec.builder("printThings").addParameter("things", producerArrayOfThings).addStatement("println(things)").build()
这会生成以下代码,KotlinPoet 将分解每种类型并在可能的情况下将其导入:
package com.example.helloworldimport com.mattel.Hoverboard
import com.misc.Thing
import kotlin.Array
import kotlin.collections.ArrayList
import kotlin.collections.Listclass HelloWorld {fun beyond(): List {val result = ArrayList()result += Hoverboard()result += Hoverboard()result += Hoverboard()return result}fun printThings(things: Array) {println(things)}
}
KotlinPoet 支持可空类型。要将一个 TypeName
转换成可为 null
的对应项,请使用copy(nullable = true)
方法:
val name = PropertySpec.builder("name", String::class.asTypeName().copy(nullable = true)).mutable().addModifiers(KModifier.PRIVATE).initializer("null").build()val address = PropertySpec.builder("address", String::class).addModifiers(KModifier.PRIVATE).initializer("%S", "china").build()TypeSpec.classBuilder("HelloWorld").addProperty(name).addProperty(address)//.addProperty("address", String::class, KModifier.PRIVATE).build()
这会生成以下代码:
class HelloWorld {private var name: String? = nullprivate val address: String = "china"
}
与ClassName
类似,KotlinPoet 有一个特殊的成员占位符(函数和属性),当代码需要访问顶级成员和在对象内部声明的成员时,它会派上用场。用%M
引用成员时,需要传递一个MemberName
实例作为占位符的参数,KotlinPoet 将自动处理导入:
package com.squareup.tacosclass Taco {}
fun createTaco() = Taco()
val Taco.isVegan: Booleanget() = false
val createTaco = MemberName("com.squareup.tacos", "createTaco")
val isVegan = MemberName("com.squareup.tacos", "isVegan")
val file = FileSpec.builder("com.squareup.example", "TacoTest").addFunction(FunSpec.builder("main").addStatement("val taco = %M()", createTaco).addStatement("println(taco.%M)", isVegan).build()).build()
println(file)
这会生成以下文件:
package com.squareup.exampleimport com.squareup.tacos.createTaco
import com.squareup.tacos.isVeganfun main() {val taco = createTaco()println(taco.isVegan)
}
如您所见,%M
可以用于扩展函数和属性引用。只需要确保没有名称冲突的情况下导入成员,否则导入失败将导致代码生成器无法通过编译。不过,有一种方法可以解决这种名称冲突情况:使用FileSpec.addAliasedImport()
用于为冲突创建别名 MemberName
:
val createTaco = MemberName("com.squareup.tacos", "createTaco")
val createCake = MemberName("com.squareup.cakes", "createCake")
val isTacoVegan = MemberName("com.squareup.tacos", "isVegan")
val isCakeVegan = MemberName("com.squareup.cakes", "isVegan")
val file = FileSpec.builder("com.squareup.example", "Test").addAliasedImport(isTacoVegan, "isTacoVegan").addAliasedImport(isCakeVegan, "isCakeVegan").addFunction(FunSpec.builder("main").addStatement("val taco = %M()", createTaco).addStatement("val cake = %M()", createCake).addStatement("println(taco.%M)", isTacoVegan).addStatement("println(cake.%M)", isCakeVegan).build()).build()
println(file)
KotlinPoet 将为以下内容生成别名导入:
package com.squareup.exampleimport com.squareup.cakes.createCake
import com.squareup.tacos.createTaco
import com.squareup.cakes.isVegan as isCakeVegan
import com.squareup.tacos.isVegan as isTacoVeganfun main() {val taco = createTaco()val cake = createCake()println(taco.isTacoVegan)println(cake.isCakeVegan)
}
请注意 %M
只能用于引用MemberName
不能用于引用ClassName
,如果你在 %M
占位处传了一个ClassName
会build时报类型转换异常。
MemberName
还支持运算符,您可以使用 MemberName(String, KOperator)
或 MemberName(ClassName, KOperator)
导入和引用运算符。
val taco = ClassName("com.squareup.tacos", "Taco")
val meat = ClassName("com.squareup.tacos.ingredient", "Meat")
val iterator = MemberName("com.squareup.tacos.internal", KOperator.ITERATOR)
val minusAssign = MemberName("com.squareup.tacos.internal", KOperator.MINUS_ASSIGN)
val file = FileSpec.builder("com.example", "Test").addFunction(FunSpec.builder("makeTacoHealthy").addParameter("taco", taco).beginControlFlow("for (ingredient %M taco)", iterator).addStatement("if (ingredient is %T) taco %M ingredient", meat, minusAssign).endControlFlow().addStatement("return taco").build()).build()
println(file)
这会生成以下代码:
package com.exampleimport com.squareup.tacos.Taco
import com.squareup.tacos.ingredient.Meat
import com.squareup.tacos.internal.iterator
import com.squareup.tacos.internal.minusAssignfun makeTacoHealthy(taco: Taco) {for (ingredient in taco) {if (ingredient is Meat) taco -= ingredient}return taco
}
KOperator中目前定义了如下可用的运算符:
public enum class KOperator(internal val operator: String,internal val functionName: String
) {UNARY_PLUS("+", "unaryPlus"),PLUS("+", "plus"),UNARY_MINUS("-", "unaryMinus"),MINUS("-", "minus"),TIMES("*", "times"),DIV("/", "div"),REM("%", "rem"),PLUS_ASSIGN("+=", "plusAssign"),MINUS_ASSIGN("-=", "minusAssign"),TIMES_ASSIGN("*=", "timesAssign"),DIV_ASSIGN("/=", "divAssign"),REM_ASSIGN("%=", "remAssign"),INC("++", "inc"),DEC("--", "dec"),EQUALS("==", "equals"),NOT_EQUALS("!=", "equals"),NOT("!", "not"),RANGE_TO("..", "rangeTo"),CONTAINS("in", "contains"),NOT_CONTAINS("!in", "contains"),GT(">", "compareTo"),LT("<", "compareTo"),GE(">=", "compareTo"),LE("<=", "compareTo"),ITERATOR("in", "iterator"),
}
生成的代码通常是自引用的。可用%N
通过名称引用另一个已生成的声明。例如下面代码中byteToHex
方法调用了hexDigit
方法:
fun byteToHex(b: Int): String {val result = CharArray(2)result[0] = hexDigit((b ushr 4) and 0xf)result[1] = hexDigit(b and 0xf)return String(result)
}fun hexDigit(i: Int): Char {return (if (i < 10) i + '0'.toInt() else i - 10 + 'a'.toInt()).toChar()
}
在生成上面的代码时,我们将创建好的hexDigit()
方法的FunSpec
作为参数,传递给构建 byteToHex()
方法的 %N
占位符:
val hexDigit = FunSpec.builder("hexDigit").addParameter("i", Int::class).returns(Char::class).addStatement("return (if (i < 10) i + '0'.toInt() else i - 10 + 'a'.toInt()).toChar()").build()val byteToHex = FunSpec.builder("byteToHex").addParameter("b", Int::class).returns(String::class).addStatement("val result = CharArray(2)").addStatement("result[0] = %N((b ushr 4) and 0xf)", hexDigit).addStatement("result[1] = %N(b and 0xf)", hexDigit).addStatement("return String(result)").build()
%N
提供的另一个方便的功能是自动转义包含带双引号的非法标识符字符的名称。假设你在代码中不小心使用了 Kotlin 的关键字作为名字创建了一个MemberName
:
val taco = ClassName("com.squareup.tacos", "Taco")
val packager = ClassName("com.squareup.tacos", "TacoPackager")
val file = FileSpec.builder("com.example", "Test").addFunction(FunSpec.builder("packageTacos").addParameter("tacos", LIST.parameterizedBy(taco)).addParameter("packager", packager).addStatement("packager.%N(tacos)", packager.member("package")).build()).build()
%N
将为您转义名称,确保输出将通过编译:
package com.exampleimport com.squareup.tacos.Taco
import com.squareup.tacos.TacoPackager
import kotlin.collections.Listfun packageTacos(tacos: List, packager: TacoPackager) {packager.`package`(tacos)
}
虽然 Kotlin 的字符串模板通常在您想要将字面量包含到生成的代码中时效果很好,但 KotlinPoet 提供了额外的语法,灵感来自但不兼容 String.format()
。它接受在输出%L
中发出一个字面量值。这就像是Formatter:%s
的用法
private fun computeRange(name: String, from: Int, to: Int, op: String): FunSpec {return FunSpec.builder(name).returns(Int::class).addStatement("var result = 0").beginControlFlow("for (i in %L until %L)", from, to).addStatement("result = result %L i", op).endControlFlow().addStatement("return result").build()
}
字面量将被直接发送到输出代码,没有转义。字面量支持的参数类型可以是String
、基本类型
和下面将要提到的几个KotlinPoet 类型
。
代码块可以通过几种方式指定其占位符的值。代码块上的每个操作只能使用一种样式。
在格式字符串中将每个占位符的参数值传递给CodeBlock.add()
方法
CodeBlock.builder().add("I ate %L %L", 3, "tacos")
可在格式字符串中的占位符之前放置一个整数索引(从 1
开始)以指定要使用的参数。
CodeBlock.builder().add("I ate %2L %1L", "tacos", 3)
使用语法%argName:X
,其中X
是格式字符的占位符,通过使用CodeBlock.addNamed()
方法为X
传递一个map
参数,map
必须包含格式化字符串中所有argName
参数名的key
。
val map = LinkedHashMap()
map += "food" to "tacos"
map += "count" to 3
CodeBlock.builder().addNamed("I ate %count:L %food:L", map)
其中,参数名argName
的命名规则:字符a-z, a-z, 0-9
和_
,并且必须以小写字符开头。
创建一个简单的类很容易,使用TypeSpec.classBuilder
即可:
TypeSpec.classBuilder(greeterClass).addModifiers(KModifier.OPEN).primaryConstructor(FunSpec.constructorBuilder().addParameter("name", String::class).build()).addProperty(PropertySpec.builder("name", String::class).addModifiers(KModifier.PRIVATE).initializer("name").build()).addFunction(FunSpec.builder("greet").addStatement("println(%P)", "Hello, \$name").build()) .build()
这会生成:
public open class Greeter(private val name: String) {public fun greet(): Unit {println("""Hello, $name""")}
}
使用TypeSpec.classBuilder
之后可以调用addProperty
和addFunction
为其添加属性和函数,并且还可以继续在上面调用addType
来添加子类和接口。
注意类的构造函数参数和属性如果想达到上面那样跟在类名后面合并的效果,就必须同时设置primaryConstructor
和addProperty
,否则在kotlin中二者是分开的。
前面代码中在类和属性上调用addModifiers
添加了修饰符,对于可见性,如果不设置,默认是public
, 但addModifiers
可以添加很多其他Kotlin支持的修饰符。
以下是 KModifier 枚举类中可用的修饰符:
修饰符 | 作用目标 |
---|---|
PUBLIC PROTECTED PRIVATE INTERNAL FINAL OPEN ABSTRACT EXTERNAL EXPECT ACTUAL | Class 类 、Property 属性 、Function 函数 |
SEALED INNER ENUM DATA VALUE ANNOTATION COMPANION | Class 类 |
CONST LATEINIT | Property 属性 |
OVERRIDE | Property 属性 、Function 函数 |
SUSPEND INLINE TAILREC INFIX OPERATOR | Function 函数 |
VARARG NOINLINE CROSSINLINE | Parameter 参数 |
REIFIED | 内联函数上的泛型参数 |
IN OUT | 类上的泛型参数:逆变、协变 |
FUN | interface 接口 |
前面通过FunSpec.builder
创建的所有函数都有函数体代码,如果要生成没有函数体的抽象函数,请使用 FunSpec.builder
的 addModifiers()
方法设置KModifier.ABSTRACT
这只有在生成抽象类或接口的函数时才是合法的。
val flux = FunSpec.builder("flux").addModifiers(KModifier.ABSTRACT, KModifier.PROTECTED).build()val helloWorld = TypeSpec.classBuilder("HelloWorld").addModifiers(KModifier.ABSTRACT).addFunction(flux).build()
这会生成以下代码:
abstract class HelloWorld {protected abstract fun flux()
}
其他修饰符在允许的情况下起作用。
Kotlin中的函数还拥有参数、可变参数、KDoc、注解、类型变量、返回类型和扩展函数的接收者类型等。而所有这些在FunSpec.Builder
中都有相关的配置项可使用。
可以通过FunSpec.builder
指定一个receiver
来生成扩展函数
val square = FunSpec.builder("square").receiver(Int::class).returns(Int::class).addStatement("var s = this * this").addStatement("return s").build()
这会输出:
fun Int.square(): Int {val s = this * thisreturn s
}
KotlinPoet 可以正确的识别单行表达式函数,它将每个函数的主体以 return
开头作为单行表达式函数:
val abs = FunSpec.builder("abs").addParameter("x", Int::class).returns(Int::class).addStatement("return if (x < 0) -x else x").build()
这会输出:
fun abs(x: Int): Int = if (x < 0) -x else x
考虑下面的例子,函数参数b
的默认值为 0
以避免重载此函数。
fun add(a: Int, b: Int = 0) {print("a + b = ${a + b}")
}
要声明一个函数参数的默认值,可以使用ParameterSpec.builder
的defaultValue()
方法来设置,并将ParameterSpec
构建生成添加到FunSpec.builder.addParameter()
方法中即可。
FunSpec.builder("add").addParameter("a", Int::class).addParameter(ParameterSpec.builder("b", Int::class).defaultValue("%L", 0).build()).addStatement("print(\"a + b = ${a + b}\")").build()
为了提供有意义的格式,KotlinPoet 会在代码行数超过长度限制的情况下用新的行符号替换代码块中的空格。我们以这个函数为例:
val funSpec = FunSpec.builder("foo").addStatement("return (100..10000).map { number -> number * number }.map { number -> number.toString() }.also { string -> println(string) }").build()
它最终可能会像这样打印出来:
fun foo() = (100..10000).map { number -> number * number }.map { number -> number.toString() }.also
{ string -> println(string) }
不幸的是,这段代码会编译报错:由于also
后面被换行了,编译器期望 also
和 {
位于同一行。KotlinPoet 无法理解表达式的上下文并修复格式,但是可以使用一个技巧来声明不间断空格:在原本要使用空格的地方使用 ·
符号。将上面示例修改如下:
val funSpec = FunSpec.builder("foo").addStatement("return (100..10000).map·{ number -> number * number }.map·{ number -> number.toString() }.also·{ string -> println(string) }").build()
这将产生以下结果:
fun foo() = (100..10000).map { number -> number * number }.map { number ->number.toString()
}.also { string -> println(string) }
代码现在就可以被正确的编译。虽然它看起来仍然不够完美,你可以尝试用 ·
符号替换代码块中的剩余的空格以获得更好看的格式。
FunSpec
也可以用于构造函数,通过调用其FunSpec.constructorBuilder()
方法:
val flux = FunSpec.constructorBuilder().addParameter("greeting", String::class).addStatement("this.%N = %N", "greeting", "greeting").build()val helloWorld = TypeSpec.classBuilder("HelloWorld").addProperty("greeting", String::class, KModifier.PRIVATE).addFunction(flux).build()
这会输出:
class HelloWorld {private val greeting: Stringconstructor(greeting: String) {this.greeting = greeting}
}
在大多数情况下,构造函数就像方法一样工作。发出代码时,KotlinPoet 会将构造函数放在输出文件中的方法之前。
很多时候你需要为一个类生成主构造函数:
val helloWorld = TypeSpec.classBuilder("HelloWorld").primaryConstructor(flux).addProperty("greeting", String::class, KModifier.PRIVATE).build()
但是,此代码会生成以下内容:
class HelloWorld(greeting: String) {private val greeting: Stringinit {this.greeting = greeting}
}
默认情况下,KotlinPoet 不会合并主构造函数参数和属性,即使它们共享相同的名称。要达到这个效果,你必须告诉 KotlinPoet 该属性是通过构造函数的参数初始化的:
val flux = FunSpec.constructorBuilder().addParameter("greeting", String::class).build()val helloWorld = TypeSpec.classBuilder("HelloWorld").primaryConstructor(flux).addProperty(PropertySpec.builder("greeting", String::class) // 声明一个属性.initializer("greeting") // 指定该属性的初始化来源是构造函数中的greeting参数.addModifiers(KModifier.PRIVATE).build()).build()
在Kotlin中成员属性Field
和 显示声明的constructor
构造函数 或者 类名后面不带val
的构造函数 中的参数
是两个不同的东西。
现在我们得到以下输出:
class HelloWorld(private val greeting: String)
请注意,KotlinPoet 会省略具有空主体的类的 { }
。
要在函数和构造函数上声明参数,可以使用 FunSpec.addParameter()
或者通过ParameterSpec.builder()
来创建一个参数:
val android = ParameterSpec.builder("android", String::class).defaultValue("\"pie\"") // 默认参数值.build()val welcomeOverlords = FunSpec.builder("welcomeOverlords").addParameter(android).addParameter("robot", String::class).build()
上面的代码生成:
fun welcomeOverlords(android: String = "pie", robot: String) {
}
当参数上面有注解时(比如@Inject
),扩展的Builder
是很必要的。
与参数一样,可以使用PropertySpec.builder
构造器或使用addProperty
方法来创建Field
属性字段:
val android = PropertySpec.builder("android", String::class).addModifiers(KModifier.PRIVATE).build()val helloWorld = TypeSpec.classBuilder("HelloWorld").addProperty(android).addProperty("robot", String::class, KModifier.PRIVATE).build()
这会产生:
class HelloWorld {private val android: Stringprivate val robot: String
}
当Field
字段具有 KDoc、注解或字段初始值设定项时,扩展形式的Builder是必需的。Field
字段的初始值设置方式与前面提到的在CodeBlock
中使用的有点类似String.format()
的语法一样:
val android = PropertySpec.builder("android", String::class).addModifiers(KModifier.PRIVATE).initializer("%S + %L", "Oreo v.", 8.1).build()
这会产生:
private val android: String = "Oreo v." + 8.1
默认情况下PropertySpec.Builder
生成val
属性,如果你需要生成 var
属性,请使用 mutable()
:
val android = PropertySpec.builder("android", String::class).mutable().addModifiers(KModifier.PRIVATE).initializer("%S + %L", "Oreo v.", 8.1).build()
KotlinPoet 对内联属性建模的方式有点特殊。以下代码片段:
val android = PropertySpec.builder("android", String::class).mutable().addModifiers(KModifier.INLINE).build()
会产生错误:
java.lang.IllegalArgumentException: KotlinPoet doesn't allow setting the inline modifier on
properties. You should mark either the getter, the setter, or both inline.
实际上,标有 inline
的属性应该至少需要有一个将由编译器内联的 accessor
访问器。让我们为这个属性添加一个 getter
:
val android = PropertySpec.builder("android", String::class).mutable().getter(FunSpec.getterBuilder().addModifiers(KModifier.INLINE).addStatement("return %S", "foo").build()).build()
现在结果如下:
var android: kotlin.Stringinline get() = "foo"
现在,如果我们想为上面的属性添加一个非内联 setter
怎么办?我们可以在不修改之前编写的任何代码的情况下这样做:
val android = PropertySpec.builder("android", String::class).mutable().getter(FunSpec.getterBuilder().addModifiers(KModifier.INLINE).addStatement("return %S", "foo").build()).setter(FunSpec.setterBuilder().addParameter("value", String::class).build()).build()
我们得到了预期的结果:
var android: kotlin.Stringinline get() = "foo"set(`value`) {}
最后,此时如果我们为 setter
添加一个 KModifier.INLINE
,KotlinPoet 可以很好地包装它并产生以下结果:
inline var android: kotlin.Stringget() = "foo"set(`value`) {}
这时如果从 getter
或 setter
中删除修饰符将使表达式回到前面的样子。
另一方面,如果 KotlinPoet 允许inline
直接标记一个属性,程序员将不得不在访问器的状态发生变化时手动添加/删除修饰符,以获得正确且可编译的输出。我们通过使访问器成为inline
修饰符的真实来源来解决这个问题。
KotlinPoet 通过ypeSpec.interfaceBuilder
来创建一个接口。请注意,接口方法的Modifier
修饰符必须始终是ABSTRACT
,在定义接口方法时需要总是显示的指定该修饰符:
val helloWorld = TypeSpec.interfaceBuilder("HelloWorld").addProperty("buzz", String::class).addFunction(FunSpec.builder("beep").addModifiers(KModifier.ABSTRACT).build()).build()
但是这些修饰符在生成代码的时候就被省略了。这些是默认设置,因此我们不需要包含它们,这正是kotlinc
编译器为我们提供的方便!
interface HelloWorld {val buzz: Stringfun beep()
}
生成带泛型的接口类:
private fun generateInterface() : TypeSpec {val K = TypeVariableName("K")val T = TypeVariableName("T")return TypeSpec.interfaceBuilder("Hello").addTypeVariable(K).addTypeVariable(T).addProperty("buzz", String::class).addFunction(FunSpec.builder("beep").addModifiers(KModifier.ABSTRACT).addParameter("a", K).returns(T).build()).build()
}
这里主要通过addTypeVariable
为接口添加一个TypeVariableName
即可,使用到泛型的接口方法也可以直接将TypeVariableName
作为TypeName
来使用。
这会生成:
public interface Hello {public val buzz: Stringpublic fun beep(a: K): T
}
Kotlin 1.4
通过fun interface
语法添加了对函数接口的支持。要在 KotlinPoet 中创建它,请使用TypeSpec.funInterfaceBuilder()
val helloWorld = TypeSpec.funInterfaceBuilder("HelloWorld").addFunction(FunSpec.builder("beep").addModifiers(KModifier.ABSTRACT).build()).build()// Generates...
fun interface HelloWorld {fun beep()
}
通过 TypeSpec.objectBuilder
来创建一个 kotlin 的 Object
对象
val helloWorld = TypeSpec.objectBuilder("HelloWorld").addProperty(PropertySpec.builder("buzz", String::class).initializer("%S", "buzz").build()).addFunction(FunSpec.builder("beep").addStatement("println(%S)", "Beep!").build()).build()
这会生成:
public object HelloWorld {public val buzz: String = "buzz"public fun beep(): Unit {println("Beep!")}
}
同样,可以通过TypeSpec.companionObjectBuilder()
创建一个伴生对象并随后调用addType()
将它们添加到类构造器中:
val companion = TypeSpec.companionObjectBuilder().addProperty(PropertySpec.builder("buzz", String::class).initializer("%S", "buzz").build()).addFunction(FunSpec.builder("beep").addStatement("println(%S)", "Beep!").build()).build()val helloWorld = TypeSpec.classBuilder("HelloWorld").addType(companion).build()
注意,伴生对象一般是用于创建到一个类中的,随后可以通过所属的类名直接访问它,因此伴生对象一般不需要名字。但是你仍然可以通过TypeSpec.companionObjectBuilder(“xxx”)
为伴随对象提供一个可选名称。
要添加一个父类请使用 TypeSpec.classBuilder().superclass()
方法
TypeSpec.classBuilder(ClassName("com.example.generated", "ChildClass")).superclass(Date::class) .addFunction(FunSpec.builder("foo").returns(String::class).addStatement("return %S", "aaa").build()).build()
这会生成:
package com.example.generated
import java.util.Date
import kotlin.Stringpublic class ChildClass : Date() {public fun foo(): String = "aaa"
}
若添加父类是一个编写时不能直接引用的类,使用ClassName
来创建即可:
val className = ClassName("com.example.generated", "ChildClass")
val parentClassName = ClassName("com.fly.compose.ksp.application", "ParentClass")
TypeSpec.classBuilder(className) .superclass(parentClassName) .addFunction(FunSpec.builder("foo").returns(String::class).addStatement("return %S", "aaa").build()).build()
superclass()
默认会添加一个空的父类构造函数,如果父类有有参数的构造函数,请使用 addSuperclassConstructorParameter
来添加父类有参构造函数的参数:
val className = ClassName("com.example.generated", "ChildClass")
val parentClassName = ClassName("com.fly.compose.ksp.application", "ParentClass")
return TypeSpec.classBuilder(className) .superclass(parentClassName).addSuperclassConstructorParameter("%S, %L", "aaa", 123).addFunction(FunSpec.builder("foo").returns(String::class).addStatement("return %S", "aaa").build()).build()
这会生成:
package com.example.generated
import com.fly.compose.ksp.application.ParentClass
import kotlin.Stringpublic class ChildClass : ParentClass("aaa", 123) {public fun foo(): String = "aaa"
}
要添加一个父接口,请使用 TypeSpec.classBuilder().addSuperinterface()
private fun addSuperInterface() : TypeSpec {val className = ClassName("com.example.generated", "Child")val parentInterface = ClassName("com.fly.compose.ksp.application", "MyInterface")return TypeSpec.classBuilder(className).addSuperinterface(parentInterface).addModifiers(KModifier.ABSTRACT).addFunction(FunSpec.builder("foo").returns(String::class).addStatement("return %S", "aaa").build()).build()
}
这会生成:
package com.example.generated
import com.fly.compose.ksp.application.MyInterface
import kotlin.Stringpublic abstract class Child : MyInterface {public fun foo(): String = "aaa"
}
这里创建的类修饰符声明为抽象类ABSTRACT
,如果不是抽象类,而是普通类,则需要为该类添加实现方法:
private fun addSuperInterface() : TypeSpec {val className = ClassName("com.example.generated", "Child")val parentInterface = ClassName("com.fly.compose.ksp.application", "MyInterface")return TypeSpec.classBuilder(className).addSuperinterface(parentInterface).addFunction(FunSpec.builder("getDescription").returns(String::class).addModifiers(KModifier.OVERRIDE).addParameter("money", Int::class).addStatement("val result = %P", "Total money is " + "\${money}.").addStatement("return result").build()).addFunction(FunSpec.builder("foo").returns(String::class).addStatement("return %S", "aaa").build()).build()
}
注意覆写接口的方法需要指定OVERRIDE
修饰符。
这会生成:
package com.example.generated
import com.fly.compose.ksp.application.MyInterface
import kotlin.Int
import kotlin.Stringpublic class Child : MyInterface {public override fun getDescription(money: Int): String {val result = """Total money is ${money}."""return result}public fun foo(): String = "aaa"
}
如果要添加的父接口带有泛型参数,可以在addSuperinterface
时使用plusParameter
:
private fun addSuperInterface2() : TypeSpec {val className = ClassName("com.example.generated", "Engine")val parentInterface = ClassName("com.fly.compose.ksp.application", "Feature")val T = ClassName("com.fly.compose.ksp.application", "Type")return TypeSpec.classBuilder(className).addSuperinterface(parentInterface.plusParameter(T)).addModifiers(KModifier.ABSTRACT).addFunction(FunSpec.builder("foo").returns(String::class).addStatement("return %S", "aaa").build()).build()
}
这会生成:
package com.example.generated
import com.fly.compose.ksp.application.Feature
import kotlin.Stringpublic abstract class Engine : Feature {public fun foo(): String = "aaa"
}
如果是多个泛型参数怎么办,你可以选择parentInterface.plusParameter(T).plusParameter(R)
,还有一个更好用的方法 parameterizedBy(T, R, ...)
, 例如:
private fun addSuperInterface() : TypeSpec {val className = ClassName("com.example.generated", "Engine")val parentInterface = ClassName("com.fly.compose.ksp.application", "Feature")val T = ClassName("com.fly.compose.ksp.application", "Type")val R = ClassName("com.fly.compose.ksp.application", "Result")return TypeSpec.classBuilder(className).addSuperinterface(parentInterface.parameterizedBy(T, R)).addFunction(FunSpec.builder("transform").addModifiers(KModifier.OVERRIDE).returns(R).addParameter("a", T).addStatement("val result = %T(a)", R).addStatement("return result").build()).build()
}
这会生成:
package com.example.generated
import com.fly.compose.ksp.application.Feature
import com.fly.compose.ksp.application.Result
import com.fly.compose.ksp.application.Typepublic class Engine : Feature {public override fun transform(a: Type): Result {val result = Result(a)return result}
}
此外parameterizedBy
在需要获取一些集合类的泛型类型时非常有用,例如:
// For: List
val stringList = ClassName("kotlin.collections", "List").parameterizedBy(String::class.asTypeName())
使用TypeSpec.enumBuilder
创建枚举类型,并使用addEnumConstant()
创建每个枚举值:
val helloWorld = TypeSpec.enumBuilder("Roshambo").addEnumConstant("ROCK").addEnumConstant("SCISSORS").addEnumConstant("PAPER").build()
这会生成:
enum class Roshambo {ROCK,SCISSORS,PAPER
}
支持花式枚举,其中枚举值覆盖方法或调用超类构造函数。这是一个综合示例:
val helloWorld = TypeSpec.enumBuilder("Roshambo").primaryConstructor(FunSpec.constructorBuilder().addParameter("handsign", String::class).build()).addEnumConstant("ROCK", TypeSpec.anonymousClassBuilder().addSuperclassConstructorParameter("%S", "fist").addFunction(FunSpec.builder("toString").addModifiers(KModifier.OVERRIDE).addStatement("return %S", "avalanche!").returns(String::class).build()).build()).addEnumConstant("SCISSORS", TypeSpec.anonymousClassBuilder().addSuperclassConstructorParameter("%S", "peace").build()).addEnumConstant("PAPER", TypeSpec.anonymousClassBuilder().addSuperclassConstructorParameter("%S", "flat").build()).addProperty(PropertySpec.builder("handsign", String::class, KModifier.PRIVATE).initializer("handsign").build()).build()
这会生成:
enum class Roshambo(private val handsign: String) {ROCK("fist") {override fun toString(): String = "avalanche!"},SCISSORS("peace"),PAPER("flat");
}
在枚举代码中,我们使用了TypeSpec.anonymousClassBuilder()
匿名内部类也可以用在代码块中。它们是可以通过%L
引用的值:
val comparator = TypeSpec.anonymousClassBuilder().addSuperinterface(Comparator::class.parameterizedBy(String::class)).addFunction(FunSpec.builder("compare").addModifiers(KModifier.OVERRIDE).addParameter("a", String::class).addParameter("b", String::class).returns(Int::class).addStatement("return %N.length - %N.length", "a", "b").build()).build()val helloWorld = TypeSpec.classBuilder("HelloWorld").addFunction(FunSpec.builder("sortByLength").addParameter("strings", List::class.parameterizedBy(String::class)).addStatement("%N.sortedWith(%L)", "strings", comparator).build()).build()
这会生成一个带有匿名内部类的方法:
class HelloWorld {fun sortByLength(strings: List) {strings.sortedWith(object : Comparator {override fun compare(a: String, b: String): Int = a.length - b.length})}
}
定义匿名内部类的一个特别棘手的部分是超类构造函数的参数。要传递它们,请使用TypeSpec.Builder
的addSuperclassConstructorParameter()
方法。
生成简单的注解很容易,例如可以通过 FunSpec.builder
的 addAnnotation()
来为函数生成添加一个注解:
val test = FunSpec.builder("test string equality").addAnnotation(Test::class).addStatement("assertThat(%1S).isEqualTo(%1S)", "foo").build()
这会生成一个带有@Test
注解的函数:
@Test
fun `test string equality`() {assertThat("foo").isEqualTo("foo")
}
可以用 AnnotationSpec.builder()
的 addMember()
设置注解的属性:
val logRecord = FunSpec.builder("recordEvent").addModifiers(KModifier.ABSTRACT).addAnnotation(AnnotationSpec.builder(Headers::class).addMember("accept = %S", "application/json; charset=utf-8").addMember("userAgent = %S", "Square Cash").build()).addParameter("logRecord", LogRecord::class).returns(LogReceipt::class).build()
这会生成一个带有accept
and userAgent
属性的@Headers
注解:
@Headers(accept = "application/json; charset=utf-8",userAgent = "Square Cash"
)
abstract fun recordEvent(logRecord: LogRecord): LogReceipt
如果你喜欢, AnnotationSpec.builder
的 addMember()
还可以生成嵌套的注解:
val headerList = ClassName("", "HeaderList")
val header = ClassName("", "Header")
val logRecord = FunSpec.builder("recordEvent").addModifiers(KModifier.ABSTRACT).addAnnotation(AnnotationSpec.builder(headerList).addMember("[\n⇥%L,\n%L⇤\n]",AnnotationSpec.builder(header).addMember("name = %S", "Accept").addMember("value = %S", "application/json; charset=utf-8").build(),AnnotationSpec.builder(header).addMember("name = %S", "User-Agent").addMember("value = %S", "Square Cash").build()).build()).addParameter("logRecord", logRecordName).returns(logReceipt).build()
这会生成:
@HeaderList([Header(name = "Accept", value = "application/json; charset=utf-8"),Header(name = "User-Agent", value = "Square Cash")]
)
abstract fun recordEvent(logRecord: LogRecord): LogReceipt
KotlinPoet 支持设置注解的 use-site target
:
val utils = FileSpec.builder("com.example", "Utils").addAnnotation(AnnotationSpec.builder(JvmName::class).useSiteTarget(UseSiteTarget.FILE).build()).addFunction(FunSpec.builder("abs").receiver(Int::class).returns(Int::class).addStatement("return if (this < 0) -this else this").build()).build()
会生成如下代码:
@file:JvmNamepackage com.exampleimport kotlin.Int
import kotlin.jvm.JvmNamefun Int.abs(): Int = if (this < 0) -this else this
AnnotationSpec.UseSiteTarget
目前有如下枚举:
public enum class UseSiteTarget(internal val keyword: String) {FILE("file"),PROPERTY("property"),FIELD("field"),GET("get"),SET("set"),RECEIVER("receiver"),PARAM("param"),SETPARAM("setparam"),DELEGATE("delegate"),}
它拥有跟 kotlin 的AnnotationTarget
相对应的大部分枚举值:
public enum class AnnotationTarget {/** Class, interface or object, annotation class is also included */CLASS,/** Annotation class only */ANNOTATION_CLASS,/** Generic type parameter */TYPE_PARAMETER,/** Property */PROPERTY,/** Field, including property's backing field */FIELD,/** Local variable */LOCAL_VARIABLE,/** Value parameter of a function or a constructor */VALUE_PARAMETER,/** Constructor only (primary or secondary) */CONSTRUCTOR,/** Function (constructors are not included) */FUNCTION,/** Property getter only */PROPERTY_GETTER,/** Property setter only */PROPERTY_SETTER,/** Type usage */TYPE,/** Any expression */EXPRESSION,/** File */FILE,/** Type alias */@SinceKotlin("1.1")TYPEALIAS
}
KotlinPoet 提供了用于创建类型别名的 API ,这些API主要包括TypeVariableName
、parameterizedBy
及LambdaTypeName
,通过它们可以支持简单的类名、泛型和 lambda 函数类型:
val k = TypeVariableName("K")
val t = TypeVariableName("T")val fileTable = Map::class.asClassName().parameterizedBy(k, Set::class.parameterizedBy(File::class))val predicate = LambdaTypeName.get(parameters = arrayOf(t),returnType = Boolean::class.asClassName()
)
val helloWorld = FileSpec.builder("com.example", "HelloWorld").addTypeAlias(TypeAliasSpec.builder("Word", String::class).build()).addTypeAlias(TypeAliasSpec.builder("FileTable", fileTable).addTypeVariable(k).build()).addTypeAlias(TypeAliasSpec.builder("Predicate", predicate).addTypeVariable(t).build()).build()
这会生成以下内容:
package com.exampleimport java.io.File
import kotlin.Boolean
import kotlin.String
import kotlin.collections.Map
import kotlin.collections.Settypealias Word = Stringtypealias FileTable = Map>typealias Predicate = (T) -> Boolean
通过LambdaTypeName
可以很方便的创建函数类型,以生成高阶函数代码,例如:
private fun functionType() : TypeSpec {val Type = ClassName("com.fly.compose.ksp.application", "Type")val E = TypeVariableName("E")val R = TypeVariableName("R")val T = TypeVariableName("T")val E_Type = E.copy(bounds = listOf(Type)) // like: val blockTypeName = LambdaTypeName.get(receiver = T,returnType = R)val blockTypeName2 = LambdaTypeName.get(parameters = arrayOf(T),returnType = R)val onSuccessTypeName = LambdaTypeName.get(parameters = arrayOf(E),returnType = Boolean::class.asClassName())val onFailedTypeName = LambdaTypeName.get(parameters = arrayOf(E),returnType = Unit::class.asClassName())val className = ClassName("com.example.generated", "Foo")return TypeSpec.classBuilder(className).addTypeVariable(E_Type).addFunction(FunSpec.builder("with").addTypeVariables(listOf(T, R)).addParameter("receiver", T).addParameter(ParameterSpec.builder("block", blockTypeName).build()).returns(R).addStatement("return receiver.block()").build()).addFunction(FunSpec.builder("let").addTypeVariables(listOf(T, R)).receiver(T).addParameter(ParameterSpec.builder("block", blockTypeName2).build()).returns(R).addStatement("return block(this)").build()).addFunction(FunSpec.builder("request").addParameter(ParameterSpec.builder("onSuccess", onSuccessTypeName).build()).addParameter(ParameterSpec.builder("onFailed", onFailedTypeName).build()).build()).build()
}
这会生成:
public class Foo {public fun with(`receiver`: T, block: T.() -> R): R = receiver.block()public fun T.let(block: (T) -> R): R = block(this)public fun request(onSuccess: (E) -> Boolean, onFailed: (E) -> Unit): Unit {}
}
可以通过以下方式创建对构造函数、函数和属性的 Callable 引用:
ClassName.constructorReference()
用于引用构造函数MemberName.reference()
用于引用函数和属性例如:
val helloClass = ClassName("com.example.hello", "Hello")
val worldFunction: MemberName = helloClass.member("world")
val byeProperty: MemberName = helloClass.nestedClass("World").member("bye")val factoriesFun = FunSpec.builder("factories").addStatement("val hello = %L", helloClass.constructorReference()).addStatement("val world = %L", worldFunction.reference()).addStatement("val bye = %L", byeProperty.reference()).build()FileSpec.builder("com.example", "HelloWorld").addFunction(factoriesFun).build()
会产生:
package com.exampleimport com.example.hello.Hellofun factories() {val hello = ::Helloval world = Hello::worldval bye = Hello.World::bye
}
具有冲突名称的顶级类和成员可能需要别名导入,就像前面使用 MemberName
一样。
要从任意的 KType 生成源代码,包括内置反射 API 无法访问的信息,KotlinPoet 依赖于kotlin-reflect。kotlin-reflect
可以读取类的元数据并访问这些额外信息。例如,KotlinPoet 可以从泛型中读取类型参数及其变体KType
,并生成适当的源代码。
kotlin-reflect
虽然是一个相对较大的依赖库,但在某些情况下,希望将其从最终可执行文件中删除以节省一些空间和/或简化 proguard/R8
设置(例如,对于生成 Kotlin 代码的 Gradle 插件)。可以这样做并且仍然使用大部分 KotlinPoet API:
dependencies {implementation("com.squareup:kotlinpoet:") {exclude(module = "kotlin-reflect")}
}
KotlinPoet 主要需要的 kotlin-reflect
相关的API是 KType.asTypeName()
和 typeNameOf
。如果你在没有依赖kotlin-reflect
库的情况下调用其中了一个,并且类型是泛型或具有注解,将会产生崩溃。
你可以将其替换为显式传递类型参数或注解且不需要kotlin-reflect
的代码。例如:
// Replace
// kotlin-reflect needed
val typeName = typeNameOf>()// With
// kotlin-reflect not needed
val typeName =List::class.asClassName().parameterizedBy(Int::class.asClassName().copy(nullable = true))