8 构建器
(未编写)
8.1. Creating a builder
(未编写)
8.1.1. BuilderSupport
(未编写)
8.1.2. FactoryBuilderSupport
(未编写)
8.2. Existing builders
(未编写)
8.2.1. 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
StreamingJsonBuilder
与 JsonBuilder
不同之处在于, JsonBuilder
在内存中创建数据结构,假如想要在输出前程序性地改变结构,那么这一构建器非常方便,而 StreamingJsonBuilder
则采用流式写入,无需任何中间的内存数据结构。如果不需要修改数据结构,使用一种存储效率更高的方法,则应该使用 StreamingJsonBuilder
。
在用法上, StreamingJsonBuilder
和 JsonBuilder
差不多,以下面这个 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
一般说来,层级组件通常是利用一系列重复的实例化 和 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论