Kotlin有趣的DSL

1,139 阅读2分钟

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

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

<?xml version="1.0" encoding="UTF-8"?>
<student enable="true">
    <name>张三</name>
    <gender></gender>
    <remark/>
</student>

如果使用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,效果是这样的:

fun main() {
    document {
        "student"("enable" to "true") {
            "name"{ +"张三" }
            "gender"{ +"男" }
            "remark"()
        }
    }
}

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

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

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的也可以这样玩,如果我们要构造一个表格:

姓名性别
张三

那么我们就可以这样写:

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还少,还支持合并单元格:

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分享出来。