12.5 使用MarkupBuilder生成XML

Groovy类MarkupBuilder是一种使用Groovy动态编程功能创建的类。前一章介绍了动态地拦截方法调用,在Java和Kotlin等静态语言中,这种功能是无法如此天衣无缝地实现的。

为了演示这种功能,下面是一个使用Groovy类MarkupBuilder的示例。你无需在Eclipse IDE中输入这些代码,但可在GroovyConsole中输入并运行它们:

  1. def xmlContent = new StringWriter()
  2. def xmlWriter = new groovy.xml.MarkupBuilder(xmlContent)
  3. xmlWriter.items {
  4. item(id: 1) {
  5. name("Item one")
  6. }
  7. item(id: 2) {
  8. name("Item two")
  9. }
  10. }
  11. println(xmlContent)

这些代码所做的工作很多,但我们先来看看结果,再详细介绍。这些代码将向控制台打印如下输出:

  1. <items>
  2. <item id='1'>
  3. <name>Item one</name>
  4. </item>
  5. <item id='2'>
  6. <name>Item two</name>
  7. </item>
  8. </items>

MarkupBuilder类的构造函数将一个实现了Java接口Writer的对象作为参数,生成的数据将写入到这个对象中。我们可以使用一个FileWriter实例将输出存储到一个文本文件中,但在这里,我们需要的是一个String,因此传入了一个StringWriter实例。

别忘了,在Groovy中调用函数时,括号是可选的。代码行xmlWriter.items { … }也可写成下面这样:

  1. xmlWriter.items({
  2. ...
  3. })

在这个示例中,显然是调用了方法items(),它将一个闭包作为输入参数。MarkupBuilder类没有方法items(),但它拦截未知的方法调用和属性访问,并根据使用的方法名和参数创建XML元素。

根据SQL查询结果生成XML

知道如何使用MarkupBuilder后,就可以编写根据博文记录生成XML的方法了。这个方法比较复杂,我们将分步完成其编写工作。首先来定义方法generateXML(),并添加生成XML的变量。为此,在Main类中添加如下代码:

  1. def generateXML() {
  2. def xmlContent = new StringWriter()
  3. def xmlWriter = new groovy.xml.MarkupBuilder(xmlContent)
  4. }

接下来定义同时获取博文数据和用户名的SQL查询。为此,在方法generateXML()的末尾添加如下代码:

  1. def connection = createDatabaseConnection()
  2. def sql = new Sql(connection)
  3. def sqlQuery = """
  4. SELECT B.id, B.title, B.post, U.name AS user_name
  5. FROM blog B
  6. INNER JOIN user U ON B.user = U.id"""
  7. sql.eachRow(sqlQuery) { record ->
  8. }

在任何情况下,在一个查询中获取尽可能多的信息都是不错的主意,因为这比依次执行多个查询的速度更快。在这里,我们使用INNER JOIN子句将数据表user和blog连接起来。这个查询返回如下列:

  • blog.id;
  • blog.title;
  • blog.post;
  • user.name。

在这个查询中,我们给user.name列指定了别名,这样可在代码中使用别名user_name来表示用户名。

我们使用Sql类的方法eachRow()来遍历返回的记录,它借鉴了函数式编程范式的做法,让我们无需手动编写循环来遍历记录,而只需指定一个将对每条返回的记录调用的闭包函数。这个闭包的代码可使用参数record来读取每天记录的值。在这个闭包中,我们将为每条记录创建一个XML条目。为此请在其中添加如下代码:

  1. xmlWriter.posts {
  2. post(id: record.id) {
  3. title(record.title)
  4. user(record.user_name)
  5. def p = record.post
  6. post(p.getSubString(1, p.length().intValue()))
  7. }
  8. }

这段代码你应该非常熟悉。我们创建了一个根节点为的XML。为了读取记录,只需将SQL查询中的列名(或别名,如user_name)视为属性名即可,唯一的例外是post字段。前面创建数据表时,post字段的类型被指定为CLOB,之所以这样做是因为我们不知道博文将包含多少行文本。类型为CLOB的字段的长度是不确定的,因此要读取它,必须指定每次要读取多少个字符。鉴于我们预期博文比内存量小得多,因此我们调用CLOB字段post的函数length()来指定每次要读取的字符数,从而一次性读取整个博文。

12.5 使用MarkupBuilder生成XML - 图1 在生产级应用程序中,最好对每次读取的字符数进行限制,以避免字段消耗过多的服务器内存。为此,一种办法是小批量地读取并处理字段中的数据。

最后,我们需要关闭数据库连接,并让方法generateXML()以普通Java字符串的方式返回生成的XML。为此,在这个方法的末尾添加如下代码:

  1. def generateXML() {
  2. def xmlContent = new StringWriter()
  3. def xmlWriter = new groovy.xml.MarkupBuilder(xmlContent)
  4. ...
  5. sql.close()
  6. return xmlContent.toString()
  7. }

在方法main()中的代码行connection.close()后面,添加调用方法generateXML()的代码:

  1. ...
  2. app.openDatabaseConnection()
  3. app.createDatabaseStructure()
  4. app.addDemoRecords()
  5. connection.close()
  6. println(app.generateXML())
  7. ...

你可能会问,为何要在方法中新建数据库连接,而不重用变量connection呢?这将在后面修改这个应用程序的实现,将其变成Web服务时做出解释。

如果现在运行这个应用程序,将看到如下输出:

  1. <posts>
  2. <post id='1'>
  3. <title>Test post</title>
  4. <user>Admin</user>
  5. <post>This is a test post</post>
  6. </post>
  7. </posts>

12.5 使用MarkupBuilder生成XML - 图2