Kotlin有趣的DSL

这阵子在研究Kotlin,它提供了类似DSL的语法能力,一些在Java中写起来冗长的方法,在Kotlin中则可以方便的使用,同时具有很高的可读性。

举个例子,如果我们要构造这样的xml:

1
2
3
4
5
6
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<student enable="true">
<name>张三</name>
<gender>男</gender>
<remark/>
</student>

如果使用Java来做的话,是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

public class XmlExample {
public static void main(String[] args) throws ParserConfigurationException {
final var document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
document.setXmlStandalone(true);
final var student = document.createElement("student");
student.setAttribute("enable", "true");
document.appendChild(student);
final var name = document.createElement("name");
name.appendChild(document.createTextNode("张三"));
student.appendChild(name);
final var gender = document.createElement("gender");
gender.appendChild(document.createTextNode("男"));
student.appendChild(gender);
student.appendChild(document.createElement("remark"));
}
}

简单的例子看起来还算清晰,但如果层级变多了可读性会迅速下降。

接下来给大伙整个活,我用Kotlin写一个DSL,效果是这样的:

1
2
3
4
5
6
7
8
9
kotlin复制代码fun main() {
document {
"student"("enable" to "true") {
"name"{ +"张三" }
"gender"{ +"男" }
"remark"()
}
}
}

可以看出代码和xml的结构是一一对应的,这样我们就非常方便地构造了一个xml实例。

以上效果的全部实现代码包括import在内仅53行,并且支持格式化输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
kotlin复制代码import org.w3c.dom.Document
import org.w3c.dom.Node
import java.io.ByteArrayOutputStream
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult

private val defaultDocumentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
private val defaultTransformerFactory = TransformerFactory.newInstance()

fun document(block: DocumentBuilderDsl.() -> Unit): Document = defaultDocumentBuilder.newDocument().apply {
xmlStandalone = true
block(DocumentBuilderDsl(this))
}

@DslMarker
@Target(AnnotationTarget.CLASS)
annotation class XmlDsl

@XmlDsl
class DocumentBuilderDsl(private val document: Document) {
operator fun String.invoke(vararg attributes: Pair<String, String?>): Node = this(*attributes) {}
operator fun String.invoke(vararg attributes: Pair<String, String?>, block: NodeBuilderDsl.() -> Unit): Node =
document.appendChild(document.createElement(this).apply {
attributes.forEach { setAttribute(it.first, it.second) }
block(NodeBuilderDsl(document, this))
})
}

@XmlDsl
class NodeBuilderDsl(private val document: Document, private val node: Node) {
operator fun String.invoke(vararg attributes: Pair<String, String?>): Node = this(*attributes) {}
operator fun String.invoke(vararg attributes: Pair<String, String?>, block: NodeBuilderDsl.() -> Unit): Node =
node.appendChild(document.createElement(this).apply {
attributes.forEach { setAttribute(it.first, it.second) }
block(NodeBuilderDsl(document, this))
})

operator fun String.unaryPlus(): Node = node.appendChild(document.createTextNode(this))
}

fun Document.asXml(format: Boolean = false, indentAmount: Int = 4): String = ByteArrayOutputStream().use {
defaultTransformerFactory.newTransformer().apply {
if (format) {
setOutputProperty(OutputKeys.INDENT, "yes")
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", indentAmount.toString())
setOutputProperty(OutputKeys.STANDALONE, "yes")
}
}.transform(DOMSource(this), StreamResult(it))
it.toString()
}

还有用poi构造Excel的也可以这样玩,如果我们要构造一个表格:

姓名 性别
张三

那么我们就可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码fun main() {
val workbook = workbook<XSSFWorkbook> {
sheet {
row {
cell { setCellValue("姓名") }
cell { setCellValue("性别") }
}
row {
cell { setCellValue("张三") }
cell { setCellValue("男") }
}
}
}
workbook.write(File("src/main/resources/test.xlsx"))
}

实现代码比上面的xml还少,还支持合并单元格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
kotlin复制代码import org.apache.poi.hssf.usermodel.HSSFWorkbook
import org.apache.poi.ss.usermodel.*
import org.apache.poi.ss.util.CellRangeAddress
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import java.io.File
import java.io.FileOutputStream

inline fun <reified T : Workbook> workbook(block: WorkbookBuilderDsl.() -> Unit): Workbook {
val workbook = when (T::class) {
HSSFWorkbook::class -> HSSFWorkbook()
XSSFWorkbook::class -> XSSFWorkbook()
else -> error("不支持的类型:${T::class}")
}
block(WorkbookBuilderDsl(workbook))
return workbook
}

@DslMarker
@Target(AnnotationTarget.CLASS)
annotation class WorkbookDsl

@WorkbookDsl
class WorkbookBuilderDsl(private val workbook: Workbook) {
fun sheet(sheetName: String? = null, block: SheetBuilderDsl.() -> Unit): Sheet =
(if (sheetName != null) workbook.createSheet(sheetName) else workbook.createSheet()).also { block(SheetBuilderDsl(it)) }
}

@WorkbookDsl
class SheetBuilderDsl(private val sheet: Sheet) {
private var rownum = 0
fun row(block: RowBuilderDsl.() -> Unit): Row = sheet.createRow(rownum).also { block(RowBuilderDsl(it, rownum++)) }
}

@WorkbookDsl
class RowBuilderDsl(private val row: Row, private val rownum: Int) {
private var column = 0
private val sheet = row.sheet
fun cell(type: CellType? = null, rowspan: Int = 1, colspan: Int = 1, block: Cell.() -> Unit): Cell {
if (colspan > 1 || rowspan > 1) sheet.addMergedRegion(CellRangeAddress(rownum, rownum + rowspan - 1, column, column + colspan - 1))
sheet.mergedRegions
.firstOrNull { rownum in it.firstRow..it.lastRow && column in it.firstColumn..it.lastColumn }
?.also { if (rownum != it.firstRow || column != it.firstColumn) column = it.lastColumn + 1 }
return (if (type != null) row.createCell(column++, type) else row.createCell(column++)).also(block)
}
}

fun Workbook.write(file: File) {
use { it.write(FileOutputStream(file)) }
}

有兴趣的同学可以玩下,当然这些只是实现了核心功能,如果要完善的实现可以根据情况自行修改,有时间的话我也打算就以上的内容放到Github分享出来。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%