返回介绍

8 构建器

发布于 2025-01-04 00:44:53 字数 17530 浏览 0 评论 0 收藏 0

(未编写)

8.1. Creating a builder

(未编写)

8.1.1. BuilderSupport

(未编写)

8.1.2. FactoryBuilderSupport

(未编写)

8.2. Existing builders

(未编写)

8.2.1. MarkupBuilder

参见 创建 Xml - MarkupBuilder

8.2.2. StreamingMarkupBuilder

参见 创建 Xml - StreamingMarkupBuilder

8.2.3. SaxBuilder

用来生成 Simple API for XML (SAX) 事件的构建器。

假设现在有下列 SAX 处理器:

class LogHandler extends org.xml.sax.helpers.DefaultHandler {

  String log = ''

  void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) {
    log += "Start Element: $localName, "
  }

  void endElement(String uri, String localName, String qName) {
    log += "End Element: $localName, "
  }
}

可以使用 SaxBuilder 为该处理器生成 SAX 事件:

def handler = new LogHandler()
def builder = new groovy.xml.SAXBuilder(handler)

builder.root() {
  helloWorld()
}

一切如预期运行:

assert handler.log == 'Start Element: root, Start Element: helloWorld, End Element: helloWorld, End Element: root, '

8.2.4. StaxBuilder

适用于 Streaming API for XML (StAX) 处理程序的 Groovy 构建器。

下面是一个很简单的范例,利用 StAX 的 Java 实现来生成 XML:

def factory = javax.xml.stream.XMLOutputFactory.newInstance()
def writer = new StringWriter()
def builder = new groovy.xml.StaxBuilder(factory.createXMLStreamWriter(writer))

builder.root(attribute:1) {
  elem1('hello')
  elem2('world')
}

assert writer.toString() == '<?xml version="1.0" ?><root attribute="1"><elem1>hello</elem1><elem2>world</elem2></root>'

类似 Jettison 这样的外部库可以这样使用:

@Grab('org.codehaus.jettison:jettison:1.3.3')
import org.codehaus.jettison.mapped.*

def writer = new StringWriter()
def mappedWriter = new MappedXMLStreamWriter(new MappedNamespaceConvention(), writer)
def builder = new groovy.xml.StaxBuilder(mappedWriter)

builder.root(attribute:1) {
   elem1('hello')
   elem2('world')
}

assert writer.toString() == '{"root":{"@attribute":"1","elem1":"hello","elem2":"world"}}' 

8.2.5 DOMBuilder

将 HTML、XHTML 以及 XML 解析为 W3C DOM 树的构建器。

比如像这个 XML String

String recordsXML = '''
  <records>
    <car name='HSV Maloo' make='Holden' year='2006'>
    <country>Australia</country>
    <record type='speed'>Production Pickup Truck with speed of 271kph</record>
    </car>
    <car name='P50' make='Peel' year='1962'>
    <country>Isle of Man</country>
    <record type='size'>Smallest Street-Legal Car at 99cm wide and 59 kg in weight</record>
    </car>
    <car name='Royale' make='Bugatti' year='1931'>
    <country>France</country>
    <record type='price'>Most Valuable Car at $15 million</record>
    </car>
  </records>'''

可以解析为带有 DOMBuilder 的 DOM 树:

def reader = new StringReader(recordsXML)
def doc = groovy.xml.DOMBuilder.parse(reader)

还可以进一步处理,比如使用 DOMCategory

def records = doc.documentElement
use(groovy.xml.dom.DOMCategory) {
  assert records.car.size() == 3
}

8.2.6 NodeBuilder

NodeBuilder 用来创建能够处理任意数据的 Node 对象的内嵌树。要想创建一个简单的用户列表,可以使用一个 NodeBuilder

def nodeBuilder = new NodeBuilder()
def userlist = nodeBuilder.userlist {
  user(id: '1', firstname: 'John', lastname: 'Smith') {
    address(type: 'home', street: '1 Main St.', city: 'Springfield', state: 'MA', zip: '12345')
    address(type: 'work', street: '2 South St.', city: 'Boston', state: 'MA', zip: '98765')
  }
  user(id: '2', firstname: 'Alice', lastname: 'Doe')
}

还可以进一步处理这些数据,比如使用 GPath 表达式

assert userlist.user.@firstname.join(', ') == 'John, Alice'
assert userlist.user.find { it.@lastname == 'Smith' }.address.size() == 2

8.2.7 JsonBuilder

Groovy 的 JsonBuilder 可以很方便地创建 JSON 对象。比如创建下列 Json 字符串:

String carRecords = '''
  {
    "records": {
    "car": {
      "name": "HSV Maloo",
      "make": "Holden",
      "year": 2006,
      "country": "Australia",
      "record": {
        "type": "speed",
        "description": "production pickup truck with speed of 271kph"
      }
      }
    }
  }
'''  

还可以这样使用 JsonBuilder


JsonBuilder builder = new JsonBuilder()
builder.records {
  car {
    name 'HSV Maloo'
    make 'Holden'
    year 2006
    country 'Australia'
    record {
      type 'speed'
      description 'production pickup truck with speed of 271kph'
    }
  }
}
String json = JsonOutput.prettyPrint(builder.toString())

还可以使用 JsonUnit 查看构建器是否产生预期结果:

JsonAssert.assertJsonEquals(json, carRecords)

8.2.8 StreamingJsonBuilder

StreamingJsonBuilderJsonBuilder 不同之处在于, JsonBuilder 在内存中创建数据结构,假如想要在输出前程序性地改变结构,那么这一构建器非常方便,而 StreamingJsonBuilder 则采用流式写入,无需任何中间的内存数据结构。如果不需要修改数据结构,使用一种存储效率更高的方法,则应该使用 StreamingJsonBuilder

在用法上, StreamingJsonBuilderJsonBuilder 差不多,以下面这个 Json 字符串为例:

String carRecords = '''
  {
    "records": {
    "car": {
      "name": "HSV Maloo",
      "make": "Holden",
      "year": 2006,
      "country": "Australia",
      "record": {
        "type": "speed",
        "description": "production pickup truck with speed of 271kph"
      }
      }
    }
  }
'''

使用 StreamingJsonBuilder

StringWriter writer = new StringWriter()
StreamingJsonBuilder builder = new StreamingJsonBuilder(writer)
builder.records {
  car {
    name 'HSV Maloo'
    make 'Holden'
    year 2006
    country 'Australia'
    record {
      type 'speed'
      description 'production pickup truck with speed of 271kph'
    }
  }
}
String json = JsonOutput.prettyPrint(writer.toString())

使用 JsonUnit 来查看是否符合预期结果:

JsonAssert.assertJsonEquals(json, carRecords)

8.2.9 SwingBuilder

SwingBuilder 能够以一种简洁的描述性方式来实现一种成熟完备的 Swing GUI。这一目的的实现有赖于一种常见的 Groovy 词汇:构建器(builder)。构建器可以帮助我们来处理复杂对象,比如初始化子对象,调用 Swing 方法,将子对象添加到父对象上,等等。因此,代码可读性和可维护性变得更好,同时还能使你访问所有 Swing 组件。

下面是使用 SwingBuilder 的一个简单范例:

import groovy.swing.SwingBuilder
import java.awt.BorderLayout as BL

count = 0
new SwingBuilder().edt {
  frame(title: 'Frame', size: [300, 300], show: true) {
  borderLayout()
  textlabel = label(text: 'Click the button!', constraints: BL.NORTH)
  button(text:'Click Me',
     actionPerformed: {count++; textlabel.text = "Clicked ${count} time(s)."; println "clicked"}, constraints:BL.SOUTH)
  }
}

下面是实际的窗口:

SwingBuilder001

SwingBuilder001

一般说来,层级组件通常是利用一系列重复的实例化 和 setter 方法,最后将子对象添加到相应的父对象上。而通过 SwingBuilder ,利用原生形式来定义这种层级,使界面设计变得更容易理解,只需读读代码即可理解。

利用 Groovy 中许多内建功能,比如说闭包、隐式地调用构造函数、别名式导入以及字符串插值等,可以实现很多灵活性。使用 SwingBuilder 完全不必理解这么多,如上例所示,它的使用是非常直观的。

下面介绍的例子稍微复杂一些,通过闭包来实现 SwingBuilder 代码重用。

import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*

def swing = new SwingBuilder()

def sharedPanel = {
   swing.panel() {
    label("Shared Panel")
  }
}

count = 0
swing.edt {
  frame(title: 'Frame', defaultCloseOperation: JFrame.EXIT_ON_CLOSE, pack: true, show: true) {
    vbox {
      textlabel = label('Click the button!')
      button(
        text: 'Click Me',
        actionPerformed: {
          count++
          textlabel.text = "Clicked ${count} time(s)."
          println "Clicked!"
        }
      )
      widget(sharedPanel())
      widget(sharedPanel())
    }
  }
}  

下面是一种依赖显式的 bean 和绑定的变体形式:

import groovy.swing.SwingBuilder
import groovy.beans.Bindable

class MyModel {
   @Bindable int count = 0
}

def model = new MyModel()
new SwingBuilder().edt {
  frame(title: 'Java Frame', size: [100, 100], locationRelativeTo: null, show: true) {
  gridLayout(cols: 1, rows: 2)
  label(text: bind(source: model, sourceProperty: 'count', converter: { v ->  v? "Clicked $v times": ''}))
  button('Click me!', actionPerformed: { model.count++ })
  }
}  

@Bindable 是一种核心 AST 转换。它可以生成将简单 bean 转换为显式 bean 所需的所有样本文件代码, bind() 节点创建出正确的 PropertyChangeListeners ,用于在 PropertyChangeEvent 触发时更新感兴趣的部分。

8.2.10. AntBuilder

尽管 Apache Ant 主要是一种构建工具,但它其实还是一种实用性极强的文件操作工具,可以进行压缩、复制以及资源处理等文件操作。但如果你使用过 build.xml 文件或一些 Jelly 脚本的话,你就会觉得受到尖括号的限制,或者觉得将 XML 用作脚本语言这一点显得很怪异。你可能想要一种更简洁更直观的东西,那么或许利用 Groovy 和 Ant 编写脚本就是你最好的选择。

Groovy 有一个辅助类: AntBuilder ,它能使利用 Ant 编写脚本这项工作变得非常轻松,而且还能允许使用真正的脚本语言来编写构造(变量、方法、循环、逻辑分支、类,等等)。它看起来还像是 Ant XML 的一种内嵌简化版,不带有任何尖括号——当然,你可以将这种标记混合并配置在脚本中。Ant 本身就是一个 Jar 文件的集合。通过将它们添加到类路径中,可以轻松地在 Groovy 中使用它们。我们相信,使用 AntBuilder 可以形成一种更简洁、可读性更好的语法。

AntBuilder 直接使用我们在 Groovy 中所习惯的构建器表示法来表示 Ant 任务。下面是一个最简单的例子,在标准输出中打印一条消息:

def ant = new AntBuilder()      1⃣️
ant.echo('hello from Ant!')     2⃣️

1⃣️ 创建一个 AntBuilder 实例。
2⃣️ 利用参数中的消息执行 echo 任务。

假设需要创建 Zip 文件:

def ant = new AntBuilder()
ant.zip(destfile: 'sources.zip', basedir: 'src')

下面的例子展示了 AntBuilder 的一个用法,如何在 Groovy 中直接使用典型的 Ant 模式来复制一个文件列表:


// 调用一个任务   
ant.echo("hello")

// Groovy 标记中的一个 Ant 代码块  
ant.sequential {
  echo("inside sequential")
  def myDir = "target/AntTest/"
  mkdir(dir: myDir)
  copy(todir: myDir) {
    fileset(dir: "src/test") {
      include(name: "**/*.groovy")
    }
  }
  echo("done")
}

// 常见的 Groovy 语句
def file = new File(ant.project.baseDir,"target/AntTest/groovy/util/AntTest.groovy")
assert file.exists()

下例展示的是如何按照特定模式对一个文件列表进行迭代:

// 创建一个文件集合扫描器  
def scanner = ant.fileScanner {
  fileset(dir:"src/test") {
    include(name:"**/Ant*.groovy")
  }
}

// 进行迭代  
def found = false
for (f in scanner) {
  println("Found file $f")
  found = true
  assert f instanceof File
  assert f.name.endsWith(".groovy")
}
assert found

执行一个 JUnit 测试:

// 创建文件集合扫描器  
ant.junit {
  test(name:'groovy.util.SomethingThatDoesNotExist')
}

我们还可以更进一步地直接在 Groovy 中编译执行 Java 文件:

ant.echo(file:'Temp.java', '''
  class Temp {
    public static void main(String[] args) {
      System.out.println("Hello");
    }
  }
''')
ant.javac(srcdir:'.', includes:'Temp.java', fork:'true')
ant.java(classpath:'.', classname:'Temp', fork:'true')
ant.echo('Done')

值得注意的是, AntBuilder 隶属于 Gradle ,所以你可以在 Gradle 中使用它,就像在 Groovy 中一样。详情可参看 Gradle 手册

8.2.11. CliBuilder

(未编写)

8.2.12. ObjectGraphBuilder

针对能够遵循 JavaBean 规范的 bean, ObjectGraphBuilder 可以构建出任意的 bean 图表,特别适用于创建测试数据。

先来看看下面这些类:

package com.acme

class Company {
  String name
  Address address
  List employees = []
}

class Address {
  String line1
  String line2
  int zip
  String state
}

class Employee {
  String name
  int employeeId
  Address address
  Company company
}  

然后使用 ObjectGraphBuilder 构建一个带有三个雇员的 Company ,这非常简单:


def builder = new ObjectGraphBuilder()              1⃣️            
builder.classLoader = this.class.classLoader          2⃣️
builder.classNameResolver = "com.acme"              3⃣️

def acme = builder.company(name: 'ACME') {            4⃣️
  3.times {
    employee(id: it.toString(), name: "Drone $it") {    5⃣️
      address(line1:"Post street")            6⃣️
    }
  }
}

assert acme != null
assert acme instanceof Company
assert acme.name == 'ACME'
assert acme.employees.size() == 3
def employee = acme.employees[0]
assert employee instanceof Employee
assert employee.name == 'Drone 0'
assert employee.address instanceof Address  

1⃣️ 创建一个新的对象图表构建器。
2⃣️ 设置用来解决类的类加载器。
3⃣️ 设置要解决类的基本包名。
4⃣️ 创建一个 Company 实例。
5⃣️ 以及 3 个 Employee 实例。
6⃣️ 每个实例都有一个独一无二的 Address

其实,对象图表构建器在幕后完成了下列任务:

  • 试图匹配节点名为一个 Class ,采用需要包名的默认 ClassNameResolver 策略。
  • 然后将创建一个合适的类实例,该类使用默认的 NewInstanceResolver 策略,调用一个无参构造函数。
  • 解决内嵌节点的父子关系,用到以下两种策略:
    • RelationNameResolver 将输出父节点的子属性名称,以及子节点的父属性名称(在该例中, Employee 的父属性将被命名为 company )。
    • ChildPropertySetter 会将子节点插入父节点,考虑子节点是否属于 Collection 。(本例中, employees 应是 Company 下的 Employee 实例。)

所有这 4 个策略都有一个默认的实现,如果代码遵循编写 JavaBean 的常见规范,这个默认实现就能正常运作。在遇到 bean 或对象并不遵循常见规范时,就需要用你自己的策略实现。比如构建一个不可变的类:

@Immutable
class Person {
  String name
  int age
}

假如想用构建器创建一个 Person

def person = builder.person(name:'Jon', age:17)

那么在运行时就会失败,相关消息为:

Cannot set readonly property: name for class: com.acme.Person

改变新实例的策略就能修补这个问题:

builder.newInstanceResolver = { Class klazz, Map attributes ->
  if (klazz.isAnnotationPresent(Immutable)) {
    def o = klazz.newInstance(attributes)
    attributes.clear()
    return o
  }
  klazz.newInstance()
}

ObjectGraphBuilder 支持每个节点的 id,这即是说可以将节点引用保存在构建器中。这非常适用于多个对象引用一个实例的情况。在一些域模型 ObjectGraphBuilder 可能存在有名为 IdentifierResolver 的策略,以便可以配置以改变默认名称,在这种情况下,名为 id 的属性可能会有其他的业务含义。对于用来引用之前保存的实例的属性来说,这一情况也可能存在, ReferenceResolver 策略可能会产生正确的值(默认是 refId ):

def company = builder.company(name: 'ACME') {
  address(id: 'a1', line1: '123 Groovy Rd', zip: 12345, state: 'JV')      1⃣️
  employee(name: 'Duke', employeeId: 1, address: a1)              2⃣️
  employee(name: 'John', employeeId: 2 ){
    address( refId: 'a1' )                          3⃣️
  }
}

1⃣️ 创建的地址都有个 id
2⃣️ employee 能根据 id 来引用 address
3⃣️ 或者使用相关地址的 refId 属性。

值得注意的是,对于被引用的 Bean 而言,你不能改变它的属性值。

8.2.13. JmxBuilder

详情参见 Working with JMX - JmxBuilder

8.2.14. FileTreeBuilder

FileTreeBuilder 构建器用来生成符合特定规范的文件目录结构。比如创建下面这样的树:

src/
  |--- main
  |   |--- groovy
  |      |--- Foo.groovy
  |--- test
    |--- groovy
         |--- FooTest.groovy

可以使用一个 FileTreeBuilder

tmpDir = File.createTempDir()
def fileTreeBuilder = new FileTreeBuilder(tmpDir)
fileTreeBuilder.dir('src') {
  dir('main') {
     dir('groovy') {
      file('Foo.groovy', 'println "Hello"')
     }
  }
  dir('test') {
     dir('groovy') {
      file('FooTest.groovy', 'class FooTest extends GroovyTestCase {}')
     }
  }
 }

还可以使用 assert 来检查一切是否如我们预期所愿:

assert new File(tmpDir, '/src/main/groovy/Foo.groovy').text == 'println "Hello"'
assert new File(tmpDir, '/src/test/groovy/FooTest.groovy').text == 'class FooTest extends GroovyTestCase {}'

FileTreeBuilder 还支持一种简写语法:

tmpDir = File.createTempDir()
def fileTreeBuilder = new FileTreeBuilder(tmpDir)
fileTreeBuilder.src {
  main {
     groovy {
      'Foo.groovy'('println "Hello"')
     }
  }
  test {
     groovy {
      'FooTest.groovy'('class FooTest extends GroovyTestCase {}')
     }
  }
 }

产生的目录结构与之前的那些 assert 所展示的相同:

assert new File(tmpDir, '/src/main/groovy/Foo.groovy').text == 'println "Hello"'
assert new File(tmpDir, '/src/test/groovy/FooTest.groovy').text == 'class FooTest extends GroovyTestCase {}'  

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文