上篇提到了 parser generator 在產生程式碼的時候,可以用四個步驟去拆解裡面的資訊並產生程式碼,我們現在來看一下範例吧!
進入範例之前,我們也複習一下之前提我們預計要產生出來的 class 內部程式碼的結構大概是會長這個樣子,我們就叫他 Code 1.1 。本篇文章有很多程式碼都是要參考這個結構,對照著看才會比較有感覺。
object RssItemParser {
fun Element.getItem(): RssItem {
// #1 Value Statement
val titleTitle: String? = readString("title")
val titleItunesTitle: String? = readString("itunes:title")
val titleGoogleplayTitle: String? = readString("googleplay:title")
val authorAuthor: String? = readString("author")
val authorItunesAuthor: String? = readString("itunes:author")
val authorGoogleplayAuthor: String? = readString("googleplay:author")
val guidGuid: TestGuid? = getElementByTag("guid")?.getGuid()
val guidItunesGuid: TestGuid? = getElementByTag("itunes:guid")?.getGuid()
val guidGoogleplayGuid: TestGuid? = getElementByTag("googleplay:guid")?.getGuid()
// #2 Use class constructor
return RssItem(
title = titleTitle ?: titleItunesTitle ?: titleGoogleplayTitle,
author = authorAuthor ?: authorItunesAuthor ?: authorGoogleplayAuthor,
guid = guidGuid ?: guidItunesGuid ?: guidGoogleplayGuid
)
}
}
接著就是呼叫四個步驟的程式碼:
private fun getClassFunSpec(
rootElement: Element,
outputClassName: String,
objectBuilder: TypeSpec.Builder
): FunSpec {
val outputClass = ClassName(rootElement.getPackage(), outputClassName)
val rssTag = rootElement.getAnnotation(RssTag::class.java)
val rssRawData = rootElement.getAnnotation(RssRawData::class.java)
if (rssTag != null && rssRawData != null) {
logger.error("@RssTag and @RssRawData should not be used on the same class!", rootElement)
}
val tagName = rssTag?.name?.takeIfNotEmpty() ?: rootElement.simpleName.toString()
rootTagName = tagName
logger.log("[KotlinParserGenerator][getActionFunSpec] $rootTagName")
val funSpec = FunSpec.builder(tagName.getFuncName())
.receiver(elementClassName)
.returns(outputClass)
val propertyToParseData = mutableMapOf<String, ParseData>()
topLevelCandidateOrder =
rssTag?.order ?: arrayOf(OrderType.RSS_STANDARD, OrderType.ITUNES, OrderType.GOOGLE)
val annotations = preProcessAnnotations(rootElement)
rootElement.enclosedElements.forEach { preProcessParseData(it, propertyToParseData, annotations) }
propertyToParseData.forEach { generateValueStatementForConstructor(it, funSpec, objectBuilder) }
funSpec.addStatement("\nreturn $outputClassName(")
// Generate constructor statements
var index = 0
val lastIndex = propertyToParseData.size - 1
propertyToParseData.forEach {
generateConstructor(it, funSpec, index == lastIndex)
index ++
}
funSpec.addStatement("${TAB})")
return funSpec.build()
}
上面這段程式碼包含了上述的四個步驟,分別可以對應到四個 function :
preProcessAnnotations
preProcessParseData
generateValueStatementForConstructor
generateConstructor
首先,我們可以透過先把 annotation 裡的 class 和 member 資訊整理好,放入 map 中以便之後快速查找,這邊用 map 的原因是之後要取用 annotation 裡面的資訊時,可以透過名稱直接拿到資訊。
protected fun preProcessAnnotations(rootElement: Element): Map<String, Any?> {
val result = mutableMapOf<String, Any?>()
rootElement.enclosedElements.forEach { child ->
if (child.kind != ElementKind.METHOD
|| !child.simpleName.isGetterMethod()
|| !child.simpleName.contains(ANNOTATION_SIGN)
) return@forEach
val nameFromMethod = child.simpleName.extractNameFromMethod()
val rssValue: RssValue? = child.getAnnotation(RssValue::class.java)
val rssTag: RssTag? = child.getAnnotation(RssTag::class.java)
val rssAttribute: RssAttribute? = child.getAnnotation(RssAttribute::class.java)
val rssRawData: RssRawData? = child.getAnnotation(RssRawData::class.java)
val nonNullCount = listOf<Any?>(rssTag, rssAttribute, rssRawData).count { it != null }
val attributeValueCount = listOf<Any?>(rssAttribute, rssValue).count { it != null }
if (nonNullCount > 1 || attributeValueCount > 1) {
logger.error(
"You can't annotate more than one annotation at a field or property!",
child
)
}
result[nameFromMethod] = rssValue ?: rssTag ?: rssAttribute ?: rssRawData
if (rssValue != null) {
hasRssValueAnnotation = true
}
}
return result
}
在處理 annotation 的時候,我們有給它一個限制就是不能在同一個元素上面標註超過一個 annotation ,所以才會有 if (nonNullCount > 1 || attributeValueCount > 1)
的判斷。
接著,我們就可以把這個 map 轉成我們自定義的 ParseData
類別來包住一些我們需要的資訊:
data class ParseData(
val type: String?,
val rawType: String?,
val dataType: DataType,
val listItemType: String?,
val packageName: String?,
val processorElement: Element,
val tagCandidates: List<String> = listOf()
)
protected fun preProcessParseData(
child: Element,
parseDataMap: MutableMap<String, ParseData>,
nameToAnnotation: Map<String, Any?>
) {
if (child.kind!= ElementKind.METHOD
|| !child.simpleName.isGetterMethod()
|| child.simpleName.contains(ANNOTATION_SIGN)
) return
val nameFromMethod = child.simpleName.extractNameFromMethod()
val exeElement = child as? ExecutableElement ?: return
val rawType = exeElement.returnType.toString()
val type: String?
val packageName: String?
val dataType: DataType
var listItemType: String? = null
if (rawType.isListType()) {
listItemType = rawType.extractListType()
type = rawType
.substringBeforeLast('<')
.extractType()
packageName = rawType.substringAfterLast('<')
.substringBeforeLast('>')
.substringBeforeLast('.')
dataType = DataType.LIST
} else {
type = rawType.extractType()
val annotation = nameToAnnotation[nameFromMethod]
dataType = when {
annotation is RssValue -> DataType.VALUE
annotation is RssAttribute -> DataType.ATTRIBUTE
rawType.isPrimitive() -> DataType.PRIMITIVE
else -> DataType.OTHER
}
packageName = rawType.substringBeforeLast('.')
}
parseDataMap[nameFromMethod] = ParseData(
type = type,
rawType = rawType,
dataType = dataType,
listItemType = listItemType,
packageName = packageName,
processorElement = child,
tagCandidates = getTagCandidates(nameToAnnotation, nameFromMethod, child)
)
}
可以看到這邊我們其實做了很多字串處理,為了要直接從 Element
的 rawType
中抽取出型別,型別抽取出來後把這些資料放到 ParseData
的 map 裡面。
利用上個步驟產生的 ParseData
map ,我們可以開始把 map 裡的資料轉換成要產生 Code 1.1 註解 #1 內的程式碼結構的... 程式碼。(恩,很饒舌)
下面的程式碼很長,但做的事情只有一件,就是把 ParseData 拆掉,按照 dataType
去產生 KotlinPoet 的 code statement 。
protected fun generateVariableStatement(
propertyToParseData: Map.Entry<String, ParseData>,
funSpec: FunSpec.Builder
) {
val name = propertyToParseData.key
val data = propertyToParseData.value
val packageName = data.packageName ?: return
val type = data.type ?: return
when (data.dataType) {
DataType.LIST -> {
val itemType = data.listItemType ?: return
if (itemType.isPrimitive()) {
val kClass = itemType.getKPrimitiveClass() ?: return
data.tagCandidates.forEach { tag ->
funSpec.addStatement(
"var ${name.getVariableName(tag)}: ArrayList<%T> = arrayListOf()",
kClass
)
}
} else {
val itemClassName = ClassName(packageName, data.listItemType)
data.tagCandidates.forEach { tag ->
funSpec.addStatement(
"var ${name.getVariableName(tag)}: ArrayList<%T> = arrayListOf()",
itemClassName
)
}
}
}
DataType.PRIMITIVE -> {
val kClass = type.getKPrimitiveClass() ?: return
data.tagCandidates.forEach { tag ->
funSpec.addStatement("var ${name.getVariableName(tag)}: %T? = null", kClass)
}
}
DataType.ATTRIBUTE -> {
val kClass = type.getKPrimitiveClass() ?: return
data.tagCandidates.forEach { tag ->
val statement =
"var ${name.getVariableName(tag)}: %T? = getAttributeValue(null, \"$tag\")"
.appendTypeConversion(type)
if (type.isBooleanType()) {
funSpec.addStatement(statement, kClass, booleanConversionMemberName)
} else {
funSpec.addStatement(statement, kClass)
}
}
}
DataType.VALUE -> {
// Do nothing
}
else -> {
val className = ClassName(packageName, type)
data.tagCandidates.forEach { tag ->
funSpec.addStatement("var ${name.getVariableName(tag)}: %T? = null", className)
}
}
}
}
最後,我們可以利用前面已經產生的 Code 1.1 註解 #1 的區域,去產生 constructor ,也就是 Code 1.1 註解 #2 的區域,主要就是按照 dataType 去產生對應的 constructor 而已。
private fun generateValueStatementForConstructor(
propertyToParseData: Map.Entry<String, ParseData>,
funSpec: FunSpec.Builder,
objectBuilder: TypeSpec.Builder
) {
val name = propertyToParseData.key
val data = propertyToParseData.value
val packageName = data.packageName ?: return
val type = data.type ?: return
when (data.dataType) {
DataType.LIST -> {
generateValueListStatement(data, objectBuilder, packageName, funSpec, name)
}
DataType.PRIMITIVE -> {
val kClass = type.getKPrimitiveClass() ?: return
data.tagCandidates.forEach { tag ->
funSpec.addStatement("val ${name.getVariableName(tag)}: %T? =", kClass)
funSpec.addPrimitiveStatement("%M(\"$tag\")", type)
}
}
DataType.ATTRIBUTE -> {
generateValueAttributeStatement(type, data, name, funSpec)
}
DataType.VALUE -> {
// Do nothing
}
else -> {
val className = ClassName(packageName, type)
data.tagCandidates.forEach { tag ->
val memberName = MemberName(type.getGeneratedClassPath(), tag.getFuncName())
funSpec.addStatement("val ${name.getVariableName(tag)}: %T? = %M(\"$tag\")?.%M()",
className, getElementByTagMemberName, memberName)
}
}
}
}
目前的範例都只有用純 Kotlin 的 parser generator 去做講解,其他像是 Android parser 和 reader generator 的程式碼產生方式也是大同小異,看到這裡你大概也有感受到,寫 annotation processor 和 code generator 其實是一個苦工,你要從這些 annotation processor 給你的這些資訊,先過濾出你要的,然後用 KotlinPoet 去產生對應的程式碼,而中間還會做一大堆的字串處理去拿到有用的資訊,真的是會很容易迷失在程式碼裡面。我自己寫完這邊的感受是,這個東西真的是寫一次就好了... 可以解決很多重複程式碼的問題,不用讓大家跟我一起 “一袋米要扛幾樓” 感受痛苦。
如果你也對其他的 parser generator 有興趣,可以參考一下原始碼: