我们先从 kotlin 的 parser 讲起,这边会顺便带到一些 KotlinPoet 的进阶用法。我们目标是读取 annotation 的资讯产生对应的 parser ,以下面这组的 annotation 为例:
@RssTag(name = "channel")
data class TestRssData(
val title: String?,
@RssTag
val link: String?,
val textInput: MyTextInput?,
@RssTag(name = "item")
val list: List<RssItem>,
@RssTag(name = "category")
val categories: List<TestCategory>,
val skipDays: SkipDays?,
val ttl: Long?,
val image: TestImage?,
val cloud: TestCloud?,
): Serializable
这个类别包含的子结构就不在这边一一列出,原始码可以参考这里。在写 generator 之前,我们要先规划产生出来程序码会长什麽样子。以上面的例子,我们可以想像会有个 TestRssDataParser
提供一系列的 function 来从目标类别 ( TestRssData
) 爬出资讯, 而它包含的子结构,可以透过子结构本身再产生一个类别提供 function 去负责爬该类别的资讯。举例来说,负责爬 TestRssData
资讯的是 TestRssDataParser
,而里面会用到子结构 RssItem
的资讯,则是产生另一个 RssItemParser
去爬取,这个概念将可以套用在其他所有的 generator 上面。
让我们一步步拆解 TestRssDataParser
来讲解怎麽做的,首先我们先订出最重要的 parse function ,输入是 XML 字串,输出是被标 annotation 的 data class ,在这边是 TestRssData
。
object TestRssDataParser {
fun parse(xml: String): TestRssData {
val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
val document = builder.parse(xml.byteInputStream())
document.documentElement.normalize()
val nodeList = document.getElementsByTagName("channel")
var result: TestRssData? = null
if (nodeList?.length == 1) {
val element = nodeList.item(0) as? Element
element?.let {
result = it.getChannel()
}
}
return result ?: throw IllegalStateException("No valid channel tag in the RSS feed.")
}
}
上述的程序码有没有很熟悉啊?它就是前面在讲 DOM parser 的时候的作法,一样的逻辑搬过来,只是现在我们要用 annotation 给的资讯,接着用 KotlinPoet 把动态产生出来。
const val PARSER_FUNC_NAME = "parse"
const val CHANNEL = "channel"
private val outputClass = ClassName(element.getPackage(), element.simpleName.toString())
private val exceptionClass = ClassName("java.lang", "IllegalStateException")
private val docBuilderFactoryClass = ClassName("javax.xml.parsers", "DocumentBuilderFactory")
fun getParseFuncSpec(): FunSpec {
return FunSpec.builder(PARSER_FUNC_NAME)
.addParameter("xml", String::class)
.addCode(
"""
| val builder = %4T.newInstance().newDocumentBuilder()
| val document = builder.parse(xml.byteInputStream())
| document.documentElement.normalize()
| val nodeList = document.getElementsByTagName("%2L")
| var result: %1T? = null
|
| if (nodeList?.length == 1) {
|${TAB}${TAB}val element = nodeList.item(0) as? Element
|${TAB}${TAB}element?.let {
|${TAB}${TAB}${TAB}result = it.getChannel()
|${TAB}${TAB}}
| }
| return result ?: throw %3T("No valid channel tag in the RSS feed.")
| """.trimMargin(),
outputClass, CHANNEL, exceptionClass, docBuilderFactoryClass
)
.returns(outputClass)
.build()
}
这个 function 就是产生上方 TestRssDataParser
内的 parse
function ,除了设定好输入输出之外,还可以针对程序码内部设定一些动态的 type 参数,也就是在 addCode
里面的 %1T
、%2L
、%3T
和 %4T
。1234 分别代表後方带入的参数顺序,对应到 addCode
後方戴的那几个参数outputClass
、 CHANNEL
、 exceptionClass
和 docBuilderFactoryClass
。L 代表字串型态,T 则代表类别。前面用 ClassName
的方式宣告在 KotlinPoet ,在它产生程序码的同时,会帮我们把对应的类别自动 import ,所以我们不用担心会有 import 错误的问题。
<<: [NestJS 带你飞!] DAY09 - Pipe (上)
这篇是 infrastructure 也可以 for each 第四篇,上次漏发了,今天补发 本章介...
Set与Map不同再於Set没有key,是指有包含值的特殊集合,且每个值只能出现一次不能重复。 Se...
接下来将要来介绍如何运用APM(Application Performance Monitoring...
接下来我们要做的是心情随笔前台的画面, 我们要在 app/Http/Controllers/Home...
在这第六世代的战争中、面对来势汹汹的 DC、SONY 当然也早就有准备、非常机歪的选在 DC 发售的...