欢迎阅读 Groovy 文档

原文:http://www.groovy-lang.org/documentation.html

起步

Groovy 安装

下载

在下载区,你可以下载 Groovy 的安装包,源文件,windows 安装文件以及文档。 为了能轻松快速的在 Mac OSX , Linux 以及 Cygwin 上开始使用,你可以使用 GVM ( Groovy 环境管理器),下载并配置你选择的 Groovy 版本。下面是关于其的基础说明。

稳定版本

你可以从发布记录以及变更日志中了解此版本的更多信息。

如果你计划使用 invokedynamic ,可以阅读这些 文档

快照版本

如果你希望体验 Groovy 中最新版本,可以使用快照版本 (snapshot) 构建。 快照版本在 CI 服务中构建成功就会立即发布到我们的 Artifactory’s OSS 快照仓库中。

前置条件

Groovy 2.4 需要 Java 6 以上版本支持,完美支持需要升级至 Java 8。目前在使用 Java 9 的 snapshot 上还存在一些问题。groovy-nio 模块需要 Java 7 以上版本支持。使用 invokeDynamic 的特性可以使 Java 7+,但还是建议使用 Java 8。

Groovy CI 服务对于 Java 版本对 Groovy 版本支持情况的检查非常有用。测试套件 (接近 10000 个测试用例)将鉴证当前主流 Java 对于 Groovy 的支持情况。

Maven Repository

如果你希望在应用中集成 Groovy , 你只需要使用你习惯的 maven repositoriesJCenter maven repository

稳定版本
Gradle Maven Explanation
org.codehaus.groovy:groovy:2.4.5
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId> <version>2.4.5</version>
Just the core of groovy without the modules (see below)
org.codehaus.groovy:groovy- $module:2.4.5` <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-$module</artifactId> <version>2.4.5</version>
Example:
<artifactId>groovy-sql </artifactId>
org.codehaus.groovy:groovy-all:2.4.5 <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.4.5</version>  

使用 InvokeDynamic 的 jars ,在 Gradle 中添加 :indy 或在 Maven 中添加 <classifier>indy</classifier>

GVM (the Groovy enVironment Manager)

使用 GVM 在 这些平台 (Mac OSX, Linux, Cygwin, Solaris or FreeBSD) 的 Bash 上安装 Groovy 都会非常的容易。

打开一个 terminal ,输入:

$ curl -s get.gvmtool.net | bash

根据屏幕上的引导说明完成安装。

打开一个新的 terminal 或输入下面命令:

$ source "$HOME/.gvm/bin/gvm-init.sh"

开始安装最新稳定版本的 Groovy:

$ gvm install groovy

安装完毕后,此版本为当前的默认版本,可以输入以下命令测试:

$ groovy -version

就是如此简单。

其他方式安装 Groovy

Mac OS X 上安装

MacOS 上已经安装 MacPorts ,可以使用下面命令安装:

sudo port install groovy

MacOS 上已经安装 Homebrew , 可以使用下面命令安装:

       brew install groovy

Windows 上安装

在 Windows 上可以使用 NSIS Windows installer.

Other Distributions

You may download other distributions of Groovy from this site.

Source Code

如果你更愿意尝鲜,也可以从 GitHub 上下载 源代码

IDE plugin

如果你使用 IDE , 你也可以使用最新的 IDE 插件,依照插件的安装步骤完成。

安装二进制包

这里将介绍通过二进制包,安装 Groovy.

  • 首先 下载 ,在文件系统将下载文件解压。
  • 设置环境变量 GROOVY_HOME 为文件的解压目录。
  • GROOVY_HOME/bin 加入到 PATH 中。
  • 设置 JAVA_HOME 环境变量指向你的 JDK。在 OS X 上为目录 /Library/Java/Home,在其他 unix 系统中可能在

/usr/java 目录中。如果你已经安装 Ant 或 Maven 等工具,可以忽略这一步。

现在你已经安装好了 Groovy , 可以通过下面命令测试:

groovysh

这将会创建一个 Groovy 的交互 shell ,用于执行 Groovy 代码。也可以运行 Swing interactive console ,如:

groovyConsole

运行一段 Groovy script 可以输入:

groovy SomeScript

与 Java 不同之处

Groovy 试图使 Java 开发人员的学习曲线更加平滑。在设计 Groovy 时,我们一直在努力遵循最小意外原则,特别对于有 Java 开发背景的开发人员。

这里我们将列举 JavaGroovy 之间所有主要不同之处。

Default imports

这些 packagesclasses 是默认引入,你可以无需明确的引入,即可使用:

java.io.*

java.lang.*

java.math.BigDecimal

java.math.BigInteger

java.net.*

java.util.*

groovy.lang.*

groovy.util.*

Multi-methods

在 Groovy 中,在运行时选择需要被执行的方法。这被称为运行时路由 或 multi-methods 。这意味着在运行时,根据参数类型来确定调用方法。 在 Java 中,方法选择是在编译时,依赖声明的类型。

下面代码可以在 JavaGroovy 中编译通过,但是执行结果并不相同:

int method(String arg) {
    return 1;
}
int method(Object arg) {
    return 2;
}
Object o = "Object";
int result = method(o);

Java 中的结果:

assertEquals(2, result);

下面是 Groovy 中的结果:

assertEquals(1, result);

在 Java 中的使用静态类型,这里 o 声明为 Object , 然而 Groovy 在方法调用时,选择具体参数类型。这里在运行时为 String ,则调用了上面的第一个方法。

Array initializers

在 Groovy 中, { ... } 是为闭包预留. 这就意味着你无法使用这样的语句创建 list :

int[] array = { 1, 2,  3}

你需要这样写:

int[] array = [1,2,3]

Package scope visibility

在 Groovy 中数据域上省略修饰符,与在 Java 中不同访问范围不同:

class Person {
    String name
}

Instead, it is used to create a property, that is to say a private field, an associated getter and an associated setter.

创建 package-private 成员变量,需要使用注解 @PackageScope:

class Person {
    @PackageScope String name
}

ARM blocks

ARM (Automatic Resource Management) 源自 Java 7, Groovy 中并没有支持。替代其, Groovy 中提供多种依赖闭包的方法来达到相同的效果并更加灵活便利, 例如:

Path file = Paths.get("/path/to/file");
Charset charset = Charset.forName("UTF-8");
try (BufferedReader reader = Files.newBufferedReader(file, charset)) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }

} catch (IOException e) {
    e.printStackTrace();
}

可以这样写:

new File('/path/to/file').eachLine('UTF-8') {
   println it
}

更接近 Java 的写法:

new File('/path/to/file').withReader('UTF-8') { reader ->
   reader.eachLine {
       println it
   }
}

Inner classes

实现匿名内部类和嵌套类继承了 Java 中的思想,但是其与 Java 语言中的规范还有比较大的差别。这里的实现看起来很像 groovy.lang.Closure ,有一些好处及不同。 访问 private fields 和 方法成为一个问题,但一方面本地变量不必为 final 。

Static inner classes

例如:

class A {
    static class B {}
}

new A.B()

The usage of static inner classes is the best supported one. If you absolutely need an inner class, you should make it a static one.

Anonymous Inner Classes
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

CountDownLatch called = new CountDownLatch(1)

Timer timer = new Timer()
timer.schedule(new TimerTask() {
    void run() {
        called.countDown()
    }
}, 0)

assert called.await(10, TimeUnit.SECONDS)
Creating Instances of Non-Static Inner Classes

In Java you can do this:

public class Y {
    public class X {}
    public X foo() {
        return new X();
    }
    public static X createX(Y y) {
        return y.new X();
    }
}

Groovy 并不支持 y.new X() 句法。你可以这样写 new X(y) 来替代,就像下面的代码:

public class Y {
    public class X {}
    public X foo() {
        return new X()
    }
    public static X createX(Y y) {
        return new X(y)
    }
}

注意,Groovy 支持调用只有一个参数的方法,而不输入参数,这样参数的赋值为 null。 在调用构造方法时,也是这样。当你使用 new X() 替代 new X(this) 会有危险,因为这种常规方式,我们还没有找到更好的方式来避免这种问题的出现。

Lambdas

Java 8 支持 lambdas 和 方法引用:

Runnable run = () -> System.out.println("Run");
list.forEach(System.out::println);

Java 8 lambdas can be more or less considered as anonymous inner classes. Groovy doesn’t support that syntax, but has closures instead: Java 8 lambdas 可以或多或少被认为是匿名内部类。Groovy 中不支持这种语法,但可以使用闭包代替:

Runnable run = { println 'run' }
list.each { println it } // or list.each(this.&println)

GStrings

双引号字符被解释为 GStrings, 当字符串中包含美元符号在 Groovy 和 Java 编译器中编译时可以能会引起编译失败或细微的差别。

通常, Groovy 将会自动转换 GString 与 String,如果接口上明确定义参数类型,需要注意, Java 接口接受一个 Object 参数,并检查其实际类型。

String and Character literals

Groovy 中使用单引号定义 String, 双引号可以定义 String 或 GString, 取决于其是否插入字符。

assert 'c'.getClass()==String
assert "c".getClass()==String
assert "c${1}".getClass() in GString

Groovy 中声明变量为 char 将会自动转化单字符串为 char。 当调用的方法参数为 char ,你需要之前确定类型的一致性。

char a='a'
assert Character.digit(a, 16)==10 : 'But Groovy does boxing'
assert Character.digit((char) 'a', 16)==10

try {
  assert Character.digit('a', 16)==10
  assert false: 'Need explicit cast'
} catch(MissingMethodException e) {
}

Groovy 中支持两种方式转化 char,这里有一些细微的不同当转化多字符字符串时。 Groovy 转化方式容错更强,其将只转化第一个字符,当 C-style 转化将会抛出异常:

// for single char strings, both are the same
assert ((char) "c").class==Character
assert ("c" as char).class==Character

// for multi char strings they are not
try {
  ((char) 'cx') == 'c'
  assert false: 'will fail - not castable'
} catch(GroovyCastException e) {
}
assert ('cx' as char) == 'c'
assert 'cx'.asType(char) == 'c'

Primitives and wrappers

Groovy 中一切皆对象,原始类型会被自动包装为引用。 因为如此,它不会像 Java 那样类型拓阔优先于装箱,例如: Because Groovy uses Objects for everything, it autowraps references to primitives. Because of this, it does not follow Java’s behavior of widening taking priority over boxing. Here’s an example using int

int i
m(i)

void m(long l) {                // <1>
  println "in m(long)"
}

void m(Integer i) {            // <2>
  println "in m(Integer)"
}

<1> Java 中的调用路径,类型范围拓宽优先于拆箱 (This is the method that Java would call, since widening has precedence over unboxing.)

<2> Groovy 中的调用路径,其中所有原始类型引用会使用其包装类。 (This is the method Groovy actually calls, since all primitive references use their wrapper class.)

Behaviour of ==

Java 中 == 表明原始类型相等或对象地址相同。 Groovy 中 == 被翻译为 a.compareTo(b)==0, 并且他们都是 Comparable 并且 a.equals(b); 为检查其地址相同,这里使用 is 例如: a.is(b)

Conversions

Java 自动拓宽或收窄:

Table 1. Java Conversions

Conversions to boolean byte short char int long float double
Conversions from
boolean
N N N N N N N
byte N
Y C Y Y Y Y
short N C
C Y Y Y Y
char N C C
Y Y Y Y
int N C C C
Y T Y
long N C C C C
T T
float N C C C C C
Y
double N C C C C C C

注解

  • Y 表示 Java 自动转换, C 表示需要指定明确的类型转换,T 表示转换过程中有数据丢失,N 表示 Java 无法转换

Groovy expands greatly on this.

Table 2. Groovy Conversions

Conversions to boolean Boolean byte Byte short Short char Character int Integer long Long BigInteger float Float double Double BigDecimal
Conversions from
boolean
B N N N N N N N N N N N N N N N N
Boolean B
N N N N N N N N N N N N N N N N
byte T T
B Y Y Y D Y Y Y Y Y Y Y Y Y Y
Byte T T B
Y Y Y D Y Y Y Y Y Y Y Y Y Y
short T T D D
B Y D Y Y Y Y Y Y Y Y Y Y
Short T T D T B
Y D Y Y Y Y Y Y Y Y Y Y
char T T Y D Y D
D Y D Y D D Y D Y D D
character T T D D D D D
D D D D D D D D D D
int T T D D D D Y D
B Y Y Y Y Y Y Y Y
Integer T T D D D D Y D B
Y Y Y Y Y Y Y Y
long T T D D D D Y D D D
B Y T T T T Y
Long T T D D D T Y D D T B
Y T T T T Y
BigInteger T T D D D D D D D D D D
D D D D T
float T T D D D D T D D D D D D
B Y Y Y
Float T T D T D T T D D T D T D B
Y Y Y
double T T D D D D T D D D D D D D D
B Y
Double T T D T D T T D D T D T D D T B
Y
BigDecimal T T D D D D D D D D D D D T D T D

注解

  • Y 表示 Groovy 自动转换,D 表示 Groovy 可以在编译期间或明确类型时进行转换, T 表示转换过程中有数据丢失,B 表示装箱及拆箱操作, N 表示 Groovy 无法转换

The truncation uses Groovy Truth when converting to boolean/Boolean. Converting from a number to a character casts the Number.intvalue() to char. Groovy constructs BigInteger and BigDecimal using Number.doubleValue() when converting from a Float or Double, otherwise it constructs using toString(). Other conversions have their behavior defined by java.lang.Number.

新增关键字

Grooy 中新增了一些关键字,可以使用它们来命名变量

  • as
  • def
  • in
  • trait

default must be last in switch case

default 必须在 switch/case 的结尾。在 Java 中 default 可以在 switch/case 中任何位置出现。 default must go at the end of the switch/case. While in Java the default can be placed anywhere in the switch/case, the default in Groovy is used more as an else than assigning a default case.

Groovy 开发包

Working with IO

Groovy 提供了大量的辅助方法用于 IO 处理。你可以使用标准的 JAVA 代码在 Groovy 中处理这些问题,Groovy 也提供了很多便利的方法处理文件,流,reader 等等

特别可以可以看一下,下面类中添加的方法:

下面章节将主要介绍这些辅助方法使用的范例,但并不全面,如果需要了解更全面的内容,可以查看 GDK API .

读取文件

第一个实例,看看如何打印出文件中的每行内容:

new File(baseDir, 'haiku.txt').eachLine { line ->
    println line
}

方法 eachLine 是由 Groovy 自动加入 File 类中。 如果你希望知道行号,可以使用下面方法:

new File(baseDir, 'haiku.txt').eachLine { line, nb ->
    println "Line $nb: $line"
}

如果有情况在 eachLine 方法中抛出异常,这方法将会确保资源是被合理关闭。Groovy 针对所有的 IO 处理都会遵循此中方式。

在一定条件下,我们使用 Reader ,同样也会感受到 Groovy 对资源自动化管理的好处。 下面的实例,将演示在异常发生时,reader 也将会被关闭。

def count = 0, MAXSIZE = 3
new File(baseDir,"haiku.txt").withReader { reader ->
    while (reader.readLine()) {
        if (++count > MAXSIZE) {
            throw new RuntimeException('Haiku should only have 3 verses')
        }
    }
}

如果你希望将每一行内容存入 list, 你可以这么办:

def list = new File(baseDir, 'haiku.txt').collect {it}

或者你可以通过 as 操作符将内容转化为数组:

def array = new File(baseDir, 'haiku.txt') as String[]

将文件内容写入 byte[] 将要写多少代码?

Groovy 让这一切变的如此简单:

byte[] contents = file.bytes

使用 IO 不仅仅在处理文件。事实上,大量的操作依靠输入输出流,这也是 Groovy 增加了大量方法支持其,可以查看 文档

在下面的例子中,你可以很容易从文件中的获取 InputStream :

def is = new File(baseDir,'haiku.txt').newInputStream()
// do something ...
is.close()

然而你会看到,这样处理你将不得不去执行关闭 inputstream 。在 Groovy 中有更好的办法,使用 withInputStream 来帮助你处理这一切:

new File(baseDir,'haiku.txt').withInputStream { stream ->
    // do something ...
}
写文件

当然在一些情况下,我们不仅仅要读文件,也需要来写文件。 我们有一个选择就是使用 Writer:

new File(baseDir,'haiku.txt').withWriter('utf-8') { writer ->
    writer.writeLine 'Into the ancient pond'
    writer.writeLine 'A frog jumps'
    writer.writeLine 'Water’s sound!'
}

但或者还有更简单的方法,使用 << 操作符就可以满足需求:

new File(baseDir,'haiku.txt') << '''Into the ancient pond
A frog jumps
Water’s sound!'''

通常我们并不处理文本内容,所以你可以使用 Writer 或直接将字节写入,如:

file.bytes = [66,22,11]

你也可以直接操作输出流,向下面这样:

def os = new File(baseDir,'data.bin').newOutputStream()
// do something ...
os.close()

当然你同样看到这里的操作,还是需要关闭输出流。同样我们可以使用 withOutputStream 来更优雅的处理关闭与异常:

new File(baseDir,'data.bin').withOutputStream { stream ->
    // do something ...
}
遍历文件树

在脚本中,遍历文件树,找到指定的文件并处理是比较常见的任务。Groovy 提供了多种方法进行处理。

列出文件目录下的所有文件:

dir.eachFile { file ->    //<1>
    println file.name
}
dir.eachFileMatch(~/.*\.txt/) { file ->   //<2>
    println file.name
}

<1> 目录中找到的文件,执行闭包中代码

<2> 目录中满足条件文件,执行闭包中代码

通常情况下,你需要处理更深层次的文件,这种情况下可以使用 eachFileRecurse:

dir.eachFileRecurse { file ->    //<1>
    println file.name
}

dir.eachFileRecurse(FileType.FILES) { file ->  //<2>
    println file.name
}

<1> 目录中递归查找到文件或目录后执行闭包中代码

<2> 目录中递归查找到文件后执行闭包中代码

对于更复杂的遍历技术你可以使用 traverse 方法,在这里需要你按指定遍历需求,进行相应的配置:

dir.traverse { file ->
    if (file.directory && file.name=='bin') {
        FileVisitResult.TERMINATE               //<1>
    } else {
        println file.name
        FileVisitResult.CONTINUE                //<2>
    }

}

<1> 如果当前文件为目录并且被命名为 bin , 将停止遍历

<2> 否则打印文件名,继续遍历

Data & objects

在 JAVA 中通常使用 java.io.DataOutputStream & java.io.DataInputStream 来做对象的序列化与反序列化。 在 Groovy 中的使用将更加简单。 看看下面的例子,你可以使用下面代码将数据序列化至文件中,并在将其反序列化:

boolean b = true
String message = 'Hello from Groovy'
// Serialize data into a file
file.withDataOutputStream { out ->
    out.writeBoolean(b)
    out.writeUTF(message)
}
// ...
// Then read it back
file.withDataInputStream { input ->
    assert input.readBoolean() == b
    assert input.readUTF() == message
}

如果你数据对象实现 Serializable 接口,你可以使用下面关于对象的输出流来处理:

Person p = new Person(name:'Bob', age:76)
// Serialize data into a file
file.withObjectOutputStream { out ->
    out.writeObject(p)
}
// ...
// Then read it back
file.withObjectInputStream { input ->
    def p2 = input.readObject()
    assert p2.name == p.name
    assert p2.age == p.age
}
执行外部进程

前面章节描述了 Groovy 中文件,流,reader的处理方式。然而对于系统管理员或开发人员,他们通常都需要与系统相关进程进行交互。 Groovy 也提供了一种简单的方式来执行命令行进程。可以简单的使用一行字符串,然后调用 exxcute() 方法来执行。 例如:

def process = "ls -l".execute()         //<1>
println "Found text ${process.text}"    //<2>

<1> 执行 ls 命令

<2> 打印命令输出结果

execute() 返回 java.lang.Process 实例,其可以通过 in/out/err streams 处理,并且检查进程退出值。 与上面命令一样,这里我们会对进程返回结果数据以流方式读取,每次读取一行数据:

def process = "ls -l".execute()         //<1>
process.in.eachLine { line ->           //<2>
    println line                                        //<3>
}

<1> 执行命令

<2> 按行读取进程输出数据

<3> 打印数据结果

值得注意的是与输入流对应的输出流命令,你可以发送数据至进程的 out .

请注意很多命令都是内建于 shell 中,如果你想在 windows 系统下列出当前目录中的文件,并且怎么写:

def process = "dir".execute()
println "${process.text}"

你就会收到 IOException 描述:Cannot run program “dir”: CreateProcess error=2, The system cannot find the file specified (无法执行 dir : CreateProcess error=2, 系统无法找到制定的文件)

这是因为 dir 内建于 cmd.exe 中,并无法单独执行。 你需要这样写:

def process = "cmd /c dir".execute()
println "${process.text}"

此外, java.lang.Process 中也隐藏了一些功能缺陷,值得我们关注。 在 javadoc 中有如下描述:

注解

由于一些平台给输入输出流提供的缓存十分有限,从子进程中无法及时写入或读取,将导致子进程阻塞或死锁。 Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the subprocess may cause the subprocess to block, and even deadlock

正因为如此,Groovy提供一些额外的辅助方法,这使得流处理的过程更容易。 下面代码将演示如果如何吞掉你程序的输出(包括错误输出流):

def p = "rm -f foo.tmp".execute([], tmpDir)
p.consumeProcessOutput()
p.waitFor()

这里有一些 consumeProcessOutput 的变化,在使用 StringBuffer, InputStream, OutputStream 等时。 具体的例子,请查看 GDK API for java.lang.Process .

另外,这里还有 pipeTo 命令,可以将进程中输出流导入到其他进程的输入流中。

参考下面例子:

Pipes in action
proc1 = 'ls'.execute()
proc2 = 'tr -d o'.execute()
proc3 = 'tr -d e'.execute()
proc4 = 'tr -d i'.execute()
proc1 | proc2 | proc3 | proc4
proc4.waitFor()
if (proc4.exitValue()) {
    println proc4.err.text
} else {
    println proc4.text
}
Consuming errors
def sout = new StringBuilder()
def serr = new StringBuilder()
proc2 = 'tr -d o'.execute()
proc3 = 'tr -d e'.execute()
proc4 = 'tr -d i'.execute()
proc4.consumeProcessOutput(sout, serr)
proc2 | proc3 | proc4
[proc2, proc3].each { it.consumeProcessErrorStream(serr) }
proc2.withWriter { writer ->
    writer << 'testfile.groovy'
}
proc4.waitForOrKill(1000)
println "Standard output: $sout"
println "Standard error: $serr"

Working with collections

Groovy 提供各种集合类型,包括 listsmapsranges。 其中大部份都是以 Java 集合类型为基础,并添加了一些附加方法在 Groovy 开发包 中。

Lists
List literals

你可以像下面那样创建 lists 。[] 用于创建空的列表。

def list = [5, 6, 7, 8]
assert list.get(2) == 7
assert list[2] == 7
assert list instanceof java.util.List

def emptyList = []
assert emptyList.size() == 0
emptyList.add(5)
assert emptyList.size() == 1

每个 list 表达式都实现了 java.util.List。 当让 lists 也可以用于创建新的 lists。

def list1 = ['a', 'b', 'c']
//construct a new list, seeded with the same items as in list1
def list2 = new ArrayList<String>(list1)

assert list2 == list1 // == checks that each corresponding element is the same

// clone() can also be called
def list3 = list1.clone()
assert list3 == list1

list 是有序序列对象:

def list = [5, 6, 7, 8]
assert list.size() == 4
assert list.getClass() == ArrayList     // the specific kind of list being used

assert list[2] == 7                     // indexing starts at 0
assert list.getAt(2) == 7               // equivalent method to subscript operator []
assert list.get(2) == 7                 // alternative method

list[2] = 9
assert list == [5, 6, 9, 8,]           // trailing comma OK

list.putAt(2, 10)                       // equivalent method to [] when value being changed
assert list == [5, 6, 10, 8]
assert list.set(2, 11) == 10            // alternative method that returns old value
assert list == [5, 6, 11, 8]

assert ['a', 1, 'a', 'a', 2.5, 2.5f, 2.5d, 'hello', 7g, null, 9 as byte]
//objects can be of different types; duplicates allowed

assert [1, 2, 3, 4, 5][-1] == 5             // use negative indices to count from the end
assert [1, 2, 3, 4, 5][-2] == 4
assert [1, 2, 3, 4, 5].getAt(-2) == 4       // getAt() available with negative index...
try {
    [1, 2, 3, 4, 5].get(-2)                 // but negative index not allowed with get()
    assert false
} catch (e) {
    assert e instanceof ArrayIndexOutOfBoundsException
}
列表用于布尔表式
.列表返回布尔值
assert ![]             // an empty list evaluates as false

//all other lists, irrespective of contents, evaluate as true
assert [1] && ['a'] && [0] && [0.0] && [false] && [null]
列表迭代器

可使用 each 和 eachWithIndex 方法,用于列表上元素的迭代操作,可参考下面代码: .. code-block:: groovy

[1, 2, 3].each {
println “Item: $it” // it is an implicit parameter corresponding to the current element

} [‘a’, ‘b’, ‘c’].eachWithIndex { it, i -> // it is the current element, while i is the index

println “$i: $it”

}

在迭代的基础上,通常需要将列表中的元素经过一定转换,构建新的列表。这种操作称为 mapping ,在 Groovy 中可以使用 collect 方法:

assert [1, 2, 3].collect { it * 2 } == [2, 4, 6]

// shortcut syntax instead of collect
assert [1, 2, 3]*.multiply(2) == [1, 2, 3].collect { it.multiply(2) }

def list = [0]
// it is possible to give `collect` the list which collects the elements
assert [1, 2, 3].collect(list) { it * 2 } == [0, 2, 4, 6]
assert list == [0, 2, 4, 6]
操作列表
过滤和查询

Groovy 开发包 中,集合上包含了很多方法用于增强标准集合的处理,可以看下面的例子:

assert [1, 2, 3].find { it > 1 } == 2           // find 1st element matching criteria
assert [1, 2, 3].findAll { it > 1 } == [2, 3]   // find all elements matching critieria
assert ['a', 'b', 'c', 'd', 'e'].findIndexOf {      // find index of 1st element matching criteria
    it in ['c', 'e', 'g']
} == 2

assert ['a', 'b', 'c', 'd', 'c'].indexOf('c') == 2  // index returned
assert ['a', 'b', 'c', 'd', 'c'].indexOf('z') == -1 // index -1 means value not in list
assert ['a', 'b', 'c', 'd', 'c'].lastIndexOf('c') == 4

assert [1, 2, 3].every { it < 5 }               // returns true if all elements match the predicate
assert ![1, 2, 3].every { it < 3 }
assert [1, 2, 3].any { it > 2 }                 // returns true if any element matches the predicate
assert ![1, 2, 3].any { it > 3 }

assert [1, 2, 3, 4, 5, 6].sum() == 21                // sum anything with a plus() method
assert ['a', 'b', 'c', 'd', 'e'].sum {
    it == 'a' ? 1 : it == 'b' ? 2 : it == 'c' ? 3 : it == 'd' ? 4 : it == 'e' ? 5 : 0
    // custom value to use in sum
} == 15
assert ['a', 'b', 'c', 'd', 'e'].sum { ((char) it) - ((char) 'a') } == 10
assert ['a', 'b', 'c', 'd', 'e'].sum() == 'abcde'
assert [['a', 'b'], ['c', 'd']].sum() == ['a', 'b', 'c', 'd']

// an initial value can be provided
assert [].sum(1000) == 1000
assert [1, 2, 3].sum(1000) == 1006

assert [1, 2, 3].join('-') == '1-2-3'           // String joining
assert [1, 2, 3].inject('counting: ') {
    str, item -> str + item                     // reduce operation
} == 'counting: 123'
assert [1, 2, 3].inject(0) { count, item ->
    count + item
} == 6

下面是在 Groovy 代码中惯用的在集合中查找最大值与最小值的方法:

def list = [9, 4, 2, 10, 5]
assert list.max() == 10
assert list.min() == 2

// we can also compare single characters, as anything comparable
assert ['x', 'y', 'a', 'z'].min() == 'a'

// we can use a closure to specify the sorting behaviour
def list2 = ['abc', 'z', 'xyzuvw', 'Hello', '321']
assert list2.max { it.size() } == 'xyzuvw'
assert list2.min { it.size() } == 'z'

除了使用闭包,也可以使用 Comparator 定义比较方法:

Comparator mc = { a, b -> a == b ? 0 : (a < b ? -1 : 1) }

def list = [7, 4, 9, -6, -1, 11, 2, 3, -9, 5, -13]
assert list.max(mc) == 11
assert list.min(mc) == -13

Comparator mc2 = { a, b -> a == b ? 0 : (Math.abs(a) < Math.abs(b)) ? -1 : 1 }


assert list.max(mc2) == -13
assert list.min(mc2) == -1

assert list.max { a, b -> a.equals(b) ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 } == -13
assert list.min { a, b -> a.equals(b) ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 } == -1
.添加/删除列表元素

我们可以使用 [] 声明一个空列表并且使用 << 向列表中追加元素:

def list = []
assert list.empty

list << 5
assert list.size() == 1

list << 7 << 'i' << 11
assert list == [5, 7, 'i', 11]

list << ['m', 'o']
assert list == [5, 7, 'i', 11, ['m', 'o']]

//first item in chain of << is target list
assert ([1, 2] << 3 << [4, 5] << 6) == [1, 2, 3, [4, 5], 6]

//using leftShift is equivalent to using <<
assert ([1, 2, 3] << 4) == ([1, 2, 3].leftShift(4))

你可以通过多种方式向列表中添加元素:

assert [1, 2] + 3 + [4, 5] + 6 == [1, 2, 3, 4, 5, 6]
// equivalent to calling the `plus` method
assert [1, 2].plus(3).plus([4, 5]).plus(6) == [1, 2, 3, 4, 5, 6]

def a = [1, 2, 3]
a += 4      // creates a new list and assigns it to `a`
a += [5, 6]
assert a == [1, 2, 3, 4, 5, 6]

assert [1, *[222, 333], 456] == [1, 222, 333, 456]
assert [*[1, 2, 3]] == [1, 2, 3]
assert [1, [2, 3, [4, 5], 6], 7, [8, 9]].flatten() == [1, 2, 3, 4, 5, 6, 7, 8, 9]

def list = [1, 2]
list.add(3)
list.addAll([5, 4])
assert list == [1, 2, 3, 5, 4]

list = [1, 2]
list.add(1, 3) // add 3 just before index 1
assert list == [1, 3, 2]

list.addAll(2, [5, 4]) //add [5,4] just before index 2
assert list == [1, 3, 5, 4, 2]

list = ['a', 'b', 'z', 'e', 'u', 'v', 'g']
list[8] = 'x' // the [] operator is growing the list as needed
// nulls inserted if required
assert list == ['a', 'b', 'z', 'e', 'u', 'v', 'g', null, 'x']

相比较 << , + 操作符将创建新的 list 对象,这样的将会带来性能问题,通常会弱化这样的操作:

Groovy 开发包 也提供通过列表中的值来删除列表的方法:

assert ['a','b','c','b','b'] - 'c' == ['a','b','b','b']
assert ['a','b','c','b','b'] - 'b' == ['a','c']
assert ['a','b','c','b','b'] - ['b','c'] == ['a']

def list = [1,2,3,4,3,2,1]
list -= 3           // creates a new list by removing `3` from the original one
assert list == [1,2,4,2,1]
assert ( list -= [2,4] ) == [1,1]

也可以通过列表索引删除元素:

def list = [1,2,3,4,5,6,2,2,1]
assert list.remove(2) == 3          // remove the third element, and return it
assert list == [1,2,4,5,6,2,2,1]

如需要删除列表中与指定值相同的第一个元素,而不是所有,可以调用 remove 方法 :

def list= ['a','b','c','b','b']
assert list.remove('c')             // remove 'c', and return true because element removed
assert list.remove('b')             // remove first 'b', and return true because element removed

assert ! list.remove('z')           // return false because no elements removed
assert list == ['a','b','b']

删除列表中的所有元素,可以使用 clear 方法:

def list= ['a',2,'c',4]
list.clear()
assert list == []
.Set operations

Groovy 开发包 中有方法可以很方便的操作集合。

assert 'a' in ['a','b','c']             // returns true if an element belongs to the list
assert ['a','b','c'].contains('a')      // equivalent to the `contains` method in Java
assert [1,3,4].containsAll([1,4])       // `containsAll` will check that all elements are found

assert [1,2,3,3,3,3,4,5].count(3) == 4  // count the number of elements which have some value
assert [1,2,3,3,3,3,4,5].count {
    it%2==0                             // count the number of elements which match the predicate
} == 2

assert [1,2,4,6,8,10,12].intersect([1,3,6,9,12]) == [1,6,12]  //  相交

assert [1,2,3].disjoint( [4,6,9] )  // 不相交
assert ![1,2,3].disjoint( [2,4,6] )
.排序

使用集合类型,通常都需要进行排序。Groovy 中提供了多种列表的排序方式,包括闭包比较方式,如下面例子:

assert [6, 3, 9, 2, 7, 1, 5].sort() == [1, 2, 3, 5, 6, 7, 9]

def list = ['abc', 'z', 'xyzuvw', 'Hello', '321']
assert list.sort {
    it.size()
} == ['z', 'abc', '321', 'Hello', 'xyzuvw']

def list2 = [7, 4, -6, -1, 11, 2, 3, -9, 5, -13]
assert list2.sort { a, b -> a == b ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 } ==
        [-1, 2, 3, 4, 5, -6, 7, -9, 11, -13]

Comparator mc = { a, b -> a == b ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 }

// JDK 8+ only
// list2.sort(mc)
// assert list2 == [-1, 2, 3, 4, 5, -6, 7, -9, 11, -13]

def list3 = [6, -3, 9, 2, -7, 1, 5]

Collections.sort(list3)
assert list3 == [-7, -3, 1, 2, 5, 6, 9]

Collections.sort(list3, mc)
assert list3 == [1, 2, -3, 5, 6, -7, 9]
.复制元素

Groovy 开发包中也提供了操作符重载复制元素的方法:

assert [1, 2, 3] * 3 == [1, 2, 3, 1, 2, 3, 1, 2, 3]
assert [1, 2, 3].multiply(2) == [1, 2, 3, 1, 2, 3]
assert Collections.nCopies(3, 'b') == ['b', 'b', 'b']

// nCopies from the JDK has different semantics than multiply for lists
assert Collections.nCopies(2, [1, 2]) == [[1, 2], [1, 2]] //not [1,2,1,2]

Maps

Map literals

在 Groovy 中可以通过语法 [:] 创建 map:

def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.get('name') == 'Gromit'
assert map.get('id') == 1234
assert map['name'] == 'Gromit'
assert map['id'] == 1234
assert map instanceof java.util.Map

def emptyMap = [:]
assert emptyMap.size() == 0
emptyMap.put("foo", 5)
assert emptyMap.size() == 1
assert emptyMap.get("foo") == 5

Map 的 key 默认为 String 类型: [a:1] 与 [‘a’:1] 等价。 Map keys are strings by default: [a:1] is equivalent to [‘a’:1]。 如果变量名为 a ,你想用 a 的值作为 map 的 key, 这样就会出现混淆。对于这种情况,就需要添加括号,向下面代码中描述:

def a = 'Bob'
def ages = [a: 43]
assert ages['Bob'] == null // `Bob` is not found
assert ages['a'] == 43     // because `a` is a literal!

ages = [(a): 43]            // now we escape `a` by using parenthesis
assert ages['Bob'] == 43   // and the value is found!

Map 中可以通过 clone 方法获取一个新的拷贝:

def map = [
        simple : 123,
        complex: [a: 1, b: 2]
]
def map2 = map.clone()
assert map2.get('simple') == map.get('simple')
assert map2.get('complex') == map.get('complex')
map2.get('complex').put('c', 3)
assert map.get('complex').get('c') == 3

上面例子是关于 map 的浅拷贝。

Map property notation

Maps 可以像 beans 那样通过属性访问符,获取或设置属性值,只要 key 为 map 中有效字符:

def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.name == 'Gromit'     // can be used instead of map.get('Gromit')
assert map.id == 1234

def emptyMap = [:]
assert emptyMap.size() == 0
emptyMap.foo = 5
assert emptyMap.size() == 1
assert emptyMap.foo == 5

map.foo 会在 map 中查找 key 为 foo 的值。这意味着 foo.class 将会返回 null ,因为在 map 中不能以 class 作为 key。如果需要得到这个 key 的 class , 你需要使用 getClass() :

def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.class == null
assert map.get('class') == null
assert map.getClass() == LinkedHashMap // this is probably what you want

map = [1      : 'a',
       (true) : 'p',
       (false): 'q',
       (null) : 'x',
       'null' : 'z']
assert map.containsKey(1) // 1 is not an identifier so used as is
assert map.true == null
assert map.false == null
assert map.get(true) == 'p'
assert map.get(false) == 'q'
assert map.null == 'z'
assert map.get(null) == 'x'
maps 上的迭代

在 Groovy 开发包中,惯用的迭代方法为 eacheachWithIndex。 值得注意的是,map 创建是有序的,如果在 map 上使用的迭代,其返回的实体顺序与加入时顺序一致。

def map = [
        Bob  : 42,
        Alice: 54,
        Max  : 33
]

// `entry` is a map entry
map.each { entry ->
    println "Name: $entry.key Age: $entry.value"
}

// `entry` is a map entry, `i` the index in the map
map.eachWithIndex { entry, i ->
    println "$i - Name: $entry.key Age: $entry.value"
}

// Alternatively you can use key and value directly
map.each { key, value ->
    println "Name: $key Age: $value"
}

// Key, value and i as the index in the map
map.eachWithIndex { key, value, i ->
    println "$i - Name: $key Age: $value"
}
操作 Map
.添加/删除元素

向 map 中添加元素,可以使用 put , 下标操作符或 putAll 方法:

def defaults = [1: 'a', 2: 'b', 3: 'c', 4: 'd']
def overrides = [2: 'z', 5: 'x', 13: 'x']

def result = new LinkedHashMap(defaults)
result.put(15, 't')
result[17] = 'u'
result.putAll(overrides)
assert result == [1: 'a', 2: 'z', 3: 'c', 4: 'd', 5: 'x', 13: 'x', 15: 't', 17: 'u']

调用 clear 方法可以清除 map 中的所有元素:

def m = [1:'a', 2:'b']
assert m.get(1) == 'a'
m.clear()
assert m == [:]

Maps 生成使用 object 的 equals 和 hashcode 方法。这意味着你绝不可使用 hash code 会变化的对象作为 key, 否则你将无法获取对应的值。 这里需要提到是,你绝不可使用 GString 作为 map 的 key,因为 GString 的 hash code 与 String 类型的 hash code 不一致:

def key = 'some key'
def map = [:]
def gstringKey = "${key.toUpperCase()}"
map.put(gstringKey,'value')
assert map.get('SOME KEY') == null
.Keys, values and entries
def map = [1:'a', 2:'b', 3:'c']

def entries = map.entrySet()
entries.each { entry ->
  assert entry.key in [1,2,3]
  assert entry.value in ['a','b','c']
}

def keys = map.keySet()
assert keys == [1,2,3] as Set

Mutating values returned by the view (be it a map entry, a key or a value) is highly discouraged because success of the operation directly depends on the type of the map being manipulated. In particular, Groovy relies on collections from the JDK that in general make no guarantee that a collection can safely be manipulated through keySet, entrySet, or values.

.过滤/查询

Groovy 开发包中包括过滤,查询,收集方法与 http://www.groovy-lang.org/groovy-dev-kit.html#List-Filtering[list] 中相似: The Groovy development kit contains filtering, searching and collecting methods similar to those found for lists:

def people = [
    1: [name:'Bob', age: 32, gender: 'M'],
    2: [name:'Johnny', age: 36, gender: 'M'],
    3: [name:'Claire', age: 21, gender: 'F'],
    4: [name:'Amy', age: 54, gender:'F']
]

def bob = people.find { it.value.name == 'Bob' } // find a single entry
def females = people.findAll { it.value.gender == 'F' }

// both return entries, but you can use collect to retrieve the ages for example
def ageOfBob = bob.value.age
def agesOfFemales = females.collect {
    it.value.age
}

assert ageOfBob == 32
assert agesOfFemales == [21,54]

// but you could also use a key/pair value as the parameters of the closures
def agesOfMales = people.findAll { id, person ->
    person.gender == 'M'
}.collect { id, person ->
    person.age
}
assert agesOfMales == [32, 36]

// `every` returns true if all entries match the predicate
assert people.every { id, person ->
    person.age > 18
}

// `any` returns true if any entry matches the predicate

assert people.any { id, person ->
    person.age == 54
}
.分组
assert ['a', 7, 'b', [2, 3]].groupBy {
    it.class
} == [(String)   : ['a', 'b'],
      (Integer)  : [7],
      (ArrayList): [[2, 3]]
]

assert [
        [name: 'Clark', city: 'London'], [name: 'Sharma', city: 'London'],
        [name: 'Maradona', city: 'LA'], [name: 'Zhang', city: 'HK'],
        [name: 'Ali', city: 'HK'], [name: 'Liu', city: 'HK'],
].groupBy { it.city } == [
        London: [[name: 'Clark', city: 'London'],
                 [name: 'Sharma', city: 'London']],
        LA    : [[name: 'Maradona', city: 'LA']],
        HK    : [[name: 'Zhang', city: 'HK'],
                 [name: 'Ali', city: 'HK'],
                 [name: 'Liu', city: 'HK']],
]
Ranges

Ranges 允许你创建连续值的列表对象。Ranges 可以像 List 一样使用,Range 继承 java.util.ListRanges 使用 .. 定义闭区间。 Ranges 使用 ..< 定义半开区间,其包含第一个值,不包含最后一个值。

// an inclusive range
def range = 5..8
assert range.size() == 4
assert range.get(2) == 7
assert range[2] == 7
assert range instanceof java.util.List
assert range.contains(5)
assert range.contains(8)

// lets use a half-open range
range = 5..<8
assert range.size() == 3
assert range.get(2) == 7
assert range[2] == 7
assert range instanceof java.util.List
assert range.contains(5)
assert !range.contains(8)

//get the end points of the range without using indexes
range = 1..10
assert range.from == 1
assert range.to == 10

int 型 ranges 实现十分有效,创建一个轻量的 java 对象可以包括启始值与终止值。

Ranges 可以在任何实现 java.lang.Comparable 接口的 java 对象上使用,可以通过 next() 和 previous() 方法分别返回范围中,当前数据前后的值。例如,你可以创建一个 String 有序范围:

// an inclusive range
def range = 'a'..'d'
assert range.size() == 4
assert range.get(2) == 'c'
assert range[2] == 'c'
assert range instanceof java.util.List
assert range.contains('a')
assert range.contains('d')
assert !range.contains('e')

你可使用 for 循环:

for (i in 1..10) {
    println "Hello ${i}"
}

也可以使用 Groovy 中比较惯用的方式,使用 each 方法进行迭代操作:

(1..10).each { i ->
    println "Hello ${i}"
}

Ranges 可以在 switch 中使用:

switch (years) {
    case 1..10: interestRate = 0.076; break;
    case 11..25: interestRate = 0.052; break;
    default: interestRate = 0.037;
}
集合语法增强

属性符号对 lists 和 maps 的支持, Groovy 提供了语法糖用于处理嵌套的集合对象,可参考下面例子:

def listOfMaps = [['a': 11, 'b': 12], ['a': 21, 'b': 22]]
assert listOfMaps.a == [11, 21] //GPath notation
assert listOfMaps*.a == [11, 21] //spread dot notation

listOfMaps = [['a': 11, 'b': 12], ['a': 21, 'b': 22], null]
assert listOfMaps*.a == [11, 21, null] // caters for null values
assert listOfMaps*.a == listOfMaps.collect { it?.a } //equivalent notation
// But this will only collect non-null values
assert listOfMaps.a == [11,21]

Spread operator 被用于集合内部的集合操作。这种语法糖可以避免使用 putAll ,有利于在单行内代码实现。

assert [ 'z': 900,
         *: ['a': 100, 'b': 200], 'a': 300] == ['a': 300, 'b': 200, 'z': 900]
//spread map notation in map definition
assert [*: [3: 3, *: [5: 5]], 7: 7] == [3: 3, 5: 5, 7: 7]

def f = { [1: 'u', 2: 'v', 3: 'w'] }
assert [*: f(), 10: 'zz'] == [1: 'u', 10: 'zz', 2: 'v', 3: 'w']
//spread map notation in function arguments
f = { map -> map.c }
assert f(*: ['a': 10, 'b': 20, 'c': 30], 'e': 50) == 30

f = { m, i, j, k -> [m, i, j, k] }
//using spread map notation with mixed unnamed and named arguments
assert f('e': 100, *[4, 5], *: ['a': 10, 'b': 20, 'c': 30], 6) ==
        [["e": 100, "b": 20, "c": 30, "a": 10], 4, 5, 6]

*. 操作符用于集合上所有元素调用方法或属性:

assert [1, 3, 5] == ['a', 'few', 'words']*.size()

class Person {
    String name
    int age
}
def persons = [new Person(name:'Hugo', age:17), new Person(name:'Sandra',age:19)]
assert [17, 19] == persons*.age

你可以使用下标表达式在 lists,maps,arrays 上进行索引定位。 字符串被看作一种特殊的集合:

def text = 'nice cheese gromit!'
def x = text[2]

assert x == 'c'
assert x.class == String

def sub = text[5..10]
assert sub == 'cheese'

def list = [10, 11, 12, 13]
def answer = list[2,3]
assert answer == [12,13]

注意你可以使用 ranges 提取集合的一部分:

list = 100..200
sub = list[1, 3, 20..25, 33]
assert sub == [101, 103, 120, 121, 122, 123, 124, 125, 133]

下标操作符可以用于更新集合(集合类型为可变集合):

list = ['a','x','x','d']
list[1..2] = ['b','c']
assert list == ['a','b','c','d']

下标可以使用负数,可以方便从集合末尾开发提取: 你可以使用负数下标从末尾计算 List ,array , String 等

text = "nice cheese gromit!"
x = text[-1]
assert x == "!"

def name = text[-7..-2]
assert name == "gromit"

你可以使用逆向范围,将集合结果进行反转。

text = "nice cheese gromit!"
name = text[3..1]
assert name == "eci"
增强集合方法

对于 lists , mapsranges , Groovy 提供了大量扩展方法用于过滤,收集,分组以及计算等等,其可以在自身上执行并且更容易使用迭代处理。

具体的内容可以阅读 Groovy 开发包 API 文档:

  • methods added to Iterable can be found here
  • methods added to Iterator can be found here
  • methods added to Collection can be found here
  • methods added to List can be found here
  • methods added to Map can be found here

Handy utilities (工具集)

ConfigSlurper

ConfigSlurper 是用于读取从 Groovy script 中读取配置信息。类似于 Java 中的 *.properties 文件, ConfigSlurper 中可以使用点符号。它也可以在闭包范围内定义内容以及任何的对象类型。

def config = new ConfigSlurper().parse('''
    app.date = new Date()                       //<1>
    app.age  = 42
    app {                                                       //<2>
        name = "Test${42}"
    }
''')

assert config.app.date instanceof Date
assert config.app.age == 42
assert config.app.name == 'Test42'

<1> 使用点符号

<2> 使用闭包替代点符号

从上面的例子可以看到,parse 返回 groovy.util.ConfigObject 实例。 ConfigObject 实现了 java.util.Map 返回配置值或 ConfigObject 实例,但绝不会为 null.

def config = new ConfigSlurper().parse('''
    app.date = new Date()
    app.age  = 42
    app.name = "Test${42}"
''')

assert config.test != null      //<1>

<1> config.test 并没有定义,但是当其被调用时返回 ConfigObject 实例。

当配置中的变量名称中包含点时,可以使用单引号或双引号标注。

def config = new ConfigSlurper().parse('''
    app."person.age"  = 42
''')

assert config.app."person.age" == 42

ConfigSlurper 可以支持按环境配置。environments 中包含各个环境组成,被用于闭包中。 让我们创建一个指定开发环境配置值。可以使用 ConfigSlurper(String) 构造方法指定目标环境。

def config = new ConfigSlurper('development').parse('''
  environments {
       development {
           app.port = 8080
       }

       test {
           app.port = 8082
       }

       production {
           app.port = 80
       }
  }
''')

assert config.app.port == 8080

ConfigSlurper 中的 environments 并没有强制指定特殊的环境名称。其可以由代码自行定义。 environments 是内置方法,registerConditionalBlock 方法可以用来注册 environments 以外的,其他的方法名称。

def slurper = new ConfigSlurper()
slurper.registerConditionalBlock('myProject', 'developers')             // <1>

def config = slurper.parse('''
  sendMail = true

  myProject {
       developers {
           sendMail = false
       }
  }
''')

assert !config.sendMail

<1> 向 ConfigSlurper 注册一个新的块

为与 Java 集成, toProperties 方法用于将 ConfigObject 转化为 java.util.Properties 对象,可以存储为 *.properties 文本文件。需要注意,配置的值将被转换为字符串类型,当将它们加入到 Properties 中时。

def config = new ConfigSlurper().parse('''
    app.date = new Date()
    app.age  = 42
    app {
        name = "Test${42}"
    }
''')

def properties = config.toProperties()

assert properties."app.date" instanceof String
assert properties."app.age" == '42'
assert properties."app.name" == 'Test42'

Expando 类用于创建动态扩展对象。

每个 Expando 实例均为独立,动态创建的,可以在运行时动态扩展属性。

def expando = new Expando()
expando.name = 'John'

assert expando.name == 'John'

当其动态属性注册为闭包代码块,一旦调用其将按照方法调用执行。

def expando = new Expando()
expando.toString = { -> 'John' }
expando.say = { String s -> "John says: ${s}" }

assert expando as String == 'John'
assert expando.say('Hi') == 'John says: Hi'
监听 list, map and set

Groovy 中自带 lists, map, sets 的观察器。这些集合对象,在添加,删除,或修改元素时,均为触发 java.beans.PropertyChangeEvent 事件。

需要注意的是,其不仅仅是发送信号,还会将留存属性值的历史变化。

根据不同的操作,这些集合将触发特定的 PropertyChangeEvent 类型。例如,添加一个元素到集合中,将触发 ObservableList.ElementAddedEvent 事件。

def event                                       // <1>
def listener = {
    if (it instanceof ObservableList.ElementEvent)  {           //<2>
        event = it
    }
} as PropertyChangeListener


def observable = [1, 2, 3] as ObservableList                            //<3>
observable.addPropertyChangeListener(listener)                          //<4>

observable.add 42                                                                                       //<5>

assert event instanceof ObservableList.ElementAddedEvent

def elementAddedEvent = event as ObservableList.ElementAddedEvent
assert elementAddedEvent.changeType == ObservableList.ChangeType.ADDED
assert elementAddedEvent.index == 3
assert elementAddedEvent.oldValue == null
assert elementAddedEvent.newValue == 42

<1> 声明一个 PropertyChangeEventListener 用于监听触发事件

<2> ObservableList.ElementEvent 对于监听器事件

<3> 注册监听器

<4> 创建一个可观察列表

<5> 触发添加事件

请注意,添加一个元素会触发两个事件。第一个是 ObservableList.ElementAddedEvent ,第二个是 PropertyChangeEvent,其监听属性数量变化。 当多个元素被移除,调用 clear() 方法,会触发 ObservableList.ElementClearedEvent 类型事件,它将保留删除的所有元素。

def event
def listener = {
    if (it instanceof ObservableList.ElementEvent)  {
        event = it
    }
} as PropertyChangeListener


def observable = [1, 2, 3] as ObservableList
observable.addPropertyChangeListener(listener)

observable.clear()

assert event instanceof ObservableList.ElementClearedEvent

def elementClearedEvent = event as ObservableList.ElementClearedEvent
assert elementClearedEvent.values == [1, 2, 3]
assert observable.size() == 0

ObservableMap and ObservableSet 概念与本章节 ObservableList 一样。

运行时&编译时元编程 (TBD)

Groovy 提供两类元编程,分别为:运行时元编程与编译时元编程。第一种允许在运行时改变类模型,而第二种发生在编译时。其各有优劣之处,这一章节我们详细讲解。

运行时元编程

在运行时元编程中,我们在运行时拦截,注入甚至合成类和接口的方法。为了深入理解 Groovy MOP , 我们需要理解 Groovy 的对象及方法的处理方式。Groovy 中有三类对象:POJO,POGO 和 Groovy 拦截器。Groovy 中对于以上对象均能使用元编程,但使用方式会有差别。

  • POJO - 正规的 Java 对象,其类可以用Java或任何其他 JVM 语言编写。
  • POGO - Groovy 对象,其类使用 Groovy 编写。继承于 java.lang.Object 并且实现 groovy.lang.GroovyObject 接口。
  • Groovy Interceptor - Groovy 对象,实现 groovy.lang.GroovyInterceptable 接口,具有方法拦截能力,我们将在 GroovyInterceptable 章节详细讲解。

For every method call Groovy checks whether the object is a POJO or a POGO. For POJOs, Groovy fetches it’s MetaClass from the groovy.lang.MetaClassRegistry and delegates method invocation to it. For POGOs, Groovy takes more steps, as illustrated in the following figure:

每个方法调用过程中, Groovy 检查当前对象是 POJO 还是 POGO。对于 POJOs , Groovy 将从 groovy.lang.MetaClassRegistry 中取出其 MetaClass 并使用代理方式进行方法调用。对于 POGOs,Groovy 将执行更多步骤,如下图

Groovy interception mechanism
GroovyObject interface

groovy.lang.GroovyObject 作为 Groovy 中的主要接口,就像 Java 中的 Object 类。 GroovyObject 的在 groovy.lang.GroovyObjectSupport 中默认实现, 其主要负责将调用传递给 MetaClass 对象。 参考下面代码:

package groovy.lang;

public interface GroovyObject {

    Object invokeMethod(String name, Object args);

    Object getProperty(String propertyName);

    void setProperty(String propertyName, Object newValue);

    MetaClass getMetaClass();

    void setMetaClass(MetaClass metaClass);
}
invokeMethod

在运行时元编程模式下,当 Groovy 对象上调用的方法不存在时,将会调用 invokeMethod 方法。 下面的例子中,就是用到重载 invokeMethod 方法:

class SomeGroovyClass {

    def invokeMethod(String name, Object args) {
        return "called invokeMethod $name $args"
    }

    def test() {
        return 'method exists'
    }
}

def someGroovyClass = new SomeGroovyClass()

assert someGroovyClass.test() == 'method exists'
assert someGroovyClass.someMethod() == 'called invokeMethod someMethod []'
get/getProperty

对象上读取属性操作都将通过重载 getProperty 方法拦截,例如:

class SomeGroovyClass {

    def property1 = 'ha'
    def field2 = 'ho'
    def field4 = 'hu'

    def getField1() {
        return 'getHa'
    }

    def getProperty(String name) {
        if (name != 'field3')
            return metaClass.getProperty(this, name)                    // <1>
        else
            return 'field3'
    }
}

def someGroovyClass = new SomeGroovyClass()

assert someGroovyClass.field1 == 'getHa'
assert someGroovyClass.field2 == 'ho'
assert someGroovyClass.field3 == 'field3'
assert someGroovyClass.field4 == 'hu'

<1> Forwards the request to the getter for all properties except field3.

通过重载 setProperty 方法可以拦截属性修改操作:

class POGO {

    String property

    void setProperty(String name, Object value) {
        this.@"$name" = 'overriden'
    }
}

def pogo = new POGO()
pogo.property = 'a'

assert pogo.property == 'overriden'
get/setMetaClass

You can a access a objects metaClass or set your own MetaClass implementation for changing the default interception mechanism. For example you can write your own implementation of the MetaClass interface and assign to it to objects and accordingly change the interception mechanism:

通过访问对象的 metaClass 或 重新设置你自己的 MetaClass 实现,可以改变默认的拦截机制。 下面例子中,你可以通过自己实现一个 MetaClass 并赋值给其对象上,这样就可以改变拦截机制:

// getMetaclass
someObject.metaClass

// setMetaClass
someObject.metaClass = new OwnMetaClassImplementation()

GroovyInterceptable 章节中有更多实例用于参考。

get/setAttribute

This functionality is related to the MetaClass implementation. In the default implementation you can access fields without invoking their getters and setters. The examples below demonstrate this approach:

这一功能与 MetaClass 的具体实现有关。在默认的实现中,你可以无需调用 fields 上的 getters 和 setters 方法,就可以通过 get/setAttribute 访问控制 fields 。 下面例子中将展示这种特性:

class SomeGroovyClass {

    def field1 = 'ha'
    def field2 = 'ho'

    def getField1() {
        return 'getHa'
    }
}

def someGroovyClass = new SomeGroovyClass()

assert someGroovyClass.metaClass.getAttribute(someGroovyClass, 'field1') == 'ha'
assert someGroovyClass.metaClass.getAttribute(someGroovyClass, 'field2') == 'ho'
class POGO {

    private String field
    String property1

    void setProperty1(String property1) {
        this.property1 = "setProperty1"
    }
}

def pogo = new POGO()
pogo.metaClass.setAttribute(pogo, 'field', 'ha')
pogo.metaClass.setAttribute(pogo, 'property1', 'ho')

assert pogo.field == 'ha'
assert pogo.property1 == 'ho'
methodMissing

Groovy 支持 methodMissing 概念. 当方法调用失败,方法的名称或参数不正确,都将调用 methodMissing 方法。

class Foo {

   def methodMissing(String name, def args) {
        return "this is me"
   }
}

assert new Foo().someUnknownMethod(42l) == 'this is me'

通常情况下,使用 methodMissing ,其可以缓存相同方法及参数调用结果,并提供给下次调用使用。

For example consider dynamic finders in GORM. These are implemented in terms of methodMissing. The code resembles something like this: 例如在 GORM 中的动态查找,这里就实现了 methodMissing 。可以参考类似代码:

class GORM {

   def dynamicMethods = [...] // an array of dynamic methods that use regex

   def methodMissing(String name, args) {
       def method = dynamicMethods.find { it.match(name) }
       if(method) {
          GORM.metaClass."$name" = { Object[] varArgs ->
             method.invoke(delegate, name, varArgs)
          }
          return method.invoke(delegate,name, args)
       }
       else throw new MissingMethodException(name, delegate, args)
   }
}

这里需要注意,如果我们找到调用的方法,然后将其注册到 ExpandoMetaClass , 在下次再次调用此方法,将会更加高效。 这是使用 methodMissing 不会有调用 invokeMethod 的开销,在第二次调用此方法,就和原生方法一样。

propertyMissing

当访问的属性不存在时,会调用 propertyMissingpropertyMissing 方法只有一个字符串类型的入参,其参数为调用的属性名称。

class Foo {
   def propertyMissing(String name) { name }
}

assert new Foo().boo == 'boo'

在运行时,当无法找到属性对应的 getter 方法的情况下,才能调用 propertyMissing(String) 方法。

对于 setter 方法, 第二个 propertyMissing 方法定义增加了一个 value 参数:

class Foo {
   def storage = [:]
   def propertyMissing(String name, value) { storage[name] = value }
   def propertyMissing(String name) { storage[name] }
}

def f = new Foo()
f.foo = "bar"

assert f.foo == "bar"

相比较于 methodMissing , 这是最好方式在运行时动态注册属性并能提升整体的查找性能。

methodMissingpropertyMissing 处理的方法和属性,都可以通过 ExpandoMetaClass 添加注册。

GroovyInterceptable

groovy.lang.GroovyInterceptable 是继承于 GroovyObject 的关键接口,实现此接口的类上的方法调用都将经过运行 时方法路由机制拦截。

package groovy.lang;

public interface GroovyInterceptable extends GroovyObject {
}

当 Groovy 对象实现 GroovyInterceptable 接口,其对象上的任何方法调用都将通过 invokeMethod() 拦截器。 可以参考下面的例子:

class Interception implements GroovyInterceptable {

    def definedMethod() { }

    def invokeMethod(String name, Object args) {
        'invokedMethod'
    }
}

下面代码中会看到,无论被调用方式是否存在,返回值都是相同的:

class InterceptableTest extends GroovyTestCase {

    void testCheckInterception() {
        def interception = new Interception()

        assert interception.definedMethod() == 'invokedMethod'
        assert interception.someMethod() == 'invokedMethod'
    }
}

需要注意,我们不能使用像 println 这样的方法,是由于其已经被注入到所有 groovy 对象中,也将会被拦截。

If we want to intercept all methods call but do not want to implement the GroovyInterceptable interface we can implement invokeMethod() on an object’s MetaClass. This approach works for both POGOs and POJOs, as shown by this example: 如果你想拦截所有方法,但是有不想实现 GroovyInterceptable 接口,可以通过在对象的 MetaClass 方法上实现 invokeMethod 方法来达到同样的效果。 这种方法对于 POGOsPOJOs 都适用,看看下面的例子:

class InterceptionThroughMetaClassTest extends GroovyTestCase {

    void testPOJOMetaClassInterception() {
        String invoking = 'ha'
        invoking.metaClass.invokeMethod = { String name, Object args ->
            'invoked'
        }

        assert invoking.length() == 'invoked'
        assert invoking.someMethod() == 'invoked'
    }

    void testPOGOMetaClassInterception() {
        Entity entity = new Entity('Hello')
        entity.metaClass.invokeMethod = { String name, Object args ->
            'invoked'
        }

        assert entity.build(new Object()) == 'invoked'
        assert entity.someMethod() == 'invoked'
    }
}

更多有关 MetaClass 的内容可以查看对应`章节 <http://www.groovy-lang.org/metaprogramming.html#_metaclasses>`_ 。

Categories

当我们对于无法控制的类,需要增加附加方法时,分类这种方式非常有用。 对于这种特性,Groovy 的实现是借鉴于 Objective-C ,并称其为分类。

分类通过称为 category 类来实现。category 类中需要根据预定规则来定义扩展方法。

这里有一些分类用于扩展相应类的功能,这种方式可以使其在 Groovy 中发挥更强的作用:

Category 类在默认情况下并不开启。当使用 category 中定义的方法时,需要使用 GDK 中提供的 use 方法。

use(TimeCategory)  {
    println 1.minute.from.now           // <1>
    println 10.hours.ago

    def someDate = new Date()         // <2>
    println someDate - 3.months
}

<1> TimeCategory adds methods to Integer <2> TimeCategory adds methods to Date

use 方法第一个参数为具体定义 category 类,第二个参数为闭包代码块。 在闭包块中可以访问 category 类中的方法。 正如上面例子中看到的, JDK 中 java.lang.Integerjava.util.Date 类也可以通过分类中定义的方法来增强。

分类也可以进行一定的封装,像下面这样:

class JPACategory{
  // Let's enhance JPA EntityManager without getting into the JSR committee
  static void persistAll(EntityManager em , Object[] entities) { //add an interface to save all
    entities?.each { em.persist(it) }
  }
}

def transactionContext = {
  EntityManager em, Closure c ->
  def tx = em.transaction
  try {
    tx.begin()
    use(JPACategory) {
      c()
    }
    tx.commit()
  } catch (e) {
    tx.rollback()
  } finally {
    //cleanup your resource here
  }
}

// user code, they always forget to close resource in exception, some even forget to commit, let's not rely on them.
EntityManager em; //probably injected
transactionContext (em) {
 em.persistAll(obj1, obj2, obj3)
 // let's do some logics here to make the example sensible
 em.persistAll(obj2, obj4, obj6)
}

当我们看到 groovy.time.TimeCategory 中所定义的方法都声明为 static 。 这种声明方式是通过 category 方式在 use 代码块中调用扩展方法的必要条件。

public class TimeCategory {

public static Date plus(final Date date, final BaseDuration duration) {
    return duration.plus(date);
}

public static Date minus(final Date date, final BaseDuration duration) {
    final Calendar cal = Calendar.getInstance();

    cal.setTime(date);
    cal.add(Calendar.YEAR, -duration.getYears());
    cal.add(Calendar.MONTH, -duration.getMonths());
    cal.add(Calendar.DAY_OF_YEAR, -duration.getDays());
    cal.add(Calendar.HOUR_OF_DAY, -duration.getHours());
    cal.add(Calendar.MINUTE, -duration.getMinutes());
    cal.add(Calendar.SECOND, -duration.getSeconds());
    cal.add(Calendar.MILLISECOND, -duration.getMillis());

    return cal.getTime();
}

// ...

另一个必要条件是静态方法中的第一个参数类型需要与将要使用的类型一致。 其他参数为方法调用中的正常参数。

由于参数及静态方法的约定,分类中方法的定义与普通方法比较起来,显得不太直观。
Groovy 可以通过 @Category 注解的方式,将经过注解的类在编译期转化为 category 类型。
class Distance {
    def number
    String toString() { "${number}m" }
}

@Category(Number)
class NumberCategory {
    Distance getMeters() {
        new Distance(number: this)
    }
}

use (NumberCategory)  {
    assert 42.meters.toString() == '42m'
}

使用 @Category 的优势是在使用实例方法时,可以不需要将第一个参数声明为目标类型。 其目标类型的设置通过注解方式替代。

这里 编译时元编程 章节将介绍 @Category 的其他用法。

Metaclasses

待续 (TBD)

Custom metaclasses

待续 (TBD)

Delegating metaClass

待续

Magic package(Maksym Stavytskyi)

待续 (TBD)

Per instance metaclass

待续 (TBD)

ExpandoMetaClass

Groovy 中自带特殊的 MetaClass 称为 ExpandoMetaClass 。 特殊之处就在于,可以通过使用闭包方式动态的添加或改变方法,构造器,属性以及静态方法。

这些动态的处理,对于在 Test guide 章节中看到的 mokingstubbing 应用场景非常有用。

Groovy 对每个 java.lang.Class 都提供了一个 metaClass 特殊属性,其返回一个 ExpandoMetaClass 实例的引用。 在这实例上可以添加方法或修改已存在的方法内容。

接下来介绍 ExpandoMetaClass 在各种场景中的使用。

Methods

通过调用 metaClass 属性来获取 ExpandoMetaClass ,使用 <<= 操作符添加方法。 Note that the left shift operator is used to append a new method. 注意 << 用于添加新方法,如果这个方法已经存在,将会抛出异常。如果你想替换方法,可以使用 =

这些操作符向 metaClass 的不存在的属性传递 闭包代码块的实例。

class Book {
   String title
}

Book.metaClass.titleInUpperCase << {-> title.toUpperCase() }

def b = new Book(title:"The Stand")

assert "THE STAND" == b.titleInUpperCase()

上面例子中就是通过通过访问 metaClass 并使用 <<= 操作符, 赋值闭包代码块的方式向类中添加新增方法。 无参数方法可以使用 {-> ...} 方式添加。

Properties

ExpandoMetaClass 支持两种方式添加,重载属性。

第一种,在 metaClass 上直接声明一个属性并赋值:

class Book {
   String title
}

Book.metaClass.author = "Stephen King"
def b = new Book()

assert "Stephen King" == b.author

另一种方式,添加属性对应的 getter 或 setter 方法:

class Book {
 String title
}
Book.metaClass.getAuthor << {-> "Stephen King" }

def b = new Book()

assert "Stephen King" == b.author

上面代码中属性通过闭包指定,并且为只读。 这里可以添加 setter 方法来存储属性值,以便后续使用。可以看看下面代码:

class Book {
  String title
}

def properties = Collections.synchronizedMap([:])

Book.metaClass.setAuthor = { String value ->
   properties[System.identityHashCode(delegate) + "author"] = value
}
Book.metaClass.getAuthor = {->
   properties[System.identityHashCode(delegate) + "author"]
}

例如在 Servlet 容器中将当前执行的 request 中的值存储到 request attributes 中。 Grails 中也有相同的应用。

Constructors

构造器添加使用特殊属性 constructor 。 同样适用 <<= 来传递闭包代码块。在代码执行时,闭包中定义的参数列表就为构造器的参数列表。

class Book {
    String title
}
Book.metaClass.constructor << { String title -> new Book(title:title) }

def book = new Book('Groovy in Action - 2nd Edition')
assert book.title == 'Groovy in Action - 2nd Edition'

需要当心的是在添加构造器时,会比较容易出现栈溢出的问题。

Static Methods

静态方法的加入也使用同样的技术,但需要在方法名称之前添加 static 限定符。

class Book {
   String title
}

Book.metaClass.static.create << { String title -> new Book(title:title) }

def b = Book.create("The Stand")
Borrowing Methods

ExpandoMetaClass 中可以使用方法指针语法,向其他类中借取方法。

class Person {
    String name
}
class MortgageLender {
   def borrowMoney() {
      "buy house"
   }
}

def lender = new MortgageLender()

Person.metaClass.buyHouse = lender.&borrowMoney

def p = new Person()

assert "buy house" == p.buyHouse()
Dynamic Method Names

Groovy 中可以使用 String 作为属性名称,同样可以允许你在运行时动态的创建方法或属性名称。 使用动态命名方法,直接使用了语言中引用属性名称作为字符串的特性。

class Person {
   String name = "Fred"
}

def methodName = "Bob"

Person.metaClass."changeNameTo${methodName}" = {-> delegate.name = "Bob" }

def p = new Person()

assert "Fred" == p.name

p.changeNameToBob()

assert "Bob" == p.name

静态方法及属性同样适用这种概念。

Grails 中可以发现动态方法命名的应用。 动态编码器这一概念,也是通过使用动态方法命名来实现。

HTMLCodec Class

class HTMLCodec {
    static encode = { theTarget ->
        HtmlUtils.htmlEscape(theTarget.toString())
    }

    static decode = { theTarget ->
        HtmlUtils.htmlUnescape(theTarget.toString())
    }
}

上面例子是一个编码器的实现。 Grails 中各种编码器都定义在各自独立的类中。 在运行环境的 classpath 中将有多个编译器类。 应用启动时将 encodeXXXdecodeXXX 方法添加到特定的 meta-classes 中,其中 XXX 就是编码器类名 的第一部分(例如: encodeHTML)。 这种处理机制将在下面的伪代码中展示:

 def codecs = classes.findAll { it.name.endsWith('Codec') }

codecs.each { codec ->
    Object.metaClass."encodeAs${codec.name-'Codec'}" = { codec.newInstance().encode(delegate) }
    Object.metaClass."decodeFrom${codec.name-'Codec'}" = { codec.newInstance().decode(delegate) }
}


def html = '<html><body>hello</body></html>'

assert '<html><body>hello</body></html>' == html.encodeAsHTML()
Runtime Discovery

在方法执行时,了解存在的其他方法或属性是十分有用的。 当前版本中,ExpandoMetaClass 提供一下方法用于此法:

  • getMetaMethod
  • hasMetaMethod
  • getMetaProperty
  • hasMetaProperty

为什么不使用放射? Groovy 中的方法分为原生方法以及运行时动态方法。 这里它们通常表示为元方法(MetaMethods)。元方法可以告诉你在运行时有效的方法。

当在重载 invokeMethod , getPropertysetProperty 时这将会非常有用。

GroovyObject Methods

ExpandoMetaClass 的另一个特性是其允许重载 invokeMethod , getPropertysetProperty 方法,这些方法都可以在 groovy.lang.GroovyObject 中 找到。

这里将展示如何重载 invokeMethod 方法:

class Stuff {
   def invokeMe() { "foo" }
}

Stuff.metaClass.invokeMethod = { String name, args ->
   def metaMethod = Stuff.metaClass.getMetaMethod(name, args)
   def result
   if(metaMethod) result = metaMethod.invoke(delegate,args)
   else {
      result = "bar"
   }
   result
}

def stf = new Stuff()

assert "foo" == stf.invokeMe()
assert "bar" == stf.doStuff()

闭包代码中的第一部分用于根据给定的方法名称及参数查找对应方法。 如果方法找到,就可以对方法进行代理处理。如果没有找到,方法返回默认值。

元方法是存在于 MetaClass 上的方法,无论是在运行时,还是编译时添加。 对于 setPropertygetProperty 同样适用。

class Person {
   String name = "Fred"
}

Person.metaClass.getProperty = { String name ->
   def metaProperty = Person.metaClass.getMetaProperty(name)
   def result
   if(metaProperty) result = metaProperty.getProperty(delegate)
   else {
      result = "Flintstone"
   }
   result
}

def p = new Person()

assert "Fred" == p.name
assert "Flintstone" == p.other

The important thing to note here is that instead of a MetaMethod a MetaProperty instance is looked up. If that exists the getProperty method of the MetaProperty is called, passing the delegate.

Overriding Static invokeMethod

ExpandoMetaClass 通过特殊的 invokeMethod 语法来重载静态方法。

class Stuff {
   static invokeMe() { "foo" }
}

Stuff.metaClass.'static'.invokeMethod = { String name, args ->
   def metaMethod = Stuff.metaClass.getStaticMetaMethod(name, args)
   def result
   if(metaMethod) result = metaMethod.invoke(delegate,args)
   else {
      result = "bar"
   }
   result
}

assert "foo" == Stuff.invokeMe()
assert "bar" == Stuff.doStuff()

这里重载逻辑与前面看的重载实例方法是一样的。 唯一的区别是使用 metaClass.static 访问属性,调用 getStaticMethodName 来获取静态元方法实例。

Extending Interfaces

使用 ExpandoMetaClass 可以在接口上添加方法。 要做到这一点,在应用启动时,需要在全局范围调用 ExpandoMetaClass.enableGlobally() 方法。

List.metaClass.sizeDoubled = {-> delegate.size() * 2 }

def list = []

list << 1
list << 2

assert 4 == list.sizeDoubled()
Extension modules
Extending existing classes

An extension module allows you to add new methods to existing classes, including classes which are precompiled, like classes from the JDK. Those new methods, unlike those defined through a metaclass or using a category, are available globally. For example, when you write:

扩展模块允许你在已有的类上添加新的方法,包括已经预编译的类,如 JDK 中的类。 这里新添加的方法,不同于通过 metaclass 或 category 定义的方法,其可以在全局使用。 例如:

Standard extension method

def file = new File(...)
def contents = file.getText('utf-8')

getText 方法并不存在于 File 类中。然而在 Groovy 在能使用,是由于其定义在一个称为 ResourceGroovyMethods 特殊类中:

ResourceGroovyMethods.java

public static String getText(File file, String charset) throws IOException {
 return IOGroovyMethods.getText(newReader(file, charset));
}

你需要注意的是,这个扩展方法使用 static 定义在帮助类中(其他扩展方法都在这里定义)。 getText 第一个参数对应其接受对象,其他参数对应这个扩展方法上的参数。 这样就可以在 File 类上调用 getText 方法。

创建一个扩展模块过程非常简单:

  • 想上面编写一个扩展方法
  • 编写一个模块描述文件

然后你需要将扩展模块及描述文件配置到 classpath 中,这样在 Groovy 中就可以使用。 这意味着,你可以选择:

  • 直接将类与模块描述文件配置在 classpath
  • 或将其打包至 jar 中,方便能够重用

扩展模块可以添加两类方法:

  • 实例方法(类实例上调用的方法)
  • 静态方法(类自身调用方法)
Instance methods

在类上添加实例方法,需要创建新的扩展类。 例如,你想在 Integer 上添加一个 maxRetries 方法,其方法接受一个闭包,在没有异常发生的情况下可以执行 n 次。 想实现上面的需求,你可以这样:

MaxRetriesExtension.groovy

class MaxRetriesExtension {                                       // <1>
    static void maxRetries(Integer self, Closure code) {          // <2>
        int retries = 0
        Throwable e
        while (retries<self) {
            try {
                code.call()
                break
            } catch (Throwable err) {
                e = err
                retries++
            }
        }
        if (retries==0 && e) {
            throw e
        }
    }
}

<1> The extension class <2> First argument of the static method corresponds to the receiver of the message, that is to say the extended instance

声明 你的扩展类之后,你可以这样调用:

int i=0
5.maxRetries {
    i++
}
assert i == 1
i=0
try {
    5.maxRetries {
        throw new RuntimeException("oops")
    }
} catch (RuntimeException e) {
    assert i == 5
}
Static methods

向类中也可以添加静态方法。 在这种情况下,静态方法需要定义在独立的文件中。 静态扩展方法和实例扩展方法不能在同一个类中定义。

StaticStringExtension.groovy

class StaticStringExtension {                               // <1>
    static String greeting(String self) {                   // <2>
        'Hello, world!'
    }
}

<1> The static extension class <2> First argument of the static method corresponds to the class being extended and is unused

这样你可以直接在 String 类上调用:

assert String.greeting() == 'Hello, world!'
模块描述文件 (Module descriptor)

在 Groovy 中加载扩展方法,需要声明其扩展类。 需要在 META-INF/services 目录下创建 org.codehaus.groovy.runtime.ExtensionModule 文件。

org.codehaus.groovy.runtime.ExtensionModule

moduleName=Test module for specifications
moduleVersion=1.0-test
extensionClasses=support.MaxRetriesExtension
staticExtensionClasses=support.StaticStringExtension

描述文件中需要 4 个属性:

  • moduleName : 模块名称
  • moduleVersion: 模块版本号。需要注意,版本号用于检查你不会加载相同模块的不同版本。
  • extensionClasses: 扩展类列表。你可以填写多个类,使用逗号分割。
  • staticExtensionClasses: 静态方法扩展类列表。你可以填写多个类,使用逗号分割。

请注意,并不要求在一个模块上同时定义静态方法与实例方法,你可以在一个模块中添加多个类。 你也可以在一个模块中扩展多个不同的类。甚至可以在一个扩展类中,对多个类型进行扩展,但是建议能针对扩展方法进行分类。

Extension modules and classpath

It’s worth noting that you can’t use an extension which is compiled at the same time as code using it. 需要注意的是,在编译后直接通过代码调用是无法使用此扩展的。 这意味着使用扩展,需要在代码调用其之前,将其编译后的 classes 配置的到 classpath

类型检查兼容性 (Compatibility with type checking)

与非类不同,扩展模块适用于类型检查:如果其配置在 classpath 中,类型检查器可以检查到扩展方法。也同样适用于静态编译。

编译时元编程 (Compile-time metaprogramming)

Groovy 中的编译时元编程允许在编译时生成代码。 这种转换是在程序的修改抽象语法树(AST),在 Groovy 中我们称其为抽象语法树转换(AST transformations)。 抽象语法树转换允许你在编译过程中修改 AST 并继续执行编译过程来生成字节码。 相比较于运行时元编程,其优点在于,任何变化在 class 文件中都是可见的(字节码中可见)。 在字节码中可见是很重要的,例如,如果你变化类的一部分(实现接口,扩展抽象类,等等)或者你需要 Java 来调用你的类。 抽象语法树转换可以在 class 中添加方法。在使用运行时元编程中,新方法只能在 Groovy 被发现调用。 如果相同方法使用编译时元编程,这个方法在 Java 中同样可见。 不仅如此,编译时元编程也带来了更好的性能表现(这个过程中无需初始化阶段)。

这一章节,我们开始讲解当前版本 Groovy 中的各种编译时转换。 在随后的章节中,我们也会介绍如何 实现抽象语法树 以及这种技术的缺点。

Available AST transformations

抽象语法树转换分为两种类别:

  • global AST transformations are applied transparently, globally, as soon as they are found on compile classpath
  • local AST transformations are applied by annotating the source code with markers. Unlike global AST transformations, local AST transformations may support parameters.

Groovy doesn’t ship with any global AST transformation, but you can find a list of local AST transformations available for you to use in your code here:

代码生成转换(Code generation transformations)

这种类别转换包括抽象语法树转换,能帮助去除代码样板。 这些代码都是些你必须写,但又不附带任何有用信息。 通过自动化生成代码模版,接下来需要写的代码将更加整洁,这样引入错误的可能性也会降低。

@groovy.transform.ToString

@ToString 抽象语法树转换,生成可读性更强 toString 方法。 例如,在 Person 上进行注解,将在其上自动生成 toString 方法:

import groovy.transform.ToString

@ToString
class Person {
    String firstName
    String lastName
}

这样定义,通过下面的断言测试,意味着生成的 toString 方法将属性值打印出:

def p = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p.toString() == 'Person(Jack, Nicholson)'

下面列表中总结了 @ToString 可以接收的参数:

(TBD)

@groovy.transform.EqualsAndHashCode

The @EqualsAndHashCode AST transformation aims at generating equals and hashCode methods for you. @EqualsAndHashCode 用于生成 equalshashcode 方法。 生成 hashcode 方法依照 Effective Java by Josh Bloch 中描述的做法:

import groovy.transform.EqualsAndHashCode

@EqualsAndHashCode
class Person {
    String firstName
    String lastName
}

def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
def p2 = new Person(firstName: 'Jack', lastName: 'Nicholson')

assert p1==p2
assert p1.hashCode() == p2.hashCode()

这里有一些选项用于改变 @EqualsAndHashCode 的行为:

(TBD)
@groovy.transform.TupleConstructor

The @TupleConstructor annotation aims at eliminating boilerplate code by generating constructors for you. A tuple constructor is created for each property, with default values (using the Java default values). For example, the following code will generate 3 constructors: @TupleConstructor 注解通过生成构造函数消除样板代码。使用其属性创建元构造函数,如果不填写则使用默认值。 例如,下面代码中生成的 3 个构造函数:

import groovy.transform.TupleConstructor

@TupleConstructor
class Person {
    String firstName
    String lastName
}

// traditional map-style constructor
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
// generated tuple constructor
def p2 = new Person('Jack', 'Nicholson')
// generated tuple constructor with default value for second property
def p3 = new Person('Jack')

The first constructor is a no-arg constructor which allows the traditional map-style construction. 第一个构造函数是一个无参数构造函数,其使用习惯的 map-style 构造。 It is worth noting that if the first property (or field) has type LinkedHashMap or if there is a single Map, AbstractMap or HashMap property (or field), then the map-style mapping is not available. 这里值得一提的是,当第一个参数是 LinkedHashMap 类型,或只有唯一一个 MapAbstractMapHashMap 参数,那么 map-style 参数设置方式将会无效。

这些构造函数生成,按照其参数定义顺序。根据其属性数量, Groovy 将生成相应的构造函数。

@TupleConstructor 接受多种配置选择:

(TBD)

@groovy.transform.Canonical

@Canonical 注解组合了 @ToString , @EqualsAndHashCode@TupleConstructor 的功能。

import groovy.transform.Canonical

@Canonical
class Person {
    String firstName
    String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack, Nicholson)' // Effect of @ToString

def p2 = new Person('Jack','Nicholson') // Effect of @TupleConstructor
assert p2.toString() == 'Person(Jack, Nicholson)'

assert p1==p2 // Effect of @EqualsAndHashCode
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode

使用 @Immutable 可以生成一个不可变类型。

@Canonical 接受多种配置选择:

(TBD)

@groovy.transform.InheritConstructors

@InheritConstructor 用于生成超类的构造方法。在重载异常类时将特别有用:

import groovy.transform.InheritConstructors

@InheritConstructors
class CustomException extends Exception {}

// all those are generated constructors
new CustomException()
new CustomException("A custom message")
new CustomException("A custom message", new RuntimeException())
new CustomException(new RuntimeException())

// Java 7 only
// new CustomException("A custom message", new RuntimeException(), false, true)

@InheritConstructor 接受多种配置选择:

(TBD)

@groovy.lang.Category

@Category 用于简化 Groovy 分类的使用。原始的分类使用方式:

class TripleCategory {
    public static Integer triple(Integer self) {
        3*self
    }
}
use (TripleCategory) {
    assert 9 == 3.triple()
}

@Category 转换,让你可以使用实例类型替代静态类型。 这种方式移除方法的中用于指向接收对象的第一个参数。例如:

@Category(Integer)
class TripleCategory {
    public Integer triple() { 3*this }
}
use (TripleCategory) {
    assert 9 == 3.triple()
}

Note that the mixed in class can be referenced using this instead. It’s also worth noting that using instance fields in a category class is inherently unsafe: categories are not stateful (like traits).

@groovy.transform.IndexedProperty

@IndexedProperty 注解用于生成 list/array 类型中属性索引的 getters/setters. 如果你希望在 Java 中使用 Groovy 类,这就会特别有用。 Groovy 中支持 GPath 访问属性,但是在 Java 是不支持的。@IndexedProperty 可以生成下面属性索引方法:

class SomeBean {
    @IndexedProperty String[] someArray = new String[2]
    @IndexedProperty List someList = []
}

def bean = new SomeBean()
bean.setSomeArray(0, 'value')
bean.setSomeList(0, 123)

assert bean.someArray[0] == 'value'
assert bean.someList == [123]
@groovy.lang.Lazy

@Lazy 用于延迟初始化属性。例如,接下来的代码:

class SomeBean {
    @Lazy LinkedList myField
}

将会生成下面代码:

List $myField
List getMyField() {
    if ($myField!=null) { return $myField }
    else {
        $myField = new LinkedList()
        return $myField
    }
}

初始化属性的默认值就是默认构造函数的声明类型。 可以在属性右侧定义闭包方式来定义默认值,例如下面代码:

class SomeBean {
    @Lazy LinkedList myField = { ['a','b','c']}()
}

这里生成下面代码:

List $myField
List getMyField() {
    if ($myField!=null) { return $myField }
    else {
        $myField = { ['a','b','c']}()
        return $myField
    }
}

If the field is declared volatile then initialization will be synchronized using the double-checked locking pattern. 如果属性声明为 volatile ,则在初始化时使用 double-checked locking 模式进行同步处理。

使用 sofe=true 参数设置,这里的属性将为使用软引用,提供了一个简单的实现方式。 在这种情况下,如果 GC 开始回收引用,初始化将在下次访问 field 时开始执行。

@groovy.lang.Newify

@Newify 用于使用可替换的语法来构建对象:

  • 使用 Python 语法:
@Newify([Tree,Leaf])
    class TreeBuilder {
    Tree tree = Tree(Leaf('A'),Leaf('B'),Tree(Leaf('C')))
}
  • 使用 Ruby 语法:
@Newify([Tree,Leaf])
class TreeBuilder {
    Tree tree = Tree.new(Leaf.new('A'),Leaf.new('B'),Tree.new(Leaf.new('C')))
}

设置 flag 为 false 可以禁止 Ruby 语法。

@groovy.transform.Sortable

The @Sortable AST transformation is used to help write classes that are Comparable and easily sorted by numerous properties. It is easy to use as shown in the following example where we annotate the Person class:

@Sortable 用于编写可比较类,通过多个属性进行排序。接下来的代码中在 Person 类上注解使用:

import groovy.transform.Sortable

@Sortable class Person {
    String first
    String last
    Integer born
}

生成的类中有如下属性:

  • 实现 Comparable 接口
  • 其中 compareTo 方法按照 first , last , born 属性顺序实现
  • 有三个比较方法:comparatorByFirst, comparatorByLastcomparatorByBorn

生成 compareTo 方法:

public int compareTo(java.lang.Object obj) {
    if (this.is(obj)) {
        return 0
    }
    if (!(obj instanceof Person)) {
        return -1
    }
    java.lang.Integer value = this.first <=> obj.first
    if (value != 0) {
        return value
    }
    value = this.last <=> obj.last
    if (value != 0) {
        return value
    }
    value = this.born <=> obj.born
    if (value != 0) {
        return value
    }
    return 0
}

作为一个生成比较器的例子, comparatorByFirst 中有一个这样的 compare 方法:

public int compare(java.lang.Object arg0, java.lang.Object arg1) {
    if (arg0 == arg1) {
        return 0
    }
    if (arg0 != null && arg1 == null) {
        return -1
    }
    if (arg0 == null && arg1 != null) {
        return 1
    }
    return arg0.first <=> arg1.first
}

这样 Person 可以用于任何需要比较的应用中,类似下面场景:

def people = [
    new Person(first: 'Johnny', last: 'Depp', born: 1963),
    new Person(first: 'Keira', last: 'Knightley', born: 1985),
    new Person(first: 'Geoffrey', last: 'Rush', born: 1951),
    new Person(first: 'Orlando', last: 'Bloom', born: 1977)
]

assert people[0] > people[2]
assert people.sort()*.last == ['Rush', 'Depp', 'Knightley', 'Bloom']
assert people.sort(false, Person.comparatorByFirst())*.first == ['Geoffrey', 'Johnny', 'Keira', 'Orlando']
assert people.sort(false, Person.comparatorByLast())*.last == ['Bloom', 'Depp', 'Knightley', 'Rush']
assert people.sort(false, Person.comparatorByBorn())*.last == ['Rush', 'Depp', 'Bloom', 'Knightley']

通常,所有属性都会按照其定义的顺序生产 compareTo 方法。 你可以通过设置 includesexcludes 注解属性来配置生成所需要的 compareTo 方法。 includes 中设置的属性顺序决定了属性比较过程中的优先顺序。 为说明这一点,可以看看下面 Person 的定义:

@Sortable(includes='first,born') class Person {
    String last
    int born
    String first
}

这里 Person 中将包含 comparatorByFirstcomparatorByBorn 比较方法,其生成的 compareTo 方法就像这样:

public int compareTo(java.lang.Object obj) {
    if (this.is(obj)) {
        return 0
    }
    if (!(obj instanceof Person)) {
        return -1
    }
    java.lang.Integer value = this.first <=> obj.first
    if (value != 0) {
        return value
    }
    value = this.born <=> obj.born
    if (value != 0) {
        return value
    }
    return 0
}

Person 可以这样使用:

def people = [
    new Person(first: 'Ben', last: 'Affleck', born: 1972),
    new Person(first: 'Ben', last: 'Stiller', born: 1965)
]

assert people.sort()*.last == ['Stiller', 'Affleck']
@groovy.transform.builder.Builder

@Builder 用于帮助创建流式 API. 这种转换支持多种构建策略,针对不同的案例;并可以根据一系列配置选项,自定义构建过程。 甚至可以定义你自己的构建策略。 下表中列举了, Groovy 中支持的策略及其策略中支持的配置选项:

Strategy Description builderClassName builderMethodName buildMethodName prefix includes/excludes
SimpleStrategy chained setters n/a n/a n/a yes default ‘set’ yes
ExternalStrategy explicit builder class, class being built untouched n/a n/a yes, default ‘build’ yes, default ‘’ yes
DefaultStrategy creates a nested helper class yes, default <TypeName>Builder yes, default ‘builder’ yes, default ‘build’ yes, default ‘’ yes
InitializerStrategy creates a nested helper class providing type-safe fluent creation yes, default <TypeName>Initializer yes, default ‘createInitializer’ yes, default ‘create’ but usually only used internally yes, default ‘’ yes
SimpleStrategy

下面例子中使用 SimpleStrategy :

import groovy.transform.builder.*

@Builder(builderStrategy=SimpleStrategy)
class Person {
    String first
    String last
    Integer born
}

然后,可以通过链式调用 setters :

def p1 = new Person().setFirst('Johnny').setLast('Depp').setBorn(1963)
assert "$p1.first $p1.last" == 'Johnny Depp'

对于每一个属性都会生成类似如下的 setter 方法:

public Person setFirst(java.lang.String first) {
    this.first = first
    return this
}

你可以向下面这样指定一个前缀:

import groovy.transform.builder.*

@Builder(builderStrategy=SimpleStrategy, prefix="")
class Person {
    String first
    String last
    Integer born
}

这里会看到,调用链式 setters 会是这种样式:

def p = new Person().first('Johnny').last('Depp').born(1963)
assert "$p.first $p.last" == 'Johnny Depp'

你可以将 SimpleStrategy 与 @Canonical 结合使用,主要可以用于 includesexcludes 特定的属性。 Groovy 内建的构建机制,如果可以满足你的需求,也可以不急于使用 @Builder 方式:

def p2 = new Person(first: 'Keira', last: 'Knightley', born: 1985)
def p3 = new Person().with {
    first = 'Geoffrey'
    last = 'Rush'
    born = 1951
}
ExternalStrategy

创建一个 builder 类,使用 @Builder 注解,指定 ExternalStrategy 及 forClass . 假设你需要构建下面这个类:

class Person {
    String first
    String last
    int born
}

明确指定创建及使用的 builder 类:

import groovy.transform.builder.*

@Builder(builderStrategy=ExternalStrategy, forClass=Person)
class PersonBuilder { }

def p = new PersonBuilder().first('Johnny').last('Depp').born(1963).build()
assert "$p.first $p.last" == 'Johnny Depp'

这里需要注意,编写的 builder 类(通常此类都是空的)将会自动生成合适的 build 方法及 setters 方法。 生成的 build 方法如下:

public Person build() {
    Person _thePerson = new Person()
    _thePerson.first = first
    _thePerson.last = last
    _thePerson.born = born
    return _thePerson
}

对于 Java 或 Groovy 中的普通 JavaBean (无参构造函数及属性 setter 方法) 都可以为其创建 builder. 下面的例子就是对于 Java 类来创建 builder :

import groovy.transform.builder.*

@Builder(builderStrategy=ExternalStrategy, forClass=javax.swing.DefaultButtonModel)
class ButtonModelBuilder {}

def model = new ButtonModelBuilder().enabled(true).pressed(true).armed(true).rollover(true).selected(true).build()
assert model.isArmed()
assert model.isPressed()
assert model.isEnabled()
assert model.isSelected()
assert model.isRollover()

这里可以使用 prefixincludesexcludes 以及 buildMethodName 注解属性来自定化 builder。 下面例子中将说明这些自定义的使用方式:

import groovy.transform.builder.*
import groovy.transform.Canonical

@Canonical
class Person {
    String first
    String last
    int born
}

@Builder(builderStrategy=ExternalStrategy, forClass=Person, includes=['first', 'last'], buildMethodName='create', prefix='with')
class PersonBuilder { }

def p = new PersonBuilder().withFirst('Johnny').withLast('Depp').create()
assert "$p.first $p.last" == 'Johnny Depp'

@Builder 中的 buildMethodNamebuilderClassName 并不适用这种模式。

你可以将 ExternalStrategy 与 @Canonical 结合使用,主要可以用于 includesexcludes 特定的属性。

DefaultStrategy

下面代码中将展示如何使用 DefaultStrategy:

import groovy.transform.builder.Builder

@Builder
class Person {
    String firstName
    String lastName
    int age
}

def person = Person.builder().firstName("Robert").lastName("Lewandowski").age(21).build()
assert person.firstName == "Robert"
assert person.lastName == "Lewandowski"
assert person.age == 21

如果你想,可以通过使用 builderClassName , buildMethodName, builderMethodName, prefix, includesexcludes 注解属性自定构建过程的各个方面,下面的例子中将使用其中的一部分:

import groovy.transform.builder.Builder

@Builder(buildMethodName='make', builderMethodName='maker', prefix='with', excludes='age')
class Person {
    String firstName
    String lastName
    int age
}

def p = Person.maker().withFirstName("Robert").withLastName("Lewandowski").make()
assert "$p.firstName $p.lastName" == "Robert Lewandowski"

这种模式下还支持注解静态方法及构造器。 在这种情况下,静态方法或构造方法的参数将成为构建对象的属性,静态方法的返回类型为目标构建类型。

如果在一个类中使用了多个 @Builder , 你需要确保生成的辅助类或工厂方法的名字不会重复,这里例子中将讲解这一点:

import groovy.transform.builder.*
import groovy.transform.*

@ToString
@Builder
class Person {
  String first, last
  int born

  Person(){}

  @Builder(builderClassName='MovieBuilder', builderMethodName='byRoleBuilder')
  Person(String roleName) {
     if (roleName == 'Jack Sparrow') {
         this.first = 'Johnny'; this.last = 'Depp'; this.born = 1963
     }
  }

  @Builder(builderClassName='NameBuilder', builderMethodName='nameBuilder', prefix='having', buildMethodName='fullName')
  static String join(String first, String last) {
      first + ' ' + last
  }

  @Builder(builderClassName='SplitBuilder', builderMethodName='splitBuilder')
  static Person split(String name, int year) {
      def parts = name.split(' ')
      new Person(first: parts[0], last: parts[1], born: year)
  }
}

assert Person.splitBuilder().name("Johnny Depp").year(1963).build().toString() == 'Person(Johnny, Depp, 1963)'
assert Person.byRoleBuilder().roleName("Jack Sparrow").build().toString() == 'Person(Johnny, Depp, 1963)'
assert Person.nameBuilder().havingFirst('Johnny').havingLast('Depp').fullName() == 'Johnny Depp'
assert Person.builder().first("Johnny").last('Depp').born(1963).build().toString() == 'Person(Johnny, Depp, 1963)'

forClass 属性不适用于这种模式。

InitializerStrategy

InitializerStrategy 的使用方式:

import groovy.transform.builder.*
import groovy.transform.*

@ToString
@Builder(builderStrategy=InitializerStrategy)
class Person {
    String firstName
    String lastName
    int age
}

Your class will be locked down to have a single public constructor taking a “fully set” initializer. It will also have a factory method to create the initializer. These are used as follows:

source
@CompileStatic
    def firstLastAge() {
    assert new Person(Person.createInitializer().firstName("John").lastName("Smith").age(21)).toString() == 'Person(John, Smith, 21)'
}
firstLastAge()

使用初始化方式,如果设置属性不完整将出现编译错误。如果你不需要严格控制,你可以不使用 @Compilation

You can use the InitializerStrategy in conjunction with @Canonical and @Immutable. 你可以将 InitializerStrategy 结合 @Canonical@Immutable 使用。 这样可以通过使用 includesexcludes 明确特定的属性的使用。

下面代码使用 @Builder 结合 @Immutable:

import groovy.transform.builder.*
import groovy.transform.*

@Builder(builderStrategy=InitializerStrategy)
@Immutable
class Person {
    String first
    String last
    int born
}

@CompileStatic
def createFirstLastBorn() {
  def p = new Person(Person.createInitializer().first('Johnny').last('Depp').born(1963))
  assert "$p.first $p.last $p.born" == 'Johnny Depp 1963'
}

createFirstLastBorn()

这种模式下还支持注解静态方法及构造器。 在这种情况下,静态方法或构造方法的参数将成为构建对象的属性,静态方法的返回类型为目标构建类型。 如果在一个类中使用了多个 @Builder , 你需要确保生成的辅助类或工厂方法的名字不会重复

forClass 属性不适用于这种模式。

Class design annotations

使用这类注解声明方式,简化设计模式(代理,单例等等)的实现方式。

@groovy.lang.Delegate

@Delegate 用于实现代理模式:

class Event {
    @Delegate Date when
    String title
}

The when field is annotated with @Delegate, meaning that the Event class will delegate calls to Date methods to the when field. In this case, the generated code looks like this: when 使用 @Delegate 注解,表示 Event 将代理 Date 的方法至 when 实例上 ,在这种情况下,将生成如下代码:

class Event {
    Date when
    String title
    boolean before(Date other) {
        when.before(other)
    }
    // ...
}

@Delegate 配置参数: (TBD)

@groovy.transform.Immutable

@Immutable 用于创建不可变类,其成员均为不可变。在这里你只需要向下面这样注解对应的类即可:

import groovy.transform.Immutable

@Immutable
class Point {
    int x
    int y
}

Immutable classes generated with @Immutable are automatically made final. 使用 @Immutable 注解的类会自动构建为 final . 对于不可变类,需要保证其成员属性的不可变性,其必须为不可变类型(原始类型或其包装类),已知不可变类型或声明为 @Immutable 类。 @Immutable 与 @Canonical 相似之处在于,前者也会自动生成 toString, equalshashCode 方法,但是如果修改其属性将会抛出 ReadOnlyPropertyException 异常。

@Immutable 依赖于一系列的不可变类型(例如 java.net.URIjava.lang.String),如果需要使用的类型不在其中,@Immutable 将无法使用。 但是通过下面的配置方式,可以指定类型为不可变: (TBD)

@groovy.transform.Memoized

@Memoized 用于简化实现方法调用结果缓存,让我看看下面的代码:

long longComputation(int seed) {
    // slow computation
    Thread.sleep(1000*seed)
    System.nanoTime()
}

这里模拟耗时计算,在不使用 @Memoized 情况下,方法的每次调用都将耗费几秒的时间:

def x = longComputation(1)
def y = longComputation(1)
assert x!=y

添加 @Memoized 将改变方法的执行过程,并添加缓存:

@Memoized
long longComputation(int seed) {
    // slow computation
    Thread.sleep(1000*seed)
    System.nanoTime()
}

def x = longComputation(1) // returns after 1 second
def y = longComputation(1) // returns immediatly
def z = longComputation(2) // returns after 2 seconds
assert x==y
assert x!=z

以下两个可选参数用于配置缓存的大小:

  • protectedCacheSize: GC 清理后,保护的结果数量大小
  • maxCacheSize: 内存中保存的结果数量上限

默认情况下,缓存结果数量不受限制也不受保护。

@groovy.lang.Singleton

@Singleton 用于实现单例模式。这种情况下,数据域使用双重检查:

@Singleton
class GreetingService {
    String greeting(String name) { "Hello, $name!" }
}
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'

默认情况下,单例在类初始化时创建,并通过 instance 属性调用。 这里也可以通过配置改变,调用单例的名称:

@Singleton(property='theOne')
class GreetingService {
    String greeting(String name) { "Hello, $name!" }
}

assert GreetingService.theOne.greeting('Bob') == 'Hello, Bob!'

这里也可以使用 lazy ,延迟初始化:

class Collaborator {
    public static boolean init = false
}
@Singleton(lazy=true,strict=false)
class GreetingService {
    static void init() {}
    GreetingService() {
        Collaborator.init = true
    }
    String greeting(String name) { "Hello, $name!" }
}
GreetingService.init() // make sure class is initialized
assert Collaborator.init == false
GreetingService.instance
assert Collaborator.init == true
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'

例子中,我们设置 strict 为 false , 可以允许自定义构造函数。

@groovy.transform.Mixin

弃用,使用 traits 替代。

Logging improvements

Groovy 支持集成一些广泛使用的日志框架。添加注释并不会阻止你在 classpath 中添加合适的日志框架。

所有的转换工作都基本相同:

  • add static final log field corresponding to the logger
  • wrap all calls to log.level() into the appropriate log.isLevelEnabled guard, depending on the underlying framework

Those transformations support two parameters:

  • value (default log) corresponds to the name of the logger field
  • category (defaults to the class name) is the name of the logger category
@groovy.util.logging.Log

@Log 注解依赖于 JDK logging 框架:

@groovy.util.logging.Log
class Greeter {
    void greet() {
        log.info 'Called greeter'
        println 'Hello, world!'
    }
}

与下面代码相同:

import java.util.logging.Level
import java.util.logging.Logger

class Greeter {
    private static final Logger log = Logger.getLogger(Greeter.name)
    void greet() {
        if (log.isLoggable(Level.INFO)) {
            log.info 'Called greeter'
        }
        println 'Hello, world!'
    }
}
@groovy.util.logging.Commons

Groovy supports the Apache Commons Logging framework using to the @Commons annotation. Writing: @Commons 用于支持 Apache Commons Logging 框架:

@groovy.util.logging.Commons
class Greeter {
    void greet() {
        log.debug 'Called greeter'
        println 'Hello, world!'
    }
}

等价于下面代码:

import org.apache.commons.logging.LogFactory
import org.apache.commons.logging.Log

class Greeter {
    private static final Log log = LogFactory.getLog(Greeter)
    void greet() {
        if (log.isDebugEnabled()) {
            log.debug 'Called greeter'
        }
        println 'Hello, world!'
    }
}
@groovy.util.logging.Log4j

@Log4j 用于支持 Apache Log4j 1.x

@groovy.util.logging.Log4j
class Greeter {
    void greet() {
        log.debug 'Called greeter'
        println 'Hello, world!'
    }
}

代码等价于:

import org.apache.log4j.Logger

class Greeter {
    private static final Logger log = Logger.getLogger(Greeter)
    void greet() {
        if (log.isDebugEnabled()) {
            log.debug 'Called greeter'
        }
        println 'Hello, world!'
    }
}
@groovy.util.logging.Log4j2

@Log4j2 用于支持 ` Apache Log4j 2.x <http://logging.apache.org/log4j/2.x/>`_

@groovy.util.logging.Log4j2
class Greeter {
    void greet() {
        log.debug 'Called greeter'
        println 'Hello, world!'
    }
}

代码等价于:

import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger

class Greeter {
    private static final Logger log = LogManager.getLogger(Greeter)
    void greet() {
        if (log.isDebugEnabled()) {
            log.debug 'Called greeter'
        }
        println 'Hello, world!'
    }
}
@groovy.util.logging.Slf4j

@Slf4j 用于支持 Simple Logging Facade for Java (SLF4J) :

@groovy.util.logging.Slf4j
class Greeter {
    void greet() {
        log.debug 'Called greeter'
        println 'Hello, world!'
    }
}

等价于下面代码:

import org.slf4j.LoggerFactory
import org.slf4j.Logger

class Greeter {
    private static final Logger log = LoggerFactory.getLogger(Greeter)
    void greet() {
        if (log.isDebugEnabled()) {
            log.debug 'Called greeter'
        }
        println 'Hello, world!'
    }
}
Declarative concurrency

Groovy 中提供了一系列注解,用于简化常见的并发模式。

@groovy.transform.Synchronized

@Synchronized 与关键字 synchronized 用法类似。可以使用在任何方法或静态方法上:

import groovy.transform.Synchronized

import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

class Counter {
    int cpt
    @Synchronized
    int incrementAndGet() {
        cpt++
    }
    int get() {
        cpt
    }
}

等价于下面代码,创建一个锁对象,并同步锁定整个方法块:

class Counter {
    int cpt
    private final Object $lock = new Object()

    int incrementAndGet() {
        synchronized($lock) {
            cpt++
        }
    }
    int get() {
        cpt
    }

}

默认情况下, @Synchronized 创建一个名为 $lock 的成员变量,也可以指定你期望的变量用于加锁:

import groovy.transform.Synchronized

import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

class Counter {
    int cpt
    private final Object myLock = new Object()

    @Synchronized('myLock')
    int incrementAndGet() {
        cpt++
    }
    int get() {
        cpt
    }
}
@groovy.transform.WithReadLock and @groovy.transform.WithWriteLock

@WithReadLock 与 @WithWriteLock 共同提供读写锁。其可以用于方法或静态方法,使用使用将创建 reentrantLock 成员变量,并添加合适的同步代码,例如下面代码:

import groovy.transform.WithReadLock
import groovy.transform.WithWriteLock

class Counters {
    public final Map<String,Integer> map = [:].withDefault { 0 }

    @WithReadLock
    int get(String id) {
        map.get(id)
    }

    @WithWriteLock
    void add(String id, int num) {
        Thread.sleep(200) // emulate long computation
        map.put(id, map.get(id)+num)
    }
}

等价于下面代码:

import groovy.transform.WithReadLock as WithReadLock
import groovy.transform.WithWriteLock as WithWriteLock

public class Counters {

    private final Map<String, Integer> map
    private final java.util.concurrent.locks.ReentrantReadWriteLock $reentrantlock

    public int get(java.lang.String id) {
        $reentrantlock.readLock().lock()
        try {
            map.get(id)
        }
        finally {
            $reentrantlock.readLock().unlock()
        }
    }

    public void add(java.lang.String id, int num) {
        $reentrantlock.writeLock().lock()
        try {
            java.lang.Thread.sleep(200)
            map.put(id, map.get(id) + num )
        }
        finally {
            $reentrantlock.writeLock().unlock()
        }
    }
}

@WithReadLock and @WithWriteLock 都支持替换锁对象。这种情况下,锁对象的引用必须用户自己声明:

import groovy.transform.WithReadLock
import groovy.transform.WithWriteLock

import java.util.concurrent.locks.ReentrantReadWriteLock

class Counters {
    public final Map<String,Integer> map = [:].withDefault { 0 }
    private final ReentrantReadWriteLock customLock = new ReentrantReadWriteLock()

    @WithReadLock('customLock')
    int get(String id) {
        map.get(id)
    }

    @WithWriteLock('customLock')
    void add(String id, int num) {
        Thread.sleep(200) // emulate long computation
        map.put(id, map.get(id)+num)
    }
}

更多细节可以参考 groovy.transform.WithReadLock groovy.transform.WithWriteLock

Easier cloning and externalizing

Groovy 中提供 @AutoClone@AutoExternalize 分别用于实现 ClonableExternalizable 接口。

@groovy.transform.AutoClone

@AutoClone 用于实现 @java.lang.Cloneable 接口:

  • the default AutoCloneStyle.CLONE strategy calls super.clone() first then clone() on each cloneable property
  • the AutoCloneStyle.SIMPLE strategy uses a regular constructor call and copies properties from the source to the clone
  • the AutoCloneStyle.COPY_CONSTRUCTOR strategy creates and uses a copy constructor
  • the AutoCloneStyle.SERIALIZATION strategy uses serialization (or externalization) to clone the object

Each of those strategies have pros and cons which are discussed in the Javadoc for groovy.transform.AutoClone and groovy.transform.AutoCloneStyle . 以上这些模式各的优缺点将在 groovy.transform.AutoClonegroovy.transform.AutoCloneStyle 中讨论。

看看下面的例子:

@AutoClone
class Book {
    String isbn
    String title
    List<String> authors
    Date publicationDate
}

等价于下面代码:

class Book implements Cloneable {
    String isbn
    String title
    List<String> authors
    Date publicationDate

    public Book clone() throws CloneNotSupportedException {
        Book result = super.clone()
        result.authors = authors instanceof Cloneable ? (List) authors.clone() : authors
        result.publicationDate = publicationDate.clone()
        result
    }
}

需要注意一点,String 类型属性不会明确的处理,由于 String 为不可变类型,clone 方法将会拷贝 String 的引用。 对于原始类型或 java.lang.Number 子类型都是相同的处理方式。

除了拷贝的样式, AutoClone 还支持多种配置选项:

@groovy.transform.AutoExternalize

@AutoExternalize 用于辅助创建 java.io.Externalizable 类型。其将自动添加接口并生成 writeExternalreadExternal 方法。 例如:


import groovy.transform.AutoExternalize

@AutoExternalize class Book {

String isbn String title float price

}

等价于下面代码:

class Book implements java.io.Externalizable {
    String isbn
    String title
    float price

    void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(isbn)
        out.writeObject(title)
        out.writeFloat( price )
    }

    public void readExternal(ObjectInput oin) {
        isbn = (String) oin.readObject()
        title = (String) oin.readObject()
        price = oin.readFloat()
    }

}

@AutoExternalize 支持两个参数,用于自定义配置: (TBD)

Safer scripting

Groovy 中在运行时可以非常容易的使用用户的脚本(如使用 groovy.lang.GroovyShell)。 你如何确保脚本不会十分消耗 CPU 或不会慢慢将线程池中的线程消耗殆尽? Groovy 中提供了一些注解用于保障脚本的安全,例如生成代码允许自动中断执行过程。

@groovy.transform.ThreadInterrupt

JVM 中一个复杂的情况就是线程不能被终止。 其中 Thread#stop 方法虽然存在,但是已经废弃(不可用),你只能依赖方法 Thread#interrupt. 调用后者将会在线程上设置一个中断标志,但是并不会终止线程执行。 线程中执行的代码有责任检查中断标志并正确退出。 This makes sense when you, as a developer, know that the code you are executing is meant to be run in an independent thread, but in general, you don’t know it. It’s even worse with user scripts, who might not even know what a thread is (think of DSLs).

@ThreadInterrupt 简化了线程代码中的中断检查:

  • loops (for, while)
  • first instruction of a method
  • first instruction of a closure body

Let’s imagine the following user script:

while (true) {
    i++
}

This is an obvious infinite loop. 这是一个明显的无限循环。 如果这段代码在它自身的线程中执行,将无法中断:如果你 join 这个线程,调用代码将继续执行,线程也将持续存活在后台运行,无法终止,并慢慢引起线程饥饿。 一种解决方法:

def config = new CompilerConfiguration()
config.addCompilationCustomizers(
        new ASTTransformationCustomizer(ThreadInterrupt)
)
def binding = new Binding(i:0)
def shell = new GroovyShell(binding,config)

这样配置将 @ThreadInterrupt 用于所有脚本中。可以运行你这样执行脚本:

def t = Thread.start {
    shell.evaluate(userCode)
}
t.join(500) // give at most 500ms for the script to complete
if (t.alive) {
    t.interrupt()
}

这种转换将自动修改你的代码:

while (true) {
    if (Thread.currentThread().interrupted) {
        throw new InterruptedException('The current thread has been interrupted.')
    }
    i++
}

这种检查方式引入内容循环保护,如果中断标志设置到当前线程上,将抛出异常,中断线程执行。

@ThreadInterrupt 支持多种配置选项用于自定义其转换方式: (TBD)

@groovy.transform.TimedInterrupt

@TimedInterrupt 用于处理与 @groovy.transform.ThreadInterrupt 略微不同的问题。 替代检查线程中断标志的方式,其用于检查当前线程执行的时间,如果时间过长,将抛出异常。

这个注解中并没有生成监控线程。 它的工作原理与 @ThreadInterrupt 类似,也是在代码中合适的位置设置检查点。 这意味着,如果在线程中有 IO 阻塞,也不会引起中断。

看看下面代码:

def fib(int n) { n<2?n:fib(n-1)+fib(n-2) }
result = fib(600)

这里是还未优化的 Fibonacci 数列计算实现。 如果 n 值较大,这一计算过程将耗费数分钟执行。 使用 @TimedInterrupt , 你可以选者等待脚本执行的消耗时间。下面代码中设置允许用户脚本最多执行 1 秒钟:

def config = new CompilerConfiguration()
config.addCompilationCustomizers(
        new ASTTransformationCustomizer(value:1, TimedInterrupt)
)
def binding = new Binding(result:0)
def shell = new GroovyShell(this.class.classLoader, binding,config)

等价于下面代码:

@TimedInterrupt(value=1, unit=TimeUnit.SECONDS)
class MyClass {
    def fib(int n) {
        n<2?n:fib(n-1)+fib(n-2)
    }
}

@TimedInterrupt 支持多种配置选项用于自定义其转换方式:

(TBD)

@TimedInterrupt 当前还无法兼容静态方法。

@groovy.transform.ConditionalInterrupt

这最后一个注解可以自定义模式中断脚本。 尤其是在,你希望管理资源(限制调用 API 的次数)可以选择这个注解。 下面例子中,用户代码使用无线循环,@ConditionalInterrupt 允许检查配额管理器并动态中断脚本:

@ConditionalInterrupt({Quotas.disallow('user')})
class UserCode {
    void doSomething() {
        int i=0
        while (true) {
            println "Consuming resources ${++i}"
        }
    }
}

The quota checking is very basic here, but it can be any code:

class Quotas {
    static def quotas = [:].withDefault { 10 }
    static boolean disallow(String userName) {
        println "Checking quota for $userName"
        (quotas[userName]--)<0
    }
}

We can make sure @ConditionalInterrupt works properly using this test code:

assert Quotas.quotas['user'] == 10
def t = Thread.start {
    new UserCode().doSomething()
}
t.join(5000)
assert !t.alive
assert Quotas.quotas['user'] < 0

Of course, in practice, it is unlikely that @ConditionalInterrupt will be itself added by hand on user code. It can be injected in a similar manner as the example shown in the ThreadInterrupt section, using the org.codehaus.groovy.control.customizers.ASTTransformationCustomizer :

def config = new CompilerConfiguration()
def checkExpression = new ClosureExpression(
        Parameter.EMPTY_ARRAY,
        new ExpressionStatement(
                new MethodCallExpression(new ClassExpression(ClassHelper.make(Quotas)), 'disallow', new ConstantExpression('user'))
        )
)
config.addCompilationCustomizers(
        new ASTTransformationCustomizer(value: checkExpression, ConditionalInterrupt)
)

def shell = new GroovyShell(this.class.classLoader,new Binding(),config)

def userCode = """
        int i=0
        while (true) {
            println "Consuming resources \\${++i}"
        }
"""

assert Quotas.quotas['user'] == 10
def t = Thread.start {
    shell.evaluate(userCode)
}
t.join(5000)
assert !t.alive
assert Quotas.quotas['user'] < 0

@ConditionalInterrupt 支持多种配置选项用于自定义其转换方式:

(TBD)

Compiler directives

Grape 依赖管理器

入门

添加依赖

Grape 是 Groovy 集成的 JAR 包依赖管理器。Grape 可以让你快速添加 maven 仓库依赖到你的 classpath 中。 添加方式非常简易,加入一段注释到你的代码上,如:

@Grab(group='org.springframework', module='spring-orm', version='3.2.5.RELEASE')
import org.springframework.jdbc.core.JdbcTemplate

也可以如下简化:

@Grab('org.springframework:spring-orm:3.2.5.RELEASE')
import org.springframework.jdbc.core.JdbcTemplate

你可以使用以上推荐方式,采用注解形式引入依赖。也可以在 mvnrepository.com 查询依赖关系,其中会提供 @Grabpom.xml 格式。

指定扩展仓库

并非所有依赖都在 maven central 中,可以通过以下方式添加指定扩展仓库:

@GrabResolver(name='restlet', root='http://maven.restlet.org/')
@Grab(group='org.restlet', module='org.restlet', version='1.1.6')
Maven Classifiers

一些 maven 依赖需要进一步细分类别,可以通过如下方式定义:

@Grab(group='net.sf.json-lib', module='json-lib', version='2.2.3', classifier='jdk15')
清除传递依赖

有时考虑到版本兼容等问题,需要清除一部分传递依赖,可以做如下配置:

@Grab('net.sourceforge.htmlunit:htmlunit:2.8')
@GrabExclude('xml-apis:xml-apis')
JDBC Drivers

警告

下面这一段还没有理解清楚:

Because of the way JDBC drivers are loaded, you’ll need to configure Grape to attach JDBC driver dependencies to the system class loader. I.e:

@GrabConfig(systemClassLoader=true)
@Grab(group='mysql', module='mysql-connector-java', version='5.1.6')
在 Groovy Shell 中使用 Grape
groovy.grape.Grape.grab(group:'org.springframework', module:'spring', version:'2.5.6')
Proxy 设置

如果需要通过代理服务,可以使用一下命令进行配置:

groovy -Dhttp.proxyHost=yourproxy -Dhttp.proxyPort=8080 yourscript.groovy

如果需要在全局范围添加代理服务,需要增加 JAVA_OPTS 环境变量设置:

JAVA_OPTS = -Dhttp.proxyHost=yourproxy -Dhttp.proxyPort=8080
Logging

如果你需要看到 Grape 执行过程日志,可以将参数 groovy.grape.report.downloads 设置为 true (e.g. add -Dgroovy.grape.report.downloads=true to invocation or JAVA_OPTS),这样 Grape 将会打印如下信息到 System.error:

  • Starting resolve of a dependency
  • Starting download of an artifact
  • Retrying download of an artifact
  • Download size and time for downloaded artifacts

如果需要打印更过日志,可以调整 Ivy 日志级别 (defaults to -1)。 如: -Divy.message.logger.level=4.

Detail

Grape (The Groovy Adaptable Packaging Engine or Groovy Advanced Packaging Engine) 是 grab() 执行的基础环境。 它能允许开发人员通过脚本获取所需要的库。Grape 在运行期间,将会从 JCenter, Ibiblio and java.net 下载所需的库文件,并连接相应库,建立所有依赖的传递关系。

下载的模块将依照 Ivy 的标准规范缓存至 ~/.groovy/grape 目录中。

使用方式

Annotation 注解

groovy.lang.Grab 注解可以添加一个或多个在任意位置,注解内容将通知编译器代码依赖的库。这将会把库添加至 Groovy 编译器的类加载器中。注解的检查将先于类的解析

import com.jidesoft.swing.JideSplitButton
@Grab(group='com.jidesoft', module='jide-oss', version='[2.2.1,2.3.0)')
public class TestClassAnnotation {
    public static String testMethod () {
        return JideSplitButton.class.name
    }
}

An appropriate grab(…​) call will be added to the static initializer of the class of the containing class (or script class in the case of an annotated script element).

Multiple Grape Annotations

在同一处使用多个 Grape 需要使用 @Grapes 注解,如下:

@Grapes([
   @Grab(group='commons-primitives', module='commons-primitives', version='1.0'),
   @Grab(group='org.ccil.cowan.tagsoup', module='tagsoup', version='0.9.7')])
class Example {
// ...
}

否则会遇到下面的错误提示:

Cannot specify duplicate annotation on the same member
方法调用

通常情况下 grab 执行会先于脚本或类的初始化。这是需要确保代码所依赖的库,已经加载进入类加载器中。 几种典型的调用方式:

import groovy.grape.Grape
// random maven library
Grape.grab(group:'com.jidesoft', module:'jide-oss', version:'[2.2.0,)')
Grape.grab([group:'org.apache.ivy', module:'ivy', version:'2.0.0-beta1', conf:['default', 'optional']],
     [group:'org.apache.ant', module:'ant', version:'1.7.0'])
  • 在同一上下文(context)中使用相同参数,调用 grab 多次,需要保证幂等性。相同代码如果在不同的类加载器中调用,需要被再次执行。
  • 如果 args 中传递给 grab 的属性中包括 noExceptions 这将不再有异常抛出。
  • grab 中的 RootLoader 或 GroovyClassLoader 需要指定或着是在调用类的类加载器链中。默认情况下以上类加载器获取失败,将会抛出异常。

命令行工具

Grape 添加命令 grape 用于检查管理本地 grape 缓存

grape install <groupId> <artifactId> [<version>]

这将会安装指定的 groovy module 或 maven artifact。如果 version 指定将安装指定版本,否则安装最新版本。

grape list

列出已经安装的 modules 及其版本号。

grape resolve (<groupId> <artifactId> <version>)+

以上返回 module 文件路径

高级配置

资源库路径

如果你需要改变 grape 默认的下载路径,你需要修改 grape.root 配置项默认值(default: ~/.groovy/grape)

groovy -Dgrape.root=/repo/grape yourscript.groovy
自定义 Ivy 设置

你可以创建 ~/.groovy/grapeConfig.xml 用于自定义 Ivy 配置。 如果没有此文件,这里 可以是 Grape 默认设置。

如何自定义 Ivy 设置更多的信息,可以参考 Ivy 文档

More Examples

.Using Apache Commons Collections:

// create and use a primitive array list
import org.apache.commons.collections.primitives.ArrayIntList

@Grab(group='commons-primitives', module='commons-primitives', version='1.0')
def createEmptyInts() { new ArrayIntList() }

def ints = createEmptyInts()
ints.add(0, 42)
assert ints.size() == 1
assert ints.get(0) == 42

.Using TagSoup:

// find the PDF links of the Java specifications
@Grab(group='org.ccil.cowan.tagsoup', module='tagsoup', version='1.2.1')
def getHtml() {
    def parser = new XmlParser(new org.ccil.cowan.tagsoup.Parser())
    parser.parse("https://docs.oracle.com/javase/specs/")
}
html.body.'**'.a.@href.grep(~/.*\.pdf/).each{ println it }

.Using Google Collections:

import com.google.common.collect.HashBiMap
@Grab(group='com.google.code.google-collections', module='google-collect', version='snapshot-20080530')
def getFruit() { [grape:'purple', lemon:'yellow', orange:'orange'] as HashBiMap }
assert fruit.lemon == 'yellow'
assert fruit.inverse().yellow == 'lemon'

.Launching a Jetty server to serve Groovy templates:

@Grapes([
    @Grab(group='org.eclipse.jetty.aggregate', module='jetty-server', version='8.1.7.v20120910'),
    @Grab(group='org.eclipse.jetty.aggregate', module='jetty-servlet', version='8.1.7.v20120910'),
    @Grab(group='javax.servlet', module='javax.servlet-api', version='3.0.1')])

import org.eclipse.jetty.server.Server
import org.eclipse.jetty.servlet.*
import groovy.servlet.*

def runServer(duration) {
    def server = new Server(8080)
    def context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
    context.resourceBase = "."
    context.addServlet(TemplateServlet, "*.gsp")
    server.start()
    sleep duration
    server.stop()
}

runServer(10000)

Grape 将下载 Jetty 和其他相关的依赖包,在第一次启动脚本时,并且缓存他们。 我们创建了一个 Jetty Server 并且监听 8080 端口,并发布 Groovy’s TemplateServlet 在根路径上。 Groovy 使用它自身强大模版引擎。我们启动服务,并让服务运行固定时间。 当访问 http://localhost:8080/somepage.gsp , 页面将显示 somepage.gsp 模版内容给用户。 这些模版页面必须位于服务脚本的相同目录下。

Test Guide

介绍

Groovy 中对于编写测试有良好的支持。除了语言特性及集成测试上拥有先进的库及框架,Groovy 生态系统中也诞生了大量的测试库及框架。

本章节开始介绍测试特性,并继续关注 JUnit 集成,Spock 规范以及 Geb 用于功能性测试。 最后,我们也会介绍其他一些测试库在 Groovy 中的使用。

语言特性

除了 JUnit 集成支持, Groovy 语言特性已经被证明在测试驱动开发中非常有价值。 这一章节讲展开讲解。

强大的断言

编写测试用例也意味着使用断言制定一系列假设。Java 中可以使用 assert 关键字(J2SE 1.4 中添加)。 Java 中断言的使用可以通过设置 JVM 参数 -ea (or -enableassertions)-da (or -disableassertions) 生效与失效。默认情况下是不生效。

Groovy 中自带了功能相当强大的一些断言声明。与 Java 中不同的是,在断言返回结果为 false 的情况的输出:

def x = 1
assert x == 2

// Output:                                      // <1>
//
// Assertion failed:
// assert x == 2
//        | |
//        1 false

<1> 这一部分内容将在 std-err 中输出

当断言为不成功时,将抛出 java.lang.AssertionError,其中包括了一些原始异常信息。断言的输出会展现出其内部表现情况。

下面例子中,我们会看见强大的断言声明在复杂的布尔语句,集合以及 toString 中发挥的强大作用:

def x = [1,2,3,4,5]
assert (x << 6) == [6,7,8,9,10]

// Output:
//
// Assertion failed:
// assert (x << 6) == [6,7,8,9,10]
//         | |     |
//         | |     false
//         | [1, 2, 3, 4, 5, 6]
//         [1, 2, 3, 4, 5, 6]

Java 中另一个重要的不同是,Groovy 中的断言是默认有效的。这是一项在语言设计层面的决定。

it makes no sense to take off your swim ring if you put your feet into real water

—Bertrand Meyer

断言中需要注意布尔表达式中带有副作用的方法。内部的错误信息构建机制,并不存储当前目标实例的引用,这样错误信息在输出时产生一定失真:

assert [[1,2,3,3,3,3,4]].first().unique() == [1,2,3]

// Output:
//
// Assertion failed:
// assert [[1,2,3,3,3,3,4]].first().unique() == [1,2,3]
//                          |       |        |
//                          |       |        false
//                          |       [1, 2, 3, 4]
//                          [1, 2, 3, 4]                                                        // <1>

<1> 错误信息显示的是集合的状态,而不是在 unique 方法执行前的状态。

你也可以使用 Java 的语法 assert expression1 : expression2``(``expression1 中判断条件,expression2 为自定义输出结果 ) 来自定义断言错误信息的输出。 要知道,这样将会使断言失去其强大的功能,完全就依赖自定义错误信息。

Mocking and Stubbing

Groovy 内部可以很好的支持 Mocking and StubbingJava 中,动态 mocking 框架非常流行。 这个关键原因使用 Java 手工定制来创建 mock 是一项十分困难的工作。这些框架在 Groovy 中可以很方便的使用,如果你选择自定义一些 mockGroovy 中也是很简单的。

下面章节将介绍使用 Groovy 语言特性来创建 mocksstubs

Map Coercion

通过使用 maps 或 expandos ,我们可以很容易获取期望行为结果:

class TranslationService {
    String convert(String key) {
        return "test"
    }
}

def service = [convert: { String key -> 'some text' }] as TranslationService
assert 'some text' == service.convert('key.text')

操作符 as 用于强制转换 map 为特定的类型。 map 的 keys 对应方法的名称,其 values 为 groovy.lang.Closure 闭包块,被指向对应的方法块。

Be aware that map coercion can get into the way if you deal with custom java.util.Map descendant classes in combination with the as operator. The map coercion mechanism is targeted directly at certain collection classes, it doesn’t take custom classes into account.

Closure Coercion

closures 上使用 as 这种方式非常适合开发者测试一些简单的场景。这项技术还不至于让我们放弃使用动态 mocking

Classesinterfaces 中只有一个方法,可以使用强制转化为闭包块为给定的类型。需要注意,在这个过程中,Groovy 内部将创建一个继承给定类型的代理对象。

看看下面例子中,转换闭包为指定类型:

def service = { String key -> 'some text' } as TranslationService
assert 'some text' == service.convert('key.text')

Groovy 支持称为 SAM 强制转换 的特性。在这种情况下,可以不使用 as 操作符, 运行环境可以推断其目标 SAM 类型。

abstract class BaseService {
    abstract void doSomething()
}

BaseService service = { -> println 'doing something' }
service.doSomething()
MockFor and StubFor

Groovy 中的 mockingstubbing 类可以在 groovy.mock.interceptor 包中找到。

MockFor 通过定义强一致顺序期望行为,来支持单元测试的隔离性。在一个典型的测试场景涉及一个测试类,以及一个或多个协作的支持类。在这个场景中,通常只希望对业务逻辑类进行测试。针对这种情况的一个策略就是将当前协作类替换为简单的 mock 对象,以便隔离出业务逻辑来进行测试。MockFor 这里创建的 mocks 使用元编程。这些协助类行为被定义为行为规范,并且自动监测,强制执行。

class Person {
    String first, last
}

class Family {
    Person father, mother
    def nameOfMother() { "$mother.first $mother.last" }
}
def mock = new MockFor(Person)                                                  // <1>
mock.demand.getFirst{ 'dummy' }
mock.demand.getLast{ 'name' }
mock.use {                                                                      // <2>
    def mary = new Person(first:'Mary', last:'Smith')
    def f = new Family(mother:mary)
    assert f.nameOfMother() == 'dummy name'
}
mock.expect.verify()                                                            // <3>

<1> a new mock is created by a new instance of MockFor

<2> a Closure is passed to use which enables the mocking functionality

<3> a call to verify checks whether the sequence and number of method calls is as expected

StubFor 通过定义松散顺序期望行为,来支持单元测试的隔离性。在一个典型的测试场景涉及一个测试类,以及一个或多个协作的支持类。在这个场景中,通常只希望测试 CUT 的业务逻辑。 针对这种情况的一个策略就是将当前协作类替换为简单的 stub 对象,以便隔离出业务逻辑来进行测试。StubFor 这里创建的 stubs 使用元编程。这些协助类行为被定义为行为规范。

def stub = new StubFor(Person)                                                  // <1>
stub.demand.with {                                                              // <2>
    getLast{ 'name' }
    getFirst{ 'dummy' }
}
stub.use {                                                                      // <3>
    def john = new Person(first:'John', last:'Smith')
    def f = new Family(father:john)
    assert f.father.first == 'dummy'
    assert f.father.last == 'name'
}
stub.expect.verify()                                                            // <4>

<1> a new stub is created by a new instance of StubFor

<2> the with method is used for delegating all calls inside the closure to the StubFor instance

<3> a Closure is passed to use which enables the stubbing functionality

<4> a call to verify (optional) checks whether the number of method calls is as expected

MockFor and StubFor can not be used to test statically compiled classes e.g for Java classes or Groovy classes that make use of @CompileStatic. To stub and/or mock these classes you can use Spock or one of the Java mocking libraries.

Expando Meta-Class (EMC)

Groovy 中有个特殊的 MetaClass 被称为 ExpandoMetaClass。 其可以使用简洁的闭包语法动态创建方法,构造器,属性以及静态方法。

每个 java.lang.Class 都有一个特别的 metaClass 属性,其指向一个 ExpandoMetaClass 实例。 看看下面例子:

String.metaClass.swapCase = {->
    def sb = new StringBuffer()
    delegate.each {
        sb << (Character.isUpperCase(it as char) ? Character.toLowerCase(it as char) :
            Character.toUpperCase(it as char))
    }
    sb.toString()
}

def s = "heLLo, worLD!"
assert s.swapCase() == 'HEllO, WORld!'

ExpandoMetaClass 是一个非常好的 mocking 功能备选方案,并且其可以提供更多的高级功能例如 mocking 静态方法。

class Book {
    String title
}

Book.metaClass.static.create << { String title -> new Book(title:title) }

def b = Book.create("The Stand")
assert b.title == 'The Stand'

也可以 moking 构造函数。

Book.metaClass.constructor << { String title -> new Book(title:title) }

def b = new Book("The Stand")
assert b.title == 'The Stand'

Mocking 构造函数像是 hack ,这种方式对一些用例是相当有作用的。在 Grails 中可以找到例子,使用 ExpandoMetaClass 可以在运行时,对领域类添加构造方法。这样可以将 领域对象自身注册到 Spring Application context , 并且允许注入 services 或 被依赖注入容器管理的其他 beans

如果你希望在测试方法级别,改变 metaClass 属性,你需要删除这些变化,否则这些变化将会持久化。删除变化需要这样做:

GroovySystem.metaClassRegistry.setMetaClass(java.lang.String, null)

另一种替代方式,使用 MetaClassRegistryChangeEventListener ,跟踪 classes 变化,在测试运行环境中 通过 cleanup 方法删除这些变化。Grails 框架中有非常好的例子。

除了在类这一级别使用 ExpandoMetaClass ,在 per-object 这一级别也可以支持使用:

def b = new Book(title: "The Stand")
b.metaClass.getTitle {-> 'My Title' }

assert b.title == 'My Title'

在这里 meta-class 仅与实例关联。取决于应用的场景,在这里这种方式较于全局的 meta-class 设置更为合适。

GDK Methods

接下来的章节将详细介绍 GDK ,其可以在测试用例中使用的方式,下面的例子介绍测试数据的生成。

Iterable#combinations

combinations 方法被加入到 java.lang.Iterable 类中,用于获取从多个 list 中的组合 list:

void testCombinations() {
    def combinations = [[2, 3],[4, 5, 6]].combinations()
    assert combinations == [[2, 4], [3, 4], [2, 5], [3, 5], [2, 6], [3, 6]]
}

这个方法可以用于在指定的方法调用上生成所有可能的参数组合。

Iterable#eachCombination

eachCombination 方法被加入到 java.lang.Iterable,当组合建立时,可以在每个元素上运行相应功能: eachCombinationGDK 中方法,满足 java.lang.Iterable 接口的所有类都包含此方法。 在 lists 每次组合中均会调用:

void testEachCombination() {
    [[2, 3],[4, 5, 6]].eachCombination { println it[0] + it[1] }
}

The method could be used in the testing context to call methods with each of the generated combinations.

Tool Support
Test code coverage

代码覆盖对于测量单元测试的效果是很有用的。高代码覆盖的程序出现严重 bug 的机率要远低于低覆盖甚至无覆盖的程序。 为获取代码覆盖指标,需要在测试之前,对生成的字节码进行测量。Groovy 中提供了 Cobertura 工具用于检测此指标数据。

各种框架及构建工具也集成 Cobertura. Grails 中代码覆盖插件基于 Cobertura ; Gradle 中使用 gradle-cobertura 插件,看名字就知道其来源。

下面例子中将展示 Cobertura 是如何在 Gradle 构建脚本中能够测试覆盖:

def pluginVersion = '<plugin version>'
def groovyVersion = '<groovy version>'
def junitVersion = '<junit version>'

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.eriwen:gradle-cobertura-plugin:${pluginVersion}'
    }
}

apply plugin: 'groovy'
apply plugin: 'cobertura'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.codehaus.groovy:groovy-all:${groovyVersion}"
    testCompile "junit:junit:${junitVersion}"
}

cobertura {
    format = 'html'
    includes = ['**/*.java', '**/*.groovy']
    excludes = ['com/thirdparty/**/*.*']
}

Cobertura 覆盖报告有多重输出格式选择,可以代码覆盖报告可以集成到构建任务中。

Unit Tests with JUnit 3 and 4

Groovy 简化了 JUnit 的测试, 使其更 Groovy. 下面章节将更加详细介绍 JUnit3/4 与 Groovy 的集成。

JUnit3

在 Groovy 中支持 JUnit3 测试最突出的类就是 GroovyTestCase。其源自 junit.framework.TestCase ,并提供了一系列附加方法使得 Groovy 中的测试更为便利。

尽管 GroovyTestCase 继承自 TestCase , 但并不意味着无法使用 Junit4 中的特性。 在最近的 Groovy 版本中捆绑了 Junit4,并且使 TestCase 可以向后兼容。 在 Groovy mailing-list 中也有关于使用 Junit4 还是 GroovyTestCase 的争论,这个争论结果主要还是一个喜好问题。 但是在使用 GroovyTestCase 上可以灵活的使用一些方法,使用测试更容易编写

在这一章节,我们将会看到 GroovyTestCase 提供的一些方法。 完整的方法列表可以在 JavaDoc 中 groovy.util.GroovyTestCase 找到。 不要忘记它是继承于 junit.framework.TestCase ,继承了所有 assert* 方法。

Assertion Methods

GroovyTestCase 继承于 junit.framework.TestCase ,继承了所有 assert* 方法

class MyTestCase extends GroovyTestCase {

    void testAssertions() {
        assertTrue(1 == 1)
        assertEquals("test", "test")

        def x = "42"
        assertNotNull "x must not be null", x
        assertNull null

        assertSame x, x
    }

}

与 Java 相比,这里代码在大多数情况下,省略了括号,这样使得 Junit 断言方法调用表达式的可读性更强。

GroovyTestCase 其中一个有趣的断言方法就是 assertScript。它将确保给出的 groovy 代码中没有任何异常:

void testScriptAssertions() {
    assertScript '''
        def x = 1
        def y = 2

        assert x + y == 3
    '''
}
shouldFail Methods

shouldFail 被用于检查代码块是否有误。如果有误,断言会将其吞掉,否则抛出异常:

void testInvalidIndexAccess1() {
    def numbers = [1,2,3,4]
    shouldFail {
        numbers.get(4)
    }
}

上面代码使用了 shouldFail 基础的方法接口,使用 groovy.lang.Closure 作为唯一参数。

如果我们想在 shouldFail 上使用指定的 java.lang.Exception 类型,可以这样做:

void testInvalidIndexAccess2() {
    def numbers = [1,2,3,4]
    shouldFail IndexOutOfBoundsException, {
        numbers.get(4)
    }
}

到现在 shouldFail 还有个特性没有演示:返回异常信息。 如果你想断言异常的错误信息,这样将会非常有用:

void testInvalidIndexAccess3() {
    def numbers = [1,2,3,4]
    def msg = shouldFail IndexOutOfBoundsException, {
        numbers.get(4)
    }
    assert msg.contains('Index: 4, Size: 4')
}
notYetImplemented Method

notYetImplemented 方法受到 HtmlUnit 的极大影响。 其允许标记测试方法为未实现。标记 notYetImplemented 的方法在执行中失败,测试也会提示通过

void testNotYetImplemented1() {
    if (notYetImplemented()) return

    assert 1 == 2
}

这里可以使用注解 @NotYetImplemented 来替代 notYetImplemented 方法调用。 可以在方法上注释其为未实现,可以不需要再调用 notYetImplemented 方法:

@NotYetImplemented
void testNotYetImplemented2() {
    assert 1 == 2
}
Junit4

在 Groovy 中编写 Junit4 的测试用例没有任何限制。 在 Junit4 中可以使用 groovy.test.GroovyAssert 中提供的静态方法来替换 GroovyTestCase 中的方法:

import org.junit.Test

import static groovy.test.GroovyAssert.shouldFail

class JUnit4ExampleTests {

    @Test
    void indexOutOfBoundsAccess() {
        def numbers = [1,2,3,4]
        shouldFail {
            numbers.get(4)
        }
    }

}

在上面例子中看到,GroovyAssert 中的静态方法被引入,可以想在 GroovyTestCase 中一样使用。

groovy.test.GroovyAssert 继承 org.junit.Assert 这意味着,其继承了 Junit 中所有的断言方法。 然而通过介绍了强大的断言声明,使用断言声明来替代 Junit 断言方法是更好的做法,其主要的理由就是这样可以增强断言的消息内容。

这里需要注意的是 GroovyAssert.shouldFailGroovyTestCase.shouldFail 是完全不同的。 GroovyTestCase.shouldFail 返回异常消息, GroovyAssert.shouldFail 返回异常本身。这里可以访问异常对象中其他的一些属性及方法:

@Test
void shouldFailReturn() {
    def e = shouldFail {
        throw new RuntimeException('foo',
                                   new RuntimeException('bar'))
    }
    assert e instanceof RuntimeException
    assert e.message == 'foo'
    assert e.cause.message == 'bar'
}

Testing with Spock

Spock 是针对 JavaGroovy 应用的测试及规范框架。使其脱引而出的是其优美和极富表现力的规范 DSL 。 Spock 规范使用 Groovy 类编写。尽管使用 Groovy 编写,他们也可用于测试 Java 类。

Spock 可以用于单元,集成以及 BBD(behavior-driven-development) 测试,其自身并非测试框架或测试库。

除了这些特性,Spock 也是一个很好的范例,用于实践如何利用 Groovy 编程语言的特性。

这一章节不会详细介绍如何使用 Spock ,而是会让大家记住它,是如何进行单元,继承,功能以及其他类型的测试。

接下来看一下 Spock 规范的结构。

It should give a pretty good feeling on what Spock is up to.

Specifications

Spock 让你编写规范来描述特性(性能,问题)来展现系统关注点。这里的 系统 可以是一个类或是一个完整的应用,用更规范的术语表达为基于规范的系统。

The feature description starts from a specific snapshot of the system and its collaborators, this snapshot is called the feature’s fixture.

Spock 规范类源自 spock.lang.Specification。 具体规范类由 fields, fixture methods, features methods 和 helper methods 组成。

下面看一个简单的规范对于单一特性方法:

class StackSpec extends Specification {

    def "adding an element leads to size increase"() {              // <1>
        setup: "a new stack instance is created"                                // <2>
            def stack = new Stack()

        when:
            stack.push 42                                                                                       // <3>

        then:                                                                   // <4>
            stack.size() == 1
    }
}

<1> Feature method, is by convention named with a String literal.

<2> Setup block, here is where any setup work for this feature needs to be done.

<3> When block describes a stimulus, a certain action under target by this feature specification.

<4> Then block any expressions that can be used to validate the result of the code that was triggered by the when block.

Spock 特性规范被定义在 spock.lang.Specification 方法中。 特性描述使用字符串替代方法名称。

一个特性方法包含多个 block ,上面例子中我们使用 setup, when, then 。这里的 setup block 比较特殊,它是可选的,其允许配置本地的变量在特性方法中可见。 when block 定义执行内容,then 描述执行返回结果。

注意,StackSpecsetup 方法里有一个描述字符串。描述字符串为可选,可以下在 block 标签后添加(例如:setup ,when, then)。

More Spock

Spock 提供了很多特性,像数据表,高级的 mocking 功能。如果想了解更多可查看 Spock GitHub page

Functional Tests with Geb

Geb 是 web 功能测试库,集成于 Junit 和 Spock。 其基于 Selenium web 驱动,和 spock 一样也提供 DSL 编写功能测试。

Geb 的强大特性非常适合功能测试:

  • DOM access via a JQuery-like $ function
  • implements the page pattern
  • support for modularization of certain web components (e.g. menu-bars, etc.) with modules
  • integration with JavaScript via the JS variable

这一章节不会详细介绍如何使用 Geb,但会让大家了解它,知道如果进行功能测试。

下面章节将通过例子介绍通过 Geb 为一个简单的 web page 写功能测试。

A Geb Script

尽管 Geb 可以单独在 Groovy 脚本中使用, 但是在大多数场景下都会与其他测试框架组合使用。 Geb 中各种基类可以在 Junit3,Junit4,TestNG 或 Spock 中使用。 这些基类是 Geb module 的一部分,需要添加依赖。

For example, the following @Grab dependencies have to be used to run Geb with the Selenium Firefox driver in JUnit4 tests. The module that is needed for JUnit 3/4 support is geb-junit:

@Grapes([
    @Grab("org.gebish:geb-core:0.9.2"),
    @Grab("org.gebish:geb-junit:0.9.2"),
    @Grab("org.seleniumhq.selenium:selenium-firefox-driver:2.26.0"),
    @Grab("org.seleniumhq.selenium:selenium-support:2.26.0")
])

核心类是 geb.Browser. 根据名字可以知道通过浏览器访问 DOM 元素。

def browser = new Browser(driver: new FirefoxDriver(), baseUrl: 'http://myhost:8080/myapp')                                        // <1>
browser.drive {
    go "/login"                         // <2>

    $("#username").text = 'John'        // <3>
    $("#password").text = 'Doe'

    $("#loginButton").click()

    assert title == "My Application - Dashboard"
}

<1> A new Browser instance is created. In this case it uses the Selenium FirefoxDriver and sets the baseUrl.

<2> go is used to navigate to an URL or relative URI

<3> $ together with CSS selectors is used to access the username and password DOM fields.

Browser 类中 drive 方法上代理了当前浏览器实例上的所有方法及属性。 在实际应用中,Browser 类基本上都会隐藏于 Geb 的测试基类中。在当前浏览器实例上调用不存在的方法都将通过其进行代理。

class SearchTests extends geb.junit4.GebTest {

    @Test
    void executeSeach() {
        go 'http://somehost/mayapp/search'                                                              // <1>
        $('#searchField').text = 'John Doe'                                                     // <2>
        $('#searchButton').click()                                                              // <3>

        assert $('.searchResult a').first().text() == 'Mr. John Doe'        // <4>
    }
}

<1> Browser#go takes a relative or absolute link and calls the page.

<2> Browser#$ is used to access DOM content. Any CSS selectors supported by the underlying Selenium drivers are allowed

<3> click is used to click a button.

<4> $ is used to get the first link out of the searchResult block

上面例子展示了使用 Junit4 基于 geb.junit4.GebTest web 测试用例。 可以看到起浏览器配置是在外部。GebTest 中基于底层的浏览器实例,代理 go$ 方法。

More Geb

前面章节我们只是轻描淡写的描述了 Geb 的特性。更多信息可以查看其 主页

领域专属语言 DSL (TBD)

Command chains

Groovy 允许在方法调用的参数周围省略括号。 在 command chain 中同样也可以如此省略括号,以及方法链之间的点号。 总体的思路是在调用 a b c da(b).c(d) 的含义是一致。 这同样适用于多参数,闭包参数和命名参数。 这样的方法链也可以用于赋值使用。 下面的一些例子中,可以看到这种语法的使用:

// equivalent to: turn(left).then(right)
turn left then right

// equivalent to: take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours

// equivalent to: paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow

// with named parameters too
// equivalent to: check(that: margarita).tastes(good)
check that: margarita tastes good

// with closures as parameters
// equivalent to: given({}).when({}).then({})
given { } when { } then { }

在方法链中如果使用不带参数的方法,是需要添加括号的:

// equivalent to: select(all).unique().from(names)
select all unique() from names

如果 command chain 由奇数个元素(方法、参数), 其结尾元素为属性:

// equivalent to: take(3).cookies
// and also this: take(3).getCookies()

take 3 cookies

command chain 使得使用 Groovy 来编写更广泛的 DSL 成为可能。

上面例子,我们看到了基于 DSLcommand chain 的使用。 下面例子中我们将介绍,第一个例子,使用 mapsclosures 来创建一个 DSL

show = { println it }
square_root = { Math.sqrt(it) }

def please(action) {
  [the: { what ->
    [of: { n -> action(what(n)) }]
  }]
}

// equivalent to: please(show).the(square_root).of(100)
please show the square_root of 100
// ==> 10.0

第二例子中将考虑如何通过 DSL 来简化现存的 API。 你可能需要将你的代码给客户,业务分析师,测试人员来阅读,但是他们都不会是 Java 开发人员。 这里我们使用 Google Guava 库 中的 Splitter,尽管它已经有了非仓不错的 Fluent API,但我们还是对他做一些包装。

@Grab('com.google.guava:guava:r09')
import com.google.common.base.*
def result = Splitter.on(',').trimResults(CharMatcher.is('_' as char)).split("_a ,_b_ ,c__").iterator().toList()

这段代码对于 Java 开发人员非常好理解,但是对于你的目标客户就不一定了,并且也需要写很多的代码。 在这里我们还是通过使用 maps 和 closures 来让它变得更加的简单:

@Grab('com.google.guava:guava:r09')
import com.google.common.base.*
def split(string) {
  [on: { sep ->
    [trimming: { trimChar ->
      Splitter.on(sep).trimResults(CharMatcher.is(trimChar as char)).split(string).iterator().toList()
    }]
  }]
}

我们可以这样写:

def result = split "_a ,_b_ ,c__" on ',' trimming '_'

操作符重载 (Operator overloading)

Groovy 中的各种操作符都对应相关的方法调用。 这样可以在 GroovyJava 对象上利用操作符重载。 下表中描述 Groovy 中操作符与方法的对应关系:

Operator Method
a + b a.plus(b)
a - b a.minus(b)
a * b a.multiply(b)
a ** b a.power(b)
a / b a.div(b)
a % b a.mod(b)
a | b a.or(b)
a ^ b a.xor(b)
a++ or ++a a.next()
a– or –a a.previous()
a[b] a.getAt(b)
a[b] = c a.putAt(b, c)
a << b a.leftShift(b)
a >> b a.rightShift(b)
switch(a) { case(b) : } b.isCase(a)
if(a) a.asBoolean()
~a a.bitwiseNegate()
-a a.negative()
+a a.positive()
a as b a.asType(b)
a == b a.equals(b)
a != b !a.equals(b)
a <=> b a.compareTo(b)
a > b a.compareTo(b) > 0
a >= b a.compareTo(b) >= 0
a < b a.compareTo(b) < 0
a <= b a.compareTo(b) <= 0

Script base classes

The Script Class

Groovy scripts 都会被编译为 classes 。例如:

println 'Hello from Groovy'

被编译为继承与抽象类 groovy.lang.Script 的类。这个类只有一个抽象方法 run 。当 script 完成变异,其代码体就成为 run 方法,脚本中其他方法将在实现类中。 Script 类通过 Binding 对象,提供了与应用集成的基础条件,例如:

def binding = new Binding()              // <1>
def shell = new GroovyShell(binding)     // <2>
binding.setVariable('x',1)               // <3>
binding.setVariable('y',3)
shell.evaluate 'z=2*x+y'                 // <4>
assert binding.getVariable('z') == 5     // <5>

<1> a binding is used to share data between the script and the calling class

<2> a GroovyShell can be used with this binding

<3> input variables are set from the calling class inside the binding

<4> then the script is evaluated

<5> and the z variable has been “exported” into the binding

这是一种非常实用的方式在调用方与脚本中共享数据,然而在一些情况下其也存在一些不足的地方。 基于此,Groovy 中可以自定义 Script ,通过定义抽象类继承 groovy.lang.Script:

abstract class MyBaseClass extends Script {
    String name
    public void greet() { println "Hello, $name!" }
}

让后将自定义的 `` script`` 类在编译器中声明,如:

def config = new CompilerConfiguration()                         // <1>
config.scriptBaseClass = 'MyBaseClass'                       // <2>
def shell = new GroovyShell(this.class.classLoader, config)  // <3>
shell.evaluate """
    setName 'Judith'                                         // <4>
    greet()
"""

<1> create a custom compiler configuration

<2> set the base script class to our custom base script class

<3> then create a GroovyShell using that configuration

<4> the script will then extend the base script class, giving direct access to the name property and greet method

The @BaseScript annotation

这里还可以使用 @BaseScript 注释:

import groovy.transform.BaseScript

@BaseScript MyBaseClass baseScript
setName 'Judith'
greet()

@BaseScript 需要在注释在自定义的脚本类型变量上,或者将自定义脚本类型作为 BaseScript 的成员,在执行脚本上注释:

@BaseScript(MyBaseClass) import groovy.transform.BaseScript

setName ‘Judith’ greet()

Alternate abstract method

我们知道了在 script 类中只有一个需要实现的抽象方法 runrun 方法在脚本引擎中自动执行。在有些情况下,基类中 实现了 run 方法,并提供了一个替代方法供脚本使用。下面例子中,run 方法用于初始化:

abstract class MyBaseClass extends Script {
    int count
    abstract void scriptBody()                    // <1>
    def run() {
        count++                                   // <2>
        scriptBody()                              // <3>
        count                                     // <4>
    }
}

<1> the base script class should define one (and only one) abstract method

<2> the run method can be overriden and perform a task before executing the script body

<3> run calls the abstract scriptBody method which will delegate to the user script

<4> then it can return something else than the value from the script

If you execute this code:

def result = shell.evaluate """
    println 'Ok'
"""
assert result == 1

你会看到脚本执行后,run 方法将返回 1。 使用 parse 替代 evaluate 执行,将会更加清晰,这样可以在同一脚本上执行多次 run 方法:

def script = shell.parse("println 'Ok'")
assert script.run() == 1
assert script.run() == 2

Adding properties to numbers

In Groovy number types are considered equal to any other types.

这样可以通过添加属性或方法来增强 numbers。这样在处理数量计算上会非常便利。详细的增强方式可以查看章节 extension modulescategories

这里我们使用 TimeCategory:

use(TimeCategory)  {
    println 1.minute.from.now                   // <1>
    println 10.hours.ago

    def someDate = new Date()               // <2>
    println someDate - 3.months
}

<1> using the TimeCategory, a property minute is added to the Integer class

<2> similarily, the months method returns a groovy.time.DatumDependentDuration which can be used in calculus

Categories 是词汇绑定,使得其非常适合定义内部 DSL.

@DelegatesTo

编译期代理策略

@groovy.lang.DelegatesTo is a documentation and compile-time annotation aimed at:

documenting APIs that use closures as arguments

providing type information for the static type checker and compiler

The Groovy language is a platform of choice for building DSLs. Using closures, it’s quite easy to create custom control structures, as well as it is simple to create builders. Imagine that you have the following code:

Groovy 语言是平台选择来构建 DSLs。使用闭包可以很容易的创建自定义的结构。 想象下面的代码:

email {
    from 'dsl-guru@mycompany.com'
    to 'john.doe@waitaminute.com'
    subject 'The pope has resigned!'
    body {
        p 'Really, the pope has resigned!'
    }
}

使用构建者策略来实现,使用命名为 email 方法来接收 一个闭包参数。该方法将代理后续方法(from , to, subject, body)的调用。 body 作为一个方法接收闭包参数,同样也使用构建者策略。

实现构建者通常可以使用下面方式:

def email(Closure cl) {
    def email = new EmailSpec()
    def code = cl.rehydrate(email, this, this)
    code.resolveStrategy = Closure.DELEGATE_ONLY
    code()
}

EmailSpec 实现了 from, to, ... 一系列方法。调用 rehydrate 拷贝一份闭包用于设置 delegate , owner 以及 thisObject 。 当设置 DELEGATE_ONLY 时,设置 owner 与 this 就不是很重要,这种情况下只处理闭包的代理。

class EmailSpec {
    void from(String from) { println "From: $from"}
    void to(String... to) { println "To: $to"}
    void subject(String subject) { println "Subject: $subject"}
    void body(Closure body) {
        def bodySpec = new BodySpec()
        def code = body.rehydrate(bodySpec, this, this)
        code.resolveStrategy = Closure.DELEGATE_ONLY
        code()
    }
}

EmailSpec 中 body 方法接收一个闭包,先拷贝,再执行。 这种方式在 Groovy 中被称为构建者模式。

One of the problems with the code that we’ve shown is that the user of the email method doesn’t have any information about the methods that he’s allowed to call inside the closure. The only possible information is from the method documentation. There are two issues with this: first of all, documentation is not always written, and if it is, it’s not always available (javadoc not downloaded, for example). Second, it doesn’t help IDEs. What would be really interesting, here, is for IDEs to help the developer by suggesting, once they are in the closure body, methods that exist on the email class.

Moreover, if the user calls a method in the closure which is not defined by the EmailSpec class, the IDE should at least issue a warning (because it’s very likely that it will break at runtime).

One more problem with the code above is that it is not compatible with static type checking. Type checking would let the user know if a method call is authorized at compile time instead of runtime, but if you try to perform type checking on this code:

email {
    from 'dsl-guru@mycompany.com'
    to 'john.doe@waitaminute.com'
    subject 'The pope has resigned!'
    body {
        p 'Really, the pope has resigned!'
    }
}

Then the type checker will know that there’s an email method accepting a Closure, but it will complain for every method call inside the closure, because from, for example, is not a method which is defined in the class. Indeed, it’s defined in the EmailSpec class and it has absolutely no hint to help it knowing that the closure delegate will, at runtime, be of type EmailSpec:

@groovy.transform.TypeChecked
void sendEmail() {
    email {
        from 'dsl-guru@mycompany.com'
        to 'john.doe@waitaminute.com'
        subject 'The pope has resigned!'
        body {
            p 'Really, the pope has resigned!'
        }
    }
}

will fail compilation with errors like this one:

注解

[Static type checking] - Cannot find matching method MyScript#from(java.lang.String). Please check if the declared type is right and if the method exists.
@ line 31, column 21.
from 'dsl-guru@mycompany.com
@DelegatesTo

For those reasons, Groovy 2.1 introduced a new annotation named @DelegatesTo. The goal of this annotation is to solve both the documentation issue, that will let your IDE know about the expected methods in the closure body, and it will also solve the type checking issue, by giving hints to the compiler about what are the potential receivers of method calls in the closure body.

The idea is to annotate the Closure parameter of the email method:

def email(@DelegatesTo(EmailSpec) Closure cl) {
    def email = new EmailSpec()
    def code = cl.rehydrate(email, this, this)
    code.resolveStrategy = Closure.DELEGATE_ONLY
    code()
}

What we’ve done here is telling the compiler (or the IDE) that when the method will be called with a closure, the delegate of this closure will be set to an object of type email. But there is still a problem: the default delegation strategy is not the one which is used in our method. So we will give more information and tell the compiler (or the IDE) that the delegation strategy is also changed:

def email(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=EmailSpec) Closure cl) {
    def email = new EmailSpec()
    def code = cl.rehydrate(email, this, this)
    code.resolveStrategy = Closure.DELEGATE_ONLY
    code()
}

Now, both the IDE and the type checker (if you are using @TypeChecked) will be aware of the delegate and the delegation strategy. This is very nice because it will both allow the IDE to provide smart completion, but it will also remove errors at compile time that exist only because the behaviour of the program is normally only known at runtime!

The following code will now pass compilation:

@TypeChecked
void doEmail() {
    email {
        from 'dsl-guru@mycompany.com'
        to 'john.doe@waitaminute.com'
        subject 'The pope has resigned!'
        body {
            p 'Really, the pope has resigned!'
        }
    }
}
DelegatesTo modes

@DelegatesTo supports multiple modes that we will describe with examples in this section.

Simple delegation

In this mode, the only mandatory parameter is the value which says to which class we delegate calls. Nothing more. We’re telling the compiler that the type of the delegate will always be of the type documented by @DelegatesTo (note that it can be a subclass, but if it is, the methods defined by the subclass will not be visible to the type checker).

void body(@DelegatesTo(BodySpec) Closure cl) {
    // ...
}
Delegation strategy

In this mode, you must specify both the delegate class and a delegation strategy. This must be used if the closure will not be called with the default delegation strategy, which is Closure.OWNER_FIRST.

void body(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=BodySpec) Closure cl) {
    // ...
}
Delegate to parameter

In this variant, we will tell the compiler that we are delegating to another parameter of the method. Take the following code:

def exec(Object target, Closure code) {
   def clone = code.rehydrate(target, this, this)
   clone()
}

Here, the delegate which will be used is not created inside the exec method. In fact, we take an argument of the method and delegate to it. Usage may look like this:

def email = new Email()
exec(email) {
   from '...'
   to '...'
   send()
}

Each of the method calls are delegated to the email parameter. This is a widely used pattern which is also supported by @DelegatesTo using a companion annotation:

def exec(@DelegatesTo.Target Object target, @DelegatesTo Closure code) {
   def clone = code.rehydrate(target, this, this)
   clone()
}

A closure is annotated with @DelegatesTo, but this time, without specifying any class. Instead, we’re annotating another parameter with @DelegatesTo.Target. The type of the delegate is then determined at compile time. One could think that we are using the parameter type, which in this case is Object but this is not true. Take this code:

class Greeter {
   void sayHello() { println 'Hello' }
}
def greeter = new Greeter()
exec(greeter) {
   sayHello()
}

Remember that this works out of the box without having to annotate with @DelegatesTo. However, to make the IDE aware of the delegate type, or the type checker aware of it, we need to add @DelegatesTo. And in this case, it will now that the Greeter variable is of type Greeter, so it will not report errors on the sayHello method even if the exec method doesn’t explicitly define the target as of type Greeter. This is a very powerful feature, because it prevents you from writing multiple versions of the same exec method for different receiver types!

In this mode, the @DelegatesTo annotation also supports the strategy parameter that we’ve described upper.

5.3.4. Multiple closures

In the previous example, the exec method accepted only one closure, but you may have methods that take multiple closures:

void fooBarBaz(Closure foo, Closure bar, Closure baz) {
...

} Then nothing prevents you from annotating each closure with @DelegatesTo:

class Foo { void foo(String msg) { println “Foo ${msg}!” } } class Bar { void bar(int x) { println “Bar ${x}!” } } class Baz { void baz(Date d) { println “Baz ${d}!” } }

void fooBarBaz(@DelegatesTo(Foo) Closure foo, @DelegatesTo(Bar) Closure bar, @DelegatesTo(Baz) Closure baz) {
...

} But more importantly, if you have multiple closures and multiple arguments, you can use several targets:

void fooBarBaz(

@DelegatesTo.Target(‘foo’) foo, @DelegatesTo.Target(‘bar’) bar, @DelegatesTo.Target(‘baz’) baz,

@DelegatesTo(target=’foo’) Closure cl1, @DelegatesTo(target=’bar’) Closure cl2, @DelegatesTo(target=’baz’) Closure cl3) { cl1.rehydrate(foo, this, this).call() cl2.rehydrate(bar, this, this).call() cl3.rehydrate(baz, this, this).call()

}

def a = new Foo() def b = new Bar() def c = new Baz() fooBarBaz(

a, b, c, { foo(‘Hello’) }, { bar(123) }, { baz(new Date()) }

) At this point, you may wonder why we don’t use the parameter names as references. The reason is that the information (the parameter name) is not always available (it’s a debug-only information), so it’s a limitation of the JVM. 5.3.5. Delegating to a generic type

In some situations, it is interesting to instruct the IDE or the compiler that the delegate type will not be a parameter but a generic type. Imagine a configurator that runs on a list of elements:

public <T> void configure(List<T> elements, Closure configuration) {
elements.each { e->
def clone = configuration.rehydrate(e, this, this) clone.resolveStrategy = Closure.DELEGATE_FIRST clone.call()

}

} Then this method can be called with any list like this:

@groovy.transform.ToString class Realm {

String name

} List<Realm> list = [] 3.times { list << new Realm() } configure(list) {

name = ‘My Realm’

} assert list.every { it.name == ‘My Realm’ } To let the type checker and the IDE know that the configure method calls the closure on each element of the list, you need to use @DelegatesTo differently:

public <T> void configure(
@DelegatesTo.Target List<T> elements, @DelegatesTo(strategy=Closure.DELEGATE_FIRST, genericTypeIndex=0) Closure configuration) {

def clone = configuration.rehydrate(e, this, this) clone.resolveStrategy = Closure.DELEGATE_FIRST clone.call()

} @DelegatesTo takes an optional genericTypeIndex argument that tells what is the index of the generic type that will be used as the delegate type. This must be used in conjunction with @DelegatesTo.Target and the index starts at 0. In the example above, that means that the delegate type is resolved against List<T>, and since the generic type at index 0 is T and inferred as a Realm, the type checker infers that the delegate type will be of type Realm.

We’re using a genericTypeIndex instead of a placeholder (T) because of JVM limitations. 5.3.6. Delegating to an arbitrary type

It is possible that none of the options above can represent the type you want to delegate to. For example, let’s define a mapper class which is parametrized with an object and defines a map method which returns an object of another type:

class Mapper<T,U> {

final T value Mapper(T value) { this.value = value } U map(Closure<U> producer) {

producer.delegate = value producer()

}

} The mapper class takes two generic type arguments: the source type and the target type The source object is stored in a final field The map method asks to convert the source object to a target object As you can see, the method signature from map does not give any information about what object will be manipulated by the closure. Reading the method body, we know that it will be the value which is of type T, but T is not found in the method signature, so we are facing a case where none of the available options for @DelegatesTo is suitable. For example, if we try to statically compile this code:

def mapper = new Mapper<String,Integer>(‘Hello’) assert mapper.map { length() } == 5 Then the compiler will fail with:

Static type checking] - Cannot find matching method TestScript0#length() In that case, you can use the type member of the @DelegatesTo annotation to reference T as a type token:

class Mapper<T,U> {

final T value Mapper(T value) { this.value = value } U map(@DelegatesTo(type=”T”) Closure<U> producer) {

producer.delegate = value producer()

}

} The @DelegatesTo annotation references a generic type which is not found in the method signature Note that you are not limited to generic type tokens. The type member can be used to represent complex types, such as List<T> or Map<T,List<U>>. The reason why you should use that in last resort is that the type is only checked when the type checker finds usage of @DelegatesTo, not when the annotated method itself is compiled. This means that type safety is only ensured at the call site. Additionally, compilation will be slower (though probably unnoticeable for most cases).

  1. Compilation customizers

6.1. Introduction

Whether you are using groovyc to compile classes or a GroovyShell, for example, to execute scripts, under the hood, a compiler configuration is used. This configuration holds information like the source encoding or the classpath but it can also be used to perform more operations like adding imports by default, applying AST transformations transparently or disabling global AST transformations.

The goal of compilation customizers is to make those common tasks easy to implement. For that, the CompilerConfiguration class is the entry point. The general schema will always be based on the following code:

import org.codehaus.groovy.control.CompilerConfiguration // create a configuration def config = new CompilerConfiguration() // tweak the configuration config.addCompilationCustomizers(...) // run your script def shell = new GroovyShell(config) shell.evaluate(script) Compilation customizers must extend the org.codehaus.groovy.control.customizers.CompilationCustomizer class. A customizer works:

on a specific compilation phase

on every class node being compiled

You can implement your own compilation customizer but Groovy includes some of the most common operations.

6.2. Import customizer

Using this compilation customizer, your code will have imports added transparently. This is in particular useful for scripts implementing a DSL where you want to avoid users from having to write imports. The import customizer will let you add all the variants of imports the Groovy language allows, that is:

class imports, optionally aliased

star imports

static imports, optionally aliased

static star imports

import org.codehaus.groovy.control.customizers.ImportCustomizer

def icz = new ImportCustomizer() // “normal” import icz.addImports(‘java.util.concurrent.atomic.AtomicInteger’, ‘java.util.concurrent.ConcurrentHashMap’) // “aliases” import icz.addImport(‘CHM’, ‘java.util.concurrent.ConcurrentHashMap’) // “static” import icz.addStaticImport(‘java.lang.Math’, ‘PI’) // import static java.lang.Math.PI // “aliased static” import icz.addStaticImport(‘pi’, ‘java.lang.Math’, ‘PI’) // import static java.lang.Math.PI as pi // “star” import icz.addStarImports ‘java.util.concurrent’ // import java.util.concurrent.* // “static star” import icz.addStaticStars ‘java.lang.Math’ // import static java.lang.Math.* A detailed description of all shortcuts can be found in org.codehaus.groovy.control.customizers.ImportCustomizer

6.3. AST transformation customizer

The AST transformation customizer is meant to apply AST transformations transparently. Unlike global AST transformations that apply on every class beeing compiled as long as the transform is found on classpath (which has drawbacks like increasing the compilation time or side effects due to transformations applied where they should not), the customizer will allow you to selectively apply a transform only for specific scripts or classes.

As an example, let’s say you want to be able to use @Log in a script. The problem is that @Log is normally applied on a class node and a script, by definition, doesn’t require one. But implementation wise, scripts are classes, it’s just that you cannot annotate this implicit class node with @Log. Using the AST customizer, you have a workaround to do it:

import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer import groovy.util.logging.Log

def acz = new ASTTransformationCustomizer(Log) config.addCompilationCustomizers(acz) That’s all! Internally, the @Log AST transformation is applied to every class node in the compilation unit. This means that it will be applied to the script, but also to classes defined within the script.

If the AST transformation that you are using accepts parameters, you can use parameters in the constructor too:

def acz = new ASTTransformationCustomizer(Log, value: ‘LOGGER’) // use name ‘LOGGER’ instead of the default ‘log’ config.addCompilationCustomizers(acz) As the AST transformation customizers works with objects instead of AST nodes, not all values can be converted to AST transformation parameters. For example, primitive types are converted to ConstantExpression (that is LOGGER is converted to new ConstantExpression(‘LOGGER’), but if your AST transformation takes a closure as an argument, then you have to give it a ClosureExpression, like in the following example:

def configuration = new CompilerConfiguration() def expression = new AstBuilder().buildFromCode(CompilePhase.CONVERSION) { -> true }.expression[0] def customizer = new ASTTransformationCustomizer(ConditionalInterrupt, value: expression, thrown: SecurityException) configuration.addCompilationCustomizers(customizer) def shell = new GroovyShell(configuration) shouldFail(SecurityException) {

shell.evaluate(“”“

// equivalent to adding @ConditionalInterrupt(value={true}, thrown: SecurityException) class MyClass {

void doIt() { }

} new MyClass().doIt()

“””)

} For a complete list of options, please refer to org.codehaus.groovy.control.customizers.ASTTransformationCustomizer

6.4. Secure AST customizer

This customizer will allow the developer of a DSL to restrict the grammar of the language, to prevent users from using some constructs, for example. It is only ``secure’’ in that sense only and it is very important to understand that it does not replace a security manager. The only reason for it to exist is to limit the expressiveness of the language. This customizer only works at the AST (abstract syntax tree) level, not at runtime! It can be strange at first glance, but it makes much more sense if you think of Groovy as a platform to build DSLs. You may not want a user to have a complete language at hand. In the example below, we will demonstrate it using an example of language that only allows arithmetic operations, but this customizer allows you to:

allow/disallow creation of closures

allow/disallow imports

allow/disallow package definition

allow/disallow definition of methods

restrict the receivers of method calls

restrict the kind of AST expressions a user can use

restrict the tokens (grammar-wise) a user can use

restrict the types of the constants that can be used in code

For all those features, the secure AST customizer works using either a whitelist (list of elements that are allowed) or a blacklist (list of elements that are disallowed). For each type of feature (imports, tokens, …) you have the choice to use either a whitelist or a blacklist, but you can mix whitelists and blacklists for distinct features. In general, you will choose whitelists (disallow all, allow selected).

import org.codehaus.groovy.control.customizers.SecureASTCustomizer import static org.codehaus.groovy.syntax.Types.*

def scz = new SecureASTCustomizer() scz.with {

closuresAllowed = false // user will not be able to write closures methodDefinitionAllowed = false // user will not be able to define methods importsWhitelist = [] // empty whitelist means imports are disallowed staticImportsWhitelist = [] // same for static imports staticStarImportsWhitelist = [‘java.lang.Math’] // only java.lang.Math is allowed // the list of tokens the user can find // constants are defined in org.codehaus.groovy.syntax.Types tokensWhitelist = [

PLUS, MINUS, MULTIPLY, DIVIDE, MOD, POWER, PLUS_PLUS, MINUS_MINUS, COMPARE_EQUAL, COMPARE_NOT_EQUAL, COMPARE_LESS_THAN, COMPARE_LESS_THAN_EQUAL, COMPARE_GREATER_THAN, COMPARE_GREATER_THAN_EQUAL,

].asImmutable() // limit the types of constants that a user can define to number types only constantTypesClassesWhiteList = [

Integer, Float, Long, Double, BigDecimal, Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE

].asImmutable() // method calls are only allowed if the receiver is of one of those types // be careful, it’s not a runtime type! receiversClassesWhiteList = [

Math, Integer, Float, Double, Long, BigDecimal

].asImmutable()

} use for token types from org.codehaus.groovy.syntax.Types you can use class literals here If what the secure AST customizer provides out of the box isn’t enough for your needs, before creating your own compilation customizer, you might be interested in the expression and statement checkers that the AST customizer supports. Basically, it allows you to add custom checks on the AST tree, on expressions (expression checkers) or statements (statement checkers). For this, you must implement org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementChecker or org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker.

Those interfaces define a single method called isAuthorized, returning a boolean, and taking a Statement (or Expression) as a parameter. It allows you to perform complex logic over expressions or statements to tell if a user is allowed to do it or not.

For example, there’s no predefined configuration flag in the customizer which will let you prevent people from using an attribute expression. Using a custom checker, it is trivial:

def scz = new SecureASTCustomizer() def checker = { expr ->

!(expr instanceof AttributeExpression)

} as SecureASTCustomizer.ExpressionChecker scz.addExpressionCheckers(checker) Then we can make sure that this works by evaluating a simple script:

new GroovyShell(config).evaluate ‘’‘
class A {
int val

}

def a = new A(val: 123) a.@val

‘’’ will fail compilation Statements can be checked using org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementChecker Expressions can be checked using org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker

6.5. Source aware customizer

This customizer may be used as a filter on other customizers. The filter, in that case, is the org.codehaus.groovy.control.SourceUnit. For this, the source aware customizer takes another customizer as a delegate, and it will apply customization of that delegate only and only if predicates on the source unit match.

SourceUnit gives you access to multiple things but in particular the file being compiled (if compiling from a file, of course). It gives you the potential to perform operation based on the file name, for example. Here is how you would create a source aware customizer:

import org.codehaus.groovy.control.customizers.SourceAwareCustomizer import org.codehaus.groovy.control.customizers.ImportCustomizer

def delegate = new ImportCustomizer() def sac = new SourceAwareCustomizer(delegate) Then you can use predicates on the source aware customizer:

// the customizer will only be applied to classes contained in a file name ending with ‘Bean’ sac.baseNameValidator = { baseName ->

baseName.endsWith ‘Bean’

}

// the customizer will only be applied to files which extension is ‘.spec’ sac.extensionValidator = { ext -> ext == ‘spec’ }

// source unit validation // allow compilation only if the file contains at most 1 class sac.sourceUnitValidator = { SourceUnit sourceUnit -> sourceUnit.AST.classes.size() == 1 }

// class validation // the customizer will only be applied to classes ending with ‘Bean’ sac.classValidator = { ClassNode cn -> cn.endsWith(‘Bean’) } 6.6. Customizer builder

If you are using compilation customizers in Groovy code (like the examples above) then you can use an alternative syntax to customize compilation. A builder (org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder) simplifies the creation of customizers using a hierarchical DSL.

import org.codehaus.groovy.control.CompilerConfiguration import static org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder.withConfig

def conf = new CompilerConfiguration() withConfig(conf) {

// ...

} static import of the builder method configuration goes here The code sample above shows how to use the builder. A static method, withConfig, takes a closure corresponding to the builder code, and automatically registers compilation customizers to the configuration. Every compilation customizer available in the distribution can be configured this way:

6.6.1. Import customizer

withConfig(configuration) {
imports { // imports customizer
normal ‘my.package.MyClass’ // a normal import alias ‘AI’, ‘java.util.concurrent.atomic.AtomicInteger’ // an aliased import star ‘java.util.concurrent’ // star imports staticMember ‘java.lang.Math’, ‘PI’ // static import staticMember ‘pi’, ‘java.lang.Math’, ‘PI’ // aliased static import

}

} 6.6.2. AST transformation customizer

withConfig(conf) {
ast(Log)

}

withConfig(conf) {
ast(Log, value: ‘LOGGER’)

} apply @Log transparently apply @Log with a different name for the logger 6.6.3. Secure AST customizer

withConfig(conf) {
secureAst {
closuresAllowed = false methodDefinitionAllowed = false

}

} 6.6.4. Source aware customizer

withConfig(configuration){
source(extension: ‘sgroovy’) {
ast(CompileStatic)

}

}

withConfig(configuration){
source(extensions: [‘sgroovy’,’sg’]) {
ast(CompileStatic)

}

}

withConfig(configuration) {
source(extensionValidator: { it.name in [‘sgroovy’,’sg’]}) {
ast(CompileStatic)

}

}

withConfig(configuration) {
source(basename: ‘foo’) {
ast(CompileStatic)

}

}

withConfig(configuration) {
source(basenames: [‘foo’, ‘bar’]) {
ast(CompileStatic)

}

}

withConfig(configuration) {
source(basenameValidator: { it in [‘foo’, ‘bar’] }) {
ast(CompileStatic)

}

}

withConfig(configuration) {
source(unitValidator: { unit -> !unit.AST.classes.any { it.name == ‘Baz’ } }) {
ast(CompileStatic)

}

} apply CompileStatic AST annotation on .sgroovy files apply CompileStatic AST annotation on .sgroovy or .sg files apply CompileStatic AST annotation on files whose name is ‘foo’ apply CompileStatic AST annotation on files whose name is ‘foo’ or ‘bar’ apply CompileStatic AST annotation on files that do not contain a class named ‘Baz’ 6.6.5. Inlining a customizer

Inlined customizer allows you to write a compilation customizer directly, without having to create a class for it.

withConfig(configuration) {
inline(phase:’CONVERSION’) { source, context, classNode ->
println “visiting $classNode”

}

} define an inlined customizer which will execute at the CONVERSION phase prints the name of the class node being compiled 6.6.6. Multiple customizers

Of course, the builder allows you to define multiple customizers at once:

withConfig(configuration) {
ast(ToString) ast(EqualsAndHashCode)

} 6.7. Config script flag

So far, we have described how you can customize compilation using a CompilationConfiguration class, but this is only possible if you embed Groovy and that you create your own instances of CompilerConfiguration (then use it to create a GroovyShell, GroovyScriptEngine, …).

If you want it to be applied on the classes you compile with the normal Groovy compiler (that is to say with groovyc, ant or gradle, for example), it is possible to use a compilation flag named configscript that takes a Groovy configuration script as argument.

This script gives you access to the CompilerConfiguration instance before the files are compiled (exposed into the configuration script as a variable named configuration), so that you can tweak it.

It also transparently integrates the compiler configuration builder above. As an example, let’s see how you would activate static compilation by default on all classes.

6.7.1. Static compilation by default

Normally, classes in Groovy are compiled with a dynamic runtime. You can activate static compilation by placing an annotation named @CompileStatic on any class. Some people would like to have this mode activated by default, that is to say not having to annotated classes. Using configscript, this is possible. First of all, you need to create a file named config.groovy into src/conf with the following contents:

withConfig(configuration) {
ast(groovy.transform.CompileStatic)

} configuration references a CompilerConfiguration instance That is actually all you need. You don’t have to import the builder, it’s automatically exposed in the script. Then, compile your files using the following command line:

groovyc -configscript src/conf/config.groovy src/main/groovy/MyClass.groovy We strongly recommend you to separate configuration files from classes, hence why we suggest using the src/main and src/conf directories above.

6.8. AST transformations

If:

runtime metaprogramming doesn’t allow you do do what you want

you need to improve the performance of the execution of your DSLs

you want to leverage the same syntax as Groovy but with different semantics

you want to improve support for type checking in your DSLs

Then AST transformations are the way to go. Unlike the techniques used so far, AST transformations are meant to change or generate code before it is compiled to bytecode. AST transformations are capable of adding new methods at compile time for example, or totally changing the body of a method based on your needs. They are a very powerful tool but also come at the price of not being easy to write. For more information about AST transformations, please take a look at the compile-time metaprogramming section of this manual.

  1. Custom type checking extensions

It may be interesting, in some circumstances, to provide feedback about wrong code to the user as soon as possible, that is to say when the DSL script is compiled, rather than having to wait for the execution of the script. However, this is not often possible with dynamic code. Groovy actually provides a practical answer to this known as type checking extensions.

  1. Builders

(TBD)

8.1. Creating a builder

(TBD)

8.1.1. BuilderSupport

(TBD)

8.1.2. FactoryBuilderSupport

(TBD)

8.2. Existing builders

(TBD)

8.2.1. MarkupBuilder

See Creating Xml - MarkupBuilder.

8.2.2. StreamingMarkupBuilder

See Creating Xml - StreamingMarkupBuilder.

8.2.3. SaxBuilder

A builder for generating Simple API for XML (SAX) events.

If you have the following SAX handler:

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, “

}

} You can use SaxBuilder to generate SAX events for the handler like this:

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

builder.root() {
helloWorld()

} And then check that everything worked as expected:

assert handler.log == ‘Start Element: root, Start Element: helloWorld, End Element: helloWorld, End Element: root, ‘ 8.2.4. StaxBuilder

A Groovy builder that works with Streaming API for XML (StAX) processors.

Here is a simple example using the StAX implementation of Java to generate 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>’ An external library such as Jettison can be used as follows:

@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

A builder for parsing HTML, XHTML and XML into a W3C DOM tree.

For example this 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>’‘’

Can be parsed into a DOM tree with a DOMBuilder like this:

def reader = new StringReader(recordsXML) def doc = groovy.xml.DOMBuilder.parse(reader) And then processed further e.g. by using DOMCategory:

def records = doc.documentElement use(groovy.xml.dom.DOMCategory) {

assert records.car.size() == 3

} 8.2.6. NodeBuilder

NodeBuilder is used for creating nested trees of Node objects for handling arbitrary data. To create a simple user list you use a NodeBuilder like this:

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’)

} Now you can process the data further, e.g. by using GPath expressions:

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

Groovys JsonBuilder makes it easy to create Json. For example to create this Json string:

String carRecords = ‘’‘
{

“records”: { “car”: {

“name”: “HSV Maloo”, “make”: “Holden”, “year”: 2006, “country”: “Australia”, “record”: {

“type”: “speed”, “description”: “production pickup truck with speed of 271kph”

}

}

}

}

‘’’ you can use a JsonBuilder like this:

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()) We use JsonUnit to check that the builder produced the expected result:

JsonAssert.assertJsonEquals(json, carRecords) 8.2.8. StreamingJsonBuilder

Unlike JsonBuilder which creates a data structure in memory, which is handy in those situations where you want to alter the structure programmatically before output, StreamingJsonBuilder directly streams to a writer without any intermediate memory data structure. If you do not need to modify the structure and want a more memory-efficient approach, use StreamingJsonBuilder.

The usage of StreamingJsonBuilder is similar to JsonBuilder. In order to create this Json string:

String carRecords = ‘’‘
{

“records”: { “car”: {

“name”: “HSV Maloo”, “make”: “Holden”, “year”: 2006, “country”: “Australia”, “record”: {

“type”: “speed”, “description”: “production pickup truck with speed of 271kph”

}

}

}

}

‘’’ you use a StreamingJsonBuilder like this:

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()) We use JsonUnit to check the expected result:

JsonAssert.assertJsonEquals(json, carRecords) 8.2.9. SwingBuilder

SwingBuilder allows you to create full-fledged Swing GUIs in a declarative and concise fashion. It accomplishes this by employing a common idiom in Groovy, builders. Builders handle the busywork of creating complex objects for you, such as instantiating children, calling Swing methods, and attaching these children to their parents. As a consequence, your code is much more readable and maintainable, while still allowing you access to the full range of Swing components.

Here’s a simple example of using 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)

}

} Here is what it will look like:

SwingBuilder001 This hierarchy of components would normally be created through a series of repetitive instantiations, setters, and finally attaching this child to its respective parent. Using SwingBuilder, however, allows you to define this hierarchy in its native form, which makes the interface design understandable simply by reading the code.

The flexibility shown here is made possible by leveraging the many programming features built-in to Groovy, such as closures, implicit constructor calling, import aliasing, and string interpolation. Of course, these do not have to be fully understood in order to use SwingBuilder; as you can see from the code above, their uses are intuitive.

Here is a slightly more involved example, with an example of SwingBuilder code re-use via a closure.

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())

}

}

} Here’s another variation that relies on observable beans and binding:

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 is one of the core AST Transformations. It generates all the required boilerplate code to turn a simple bean into an observable one. The bind() node creates appropriate PropertyChangeListeners that will update the interested parties whenever a PropertyChangeEvent is fired.

8.2.10. AntBuilder

Despite being primarily a build tool, Apache Ant is a very practical tool for manipulating files including zip files, copy, resource processing, …​ But if ever you’ve been working with a build.xml file or some Jelly script and found yourself a little restricted by all those pointy brackets, or found it a bit weird using XML as a scripting language and wanted something a little cleaner and more straight forward, then maybe Ant scripting with Groovy might be what you’re after.

Groovy has a helper class called AntBuilder which makes the scripting of Ant tasks really easy; allowing a real scripting language to be used for programming constructs (variables, methods, loops, logical branching, classes etc). It still looks like a neat concise version of Ant’s XML without all those pointy brackets; though you can mix and match this markup inside your script. Ant itself is a collection of jar files. By adding them to your classpath, you can easily use them within Groovy as is. We believe using AntBuilder leads to more concise and readily understood syntax.

AntBuilder exposes Ant tasks directly using the convenient builder notation that we are used to in Groovy. Here is the most basic example, which is printing a message on the standard output:

def ant = new AntBuilder() ant.echo(‘hello from Ant!’) creates an instance of AntBuilder executes the echo task with the message in parameter Imagine that you need to create a ZIP file:

def ant = new AntBuilder() ant.zip(destfile: ‘sources.zip’, basedir: ‘src’) In the next example, we demonstrate the use of AntBuilder to copy a list of files using a classical Ant pattern directly in Groovy:

// lets just call one task ant.echo(“hello”)

// here is an example of a block of Ant inside GroovyMarkup ant.sequential {

echo(“inside sequential”) def myDir = “target/AntTest/” mkdir(dir: myDir) copy(todir: myDir) {

fileset(dir: “src/test”) {
include(name: “**/*.groovy”)

}

} echo(“done”)

}

// now lets do some normal Groovy again def file = new File(ant.project.baseDir,”target/AntTest/groovy/util/AntTest.groovy”) assert file.exists() Another example would be iterating over a list of files matching a specific pattern:

// lets create a scanner of filesets def scanner = ant.fileScanner {

fileset(dir:”src/test”) {
include(name:”**/Ant*.groovy”)

}

}

// now lets iterate over def found = false for (f in scanner) {

println(“Found file $f”) found = true assert f instanceof File assert f.name.endsWith(”.groovy”)

} assert found Or execute a JUnit test:

// lets create a scanner of filesets ant.junit {

test(name:’groovy.util.SomethingThatDoesNotExist’)

} We can even go further by compiling and executing a Java file directly from Groovy:

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’) It is worth mentioning that AntBuilder is included in Gradle, so you can use it in Gradle just like you would in Groovy. Additional documentation can be found in the Gradle manual.

8.2.11. CliBuilder

(TBD)

8.2.12. ObjectGraphBuilder

ObjectGraphBuilder is a builder for an arbitrary graph of beans that follow the JavaBean convention. It is in particular useful for creating test data.

Let’s start with a list of classes that belong to your domain:

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

} Then using ObjectGraphBuilder building a Company with three employees is as easy as:

def builder = new ObjectGraphBuilder() builder.classLoader = this.class.classLoader builder.classNameResolver = “com.acme”

def acme = builder.company(name: ‘ACME’) {
3.times {
employee(id: it.toString(), name: “Drone $it”) {
address(line1:”Post street”)

}

}

}

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 creates a new object graph builder sets the classloader where the classes will be resolved sets the base package name for classes to be resolved creates a Company instance with 3 Employee instances each of them having a distinct Address Behind the scenes, the object graph builder:

will try to match a node name into a Class, using a default ClassNameResolver strategy that requires a package name

then will create an instance of the appropriate class using a default NewInstanceResolver strategy that calls a no-arg constructor

resolves the parent/child relationship for nested nodes, involving two other strategies:

RelationNameResolver will yield the name of the child property in the parent, and the name of the parent property in the child (if any, in this case, Employee has a parent property aptly named company)

ChildPropertySetter will insert the child into the parent taking into account if the child belongs to a Collection or not (in this case employees should be a list of Employee instances in Company).

All 4 strategies have a default implementation that work as expected if the code follows the usual conventions for writing JavaBeans. In case any of your beans or objects do not follow the convention you may plug your own implementation of each strategy. For example imagine that you need to build a class which is immutable:

@Immutable class Person {

String name int age

} Then if you try to create a Person with the builder:

def person = builder.person(name:’Jon’, age:17) It will fail at runtime with:

Cannot set readonly property: name for class: com.acme.Person Fixing this can be done by changing the new instance strategy:

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

} klazz.newInstance()

} ObjectGraphBuilder supports ids per node, meaning that you can store a reference to a node in the builder. This is useful when multiple objects reference the same instance. Because a property named id may be of business meaning in some domain models ObjectGraphBuilder has a strategy named IdentifierResolver that you may configure to change the default name value. The same may happen with the property used for referencing a previously saved instance, a strategy named ReferenceResolver will yield the appropriate value (default is `refId’):

def company = builder.company(name: ‘ACME’) {

address(id: ‘a1’, line1: ‘123 Groovy Rd’, zip: 12345, state: ‘JV’) employee(name: ‘Duke’, employeeId: 1, address: a1) employee(name: ‘John’, employeeId: 2 ){

address( refId: ‘a1’ )

}

} an address can be created with an id an employee can reference the address directly with its id or use the refId attribute corresponding to the id of the corresponding address Its worth mentioning that you cannot modify the properties of a referenced bean.

8.2.13. JmxBuilder

See Working with JMX - JmxBuilder for details.

8.2.14. FileTreeBuilder

FileTreeBuilder is a builder for generating a file directory structure from a specification. For example, to create the following tree:

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

You can use a FileTreeBuilder like this:

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 {}’)

}

}

}

To check that everything worked as expected we use the following `assert`s:

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 also supports a shorthand syntax:

tmpDir = File.createTempDir() def fileTreeBuilder = new FileTreeBuilder(tmpDir) fileTreeBuilder.src {

main {
groovy {
‘Foo.groovy’(‘println “Hello”’)

}

} test {

groovy {
‘FooTest.groovy’(‘class FooTest extends GroovyTestCase {}’)

}

}

}

This produces the same directory structure as above, as shown by these `assert`s:

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 {}’

应用集成 Groovy

Groovy 集成机制

Groovy 语言提供了几种在运行时集成至应用程序中的方法,包括从最基础的代码执行,到集成缓存及自定义编译器。 本章节的实例使用 Groovy 编写,其同样集成机制也适用于 Java 。

Eval

groovy.util.Eval 类在运行时,动态执行 Groovy 最简单的方式。可以直接通过方法 me 实现调用:

import groovy.util.Eval

assert Eval.me('33*3') == 99
assert Eval.me('"foo".toUpperCase()') == 'FOO'

Eval 支持接受参数的多变种执行:

assert Eval.x(4, '2*x') == 8               //<1>
assert Eval.me('k', 4, '2*k') == 8         //<2>
assert Eval.xy(4, 5, 'x*y') == 20          //<3>
assert Eval.xyz(4, 5, 6, 'x*y+z') == 26    //<4>

<1> 绑定参数命名为 x

<2> 自定义绑定参数命名为 k

<3> 两个绑定参数分别命名为 x 和 y

<4> 三个绑定参数分别命名为 x , y 和 z

Eval 可以执行简单脚本,对于复杂的脚本将无能为力:由于其无法缓存脚本,但并不意味着只能执行单行脚本。

GroovyShell
Multiple sources

groovy.lang.GroovyShell 适用于需要缓存脚本实例的执行过程。 Eval 类可以返回脚本执行结果, GroovyShell 类可以提供更多选择。

def shell = new GroovyShell()                                                   //<1>
def result = shell.evaluate '3*5'                               //<2>
def result2 = shell.evaluate(new StringReader('3*5'))           //<3>
assert result == result2
def script = shell.parse '3*5'                              //<4>
assert script instanceof groovy.lang.Script
assert script.run() == 15                                                               //<5>

<1> 创建一个 GroovyShell 实例

<2> 与 Eval 一样可以直接执行代码

<3> 可以从多个数据源读取数据 (String, Reader, File, InputStream)

<4> 可以延缓脚本执行。parse 返回 Script 实例

<5> Script 定义了一个 run 方法

Sharing data between a script and the application

使用 groovy.lang.Binding 可以在应用程序与脚本之间共享数据:

def sharedData = new Binding()                                                  //<1>
def shell = new GroovyShell(sharedData)                         //<2>
def now = new Date()
sharedData.setProperty('text', 'I am shared data!')         //<3>
sharedData.setProperty('date', now)                             //<4>

String result = shell.evaluate('"At $date, $text"')             //<5>

assert result == "At $now, I am shared data!"

<1> 创建 Binding 对象,用于存储共享数据

<2> 创建 GroovyShell 对象,使用共享数据

<3> 添加字符串数据共享绑定

<4> 添加一个 date 类型数据绑定(共享数据不局限于基本数据类型)

<5> 执行脚本

注解

脚本中也将数据写入共享数据对象。

def sharedData = new Binding()                          //<1>
def shell = new GroovyShell(sharedData)         //<2>

shell.evaluate('foo=123')                       //<3>

assert sharedData.getProperty('foo') == 123     //<4>

<1> 创建 Binding 对象

<2> 创建 GroovyShell 对象,使用共享数据

<3> 使用未声明的变量存储数据结果至共享数据对象中

<4> 读取共享结果数据

从脚本中写入的共享数据,必须为未声明的变量。像下面例子中,使用定义或明确类型将会失败,其原因是这样你就已定义了一个本地变量:

def sharedData = new Binding()
def shell = new GroovyShell(sharedData)

shell.evaluate('int foo=123')

try {
    assert sharedData.getProperty('foo')
} catch (MissingPropertyException e) {
    println "foo is defined as a local variable"
}

在多线程环境中使用共享数据,需要更加的小心。Binding 实例传递给 GroovyShell 是非线程安全的,其在所有脚本中共享。

It is possible to work around the shared instance of Binding by leveraging the Script instance which is returned by parse:

def shell = new GroovyShell()

def b1 = new Binding(x:3)                               //<1>
def b2 = new Binding(x:4)                   //<2>
def script = shell.parse('x = 2*x')
script.binding = b1
script.run()
script.binding = b2
script.run()
assert b1.getProperty('x') == 6
assert b2.getProperty('x') == 8
assert b1 != b2

<1> will store the x variable inside b1

<2> will store the x variable inside b2

然而,你需要注意的是你仍旧在同一个共享的脚本实例上。如果有两个线程在同一个脚本上工作,这项技术将不可使用。在那种情况下,就必须创建两个独立的脚本实例:

def shell = new GroovyShell()

def b1 = new Binding(x:3)
def b2 = new Binding(x:4)
def script1 = shell.parse('x = 2*x')                            //<1>
def script2 = shell.parse('x = 2*x')                    //<2>
assert script1 != script2
script1.binding = b1                                    //<3>
script2.binding = b2                                    //<4>
def t1 = Thread.start { script1.run() }                 //<5>
def t2 = Thread.start { script2.run() }                 //<6>
[t1,t2]*.join()                                         //<7>
assert b1.getProperty('x') == 6
assert b2.getProperty('x') == 8
assert b1 != b2

<1> 为 thread 1 创建 script 实例

<2> 为 thread 2 创建 script 实例

<3> 第一个共享对象分配给 script 1

<4> 第二个共享对象分配给 script 2

<5> 在 thread 1 中启动 script 1

<6> 在 thread 2 中启动 script 2

<7> 等待执行结束

如果你需要线程安全,建议直接使用 GroovyClassLoader

Custom script class

我们已经知道 parse 方法返回 groovy.lang.Script 实例,还能够通过自定义扩展 Script 类来增强 script 的处理能力,例如:

abstract class MyScript extends Script {
    String name

    String greet() {
        "Hello, $name!"
    }
}

扩展类自定义了一个 name 属性以及 greet 方法。通过配置这个类可以按照 script 类方式使用。

import org.codehaus.groovy.control.CompilerConfiguration

def config = new CompilerConfiguration()                                             // <1>
config.scriptBaseClass = 'MyScript'                                                  // <2>

def shell = new GroovyShell(this.class.classLoader, new Binding(), config)           // <3>
def script = shell.parse('greet()')                                                  // <4>
assert script instanceof MyScript
script.setName('Michel')
assert script.run() == 'Hello, Michel!'

<1> 创建 CompilerConfiguration 实例

<2> 指定 scripts 执行的基类名称为 MyScript

<3> 创建 shell 是指定当前定义的编译器配置信息

<4> 此 script 可以访问 greet 方法

并不限于使用唯一的 scriptBaseClass 配置。可以使用任意多个编译器配置,甚至自定义。

GroovyClassLoader

前面章节,我看到 GroovyShell 是用来执行 scripts 的一个简单工具。在其内部,它使用 groovy.lang.GroovyClassLoader 在运行时编译并加载 classes.

通过使用 GroovyClassLoader 替代 GroovyShell ,你可以加载 classes 替代 scripts 实例。

import groovy.lang.GroovyClassLoader

def gcl = new GroovyClassLoader()                                                                                                       // <1>
def clazz = gcl.parseClass('class Foo { void doIt() { println "ok" } }')                                // <2>
assert clazz.name == 'Foo'                                                                              // <3>
def o = clazz.newInstance()                                                                             // <4>
o.doIt()                                                                                                // <5>

<1> create a new GroovyClassLoader

<2> parseClass will return an instance of Class

<3> you can check that the class which is returns is really the one defined in the script

<4> and you can create a new instance of the class, which is not a script

<5> then call any method on it

GroovyClassLoader 中维护它创建的所有 classes , 所有存储溢出会比较容易出现。尤其是, 当你执行两次相同的脚本,你将得到两个不同的类!

import groovy.lang.GroovyClassLoader

def gcl = new GroovyClassLoader()
def clazz1 = gcl.parseClass('class Foo { }')                    // <1>
def clazz2 = gcl.parseClass('class Foo { }')                     // <2>
assert clazz1.name == 'Foo'                                                                     // <3>
assert clazz2.name == 'Foo'
assert clazz1 != clazz2                                         // <4>

<1> dynamically create a class named “Foo”

<2> create an identical looking class, using a separate parseClass call

<3> make sure both classes have the same name

<4> but they are actually different!

如果你希望获得相同的实例, 源数据必须为文件,例如:

def gcl = new GroovyClassLoader()
def clazz1 = gcl.parseClass(file)                                                               // <1>
def clazz2 = gcl.parseClass(new File(file.absolutePath))                // <2>
assert clazz1.name == 'Foo'                                             // <3>
assert clazz2.name == 'Foo'
assert clazz1 == clazz2                                                                                         // <4>

<1> parse a class from a File

<2> parse a class from a distinct file instance, but pointing to the same physical file

<3> make sure our classes have the same name

<4> but now, they are the same instance

使用文件作为数据源, GroovyClassLoader 可以缓存生成的 class 文件, 这样可以避免在运行时创建多个类从单一源中。 Using a File as input, the GroovyClassLoader is capable of caching the generated class file, which avoids creating multiple classes at runtime for the same source.

GroovyScriptEngine

groovy.util.GroovyScriptEngine 依靠 script 重载和依赖,为应用的灵活扩展提供基础设施。GroovyShell 关注独立的 ScriptGroovyClassLoader 用于动态编译及加载 Groovy 类, GroovyScriptEngineGroovyClassLoader 之上建立一层用于处理 script 的依赖及重新加载。

为说明这点,我们将创建一个 script engine 并在无限循环中执行代码。 首先创建下面代码:

ReloadingTest.groovy

class Greeter {
    String sayHello() {
        def greet = "Hello, world!"
        greet
    }
}

new Greeter()

然后使用 GroovyScriptEngine 执行代码:

def binding = new Binding()
def engine = new GroovyScriptEngine([tmpDir.toURI().toURL()] as URL[])             // <1>

while (true) {
    def greeter = engine.run('ReloadingTest.groovy', binding)                     // <2>
    println greeter.sayHello()                                                    // <3>
    Thread.sleep(1000)
}

<1> create a script engine which will look for sources into our source directory

<2> execute the script, which will return an instance of Greeter

<3> print the greeting message

每秒钟将看到下面的打印信息:

Hello, world!
Hello, world!
...

在不终止 script 执行,修改 ReloadingTest 文件,如:

//ReloadingTest.groovy
class Greeter {
    String sayHello() {
        def greet = "Hello, Groovy!"
        greet
    }
}
new Greeter()

打印内容将会变化:

Hello, world!
...
Hello, Groovy!
Hello, Groovy!
...

下面将演示 script 的依赖,新建下面文件,同样不中断 script 的执行:

//Depencency.groovy
class Dependency {
    String message = 'Hello, dependency 1'
}

更新 ReloadingTest 文件:

//ReloadingTest.groovy
import Dependency

class Greeter {
    String sayHello() {
        def greet = new Dependency().message
        greet
    }
}

new Greeter()

打印内容将会有如下变化:

Hello, Groovy!
...
Hello, dependency 1!
Hello, dependency 1!
...

你可以更新 Dependency.groovy 文件:

//Depencency.groovy
class Dependency {
    String message = 'Hello, dependency 2'
}

你将看到 dependency 文件被从新加载:

Hello, dependency 1!
...
Hello, dependency 2!
Hello, dependency 2!
CompilationUnit

Ultimately, it is possible to perform more operations during compilation by relying directly on the org.codehaus.groovy.control.CompilationUnit class. This class is responsible for determining the various steps of compilation and would let you introduce new steps or even stop compilation at various phases. This is for example how stub generation is done, for the joint compiler.

However, overriding CompilationUnit is not recommended and should only be done if no other standard solution works.

Bean Scripting Framework

Bean Scripting Framework 用于创建 Java 调用脚本语言的 APIBSF 已经有很长时间没有更新,并且在 JSR-223 中已经废弃。

Groovy 中的 BSF 引擎使用 org.codehaus.groovy.bsf.GroovyEngine 实现。事实上, BSF APIs 已经将其隐藏。通过 BSF API 使用 Groovy 和其他脚本语言一样。

由于 Groovy 其原生的支持 Java ,你所需要关心的 BSF 如何调用其他语言,例如 : JRuby

Getting started

GroovyBSFjars 加入到 classpath。你可以像下面 Java 代码中调用 Groovy 脚本:

String myScript = "println('Hello World')\n  return [1, 2, 3]";
BSFManager manager = new BSFManager();
List answer = (List) manager.eval("groovy", "myScript.groovy", 0, 0, myScript);
assertEquals(3, answer.size());
传递参数

BSF 可以在 Java 与脚本语言中传递参数。你可以在 BSF 中注册/注销 beans,之后可以在 BSF 方法中调用。注册的内容可以直接在脚本中使用。 例如:

BSFManager manager = new BSFManager();
manager.declareBean("xyz", 4, Integer.class);
Object answer = manager.eval("groovy", "test.groovy", 0, 0, "xyz + 1");
assertEquals(5, answer);
Other calling options

前面例子中使用 eval 方法。BSF 中有多种方法可以使用,详细可以查看 BSF 文档 。这里介绍另一个方法 apple,其可以使用脚本语言定义匿名函数,并使用其参数。Groovy 上可以使用闭包支持这种函数,如下:

BSFManager manager = new BSFManager();
Vector<String> ignoreParamNames = null;
Vector<Integer> args = new Vector<Integer>();
args.add(2);
args.add(5);
args.add(1);
Integer actual = (Integer) manager.apply("groovy", "applyTest", 0, 0,
        "def summer = { a, b, c -> a * 100 + b * 10 + c }", ignoreParamNames, args);
assertEquals(251, actual.intValue());
Access to the scripting engine

BSF 中提供勾子,用于直接获取脚本引擎。如下:

BSFManager manager = new BSFManager();
BSFEngine bsfEngine = manager.loadScriptingEngine("groovy");
manager.declareBean("myvar", "hello", String.class);
Object myvar = manager.lookupBean("myvar");
String result = (String) bsfEngine.call(myvar, "reverse", new Object[0]);
assertEquals("olleh", result);
JSR 223 javax.script API

JSR-223Java 中调用脚本语言框架的标准接口。从 Java 6 开始,其目标是为了提供一套通用框架来调用脚本语言。 Groovy 提供了丰富的集成机制,我们也建议使用 Groovy 集成机制替代 JSR-223 API

这里有关于使用 JSR-223 引擎的实例:

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
...
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("groovy");

接着下面可以开始执行 Groovy 脚本: Then you can execute Groovy scripts easily:

Integer sum = (Integer) engine.eval("(1..10).sum()");
assertEquals(new Integer(55), sum);

其中也可以共享变量:

engine.put("first", "HELLO");
engine.put("second", "world");
String result = (String) engine.eval("first.toLowerCase() + ' ' + second.toUpperCase()");
assertEquals("hello WORLD", result);

下面演示调用可执行方法:

import javax.script.Invocable;
...
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("groovy");
String fact = "def factorial(n) { n == 1 ? 1 : n * factorial(n - 1) }";
engine.eval(fact);
Invocable inv = (Invocable) engine;
Object[] params = {5};
Object result = inv.invokeFunction("factorial", params);
assertEquals(new Integer(120), result);

Groovy 中的设计模式 (TBD)

编码风格

Java 开发人员在开始学习 Groovy 时通常都会保持着 Java 的思想,但随着逐步的学习 Groovy ,一步一个脚印,会更加熟练编写 Groovy。 本章节主要为这样的读者服务,介绍 Groovy 的一些通用的句法,操作以及一些新的特性,例如:闭包。

无需分号

C / C++ / C# / Java 中无处不在的使用的着分号。 Groovy 可以支持 99% 的 Java 语法,甚至可以直接将 Java 代码复制到 Groovy 中使用,这样你会看到到处都是分号。在 Groovy 中分号不是必须,你可以 忽略它们,当然最好是直接删除掉。

return 为可选

Groovy 可以无需 return 关键字,方法中最后一个表达式将作为返回值。 在简短的方法或闭包中,这种方式非常的友好:

String toString() { return "a server" }
String toString() { "a server" }

当使用变量作为返回时,有时的体验就并不是很好:

def props() {
    def m1 = [a: 1, b: 2]
    m2 = m1.findAll { k, v -> v % 2 == 0 }
    m2.c = 3
    m2
}

在这种情况下,在最后一个表达式之前添加一行,或明确的使用 return 将会有更好的可读性。 对于我自己而言有时使用 return 有时不使用,这完全属于自己的喜好。但是在闭包中,我们更多情况下是不使用 return 。 尽管 return 作为关键字是可选的,但也并不意味着强制不可使用,这还是完全取决于你代码的可读性。

这里需要注意的是,当使用 def 来定义方法的具体类型,方法的返回取决于其最后的表达式的返回值。 通常更倾向于使用具体的返回类型,如 : void 或 类型。 在上面例子中,想象如果我们忘记写最后一行 m2,这里最后一行的表达式将是 m2.c = 3 , 其放回值就会是 3,而不是你所期望的 map.

Statements like if/else, try/catch can thus return a value as well, as there’s a “last expression” evaluated in those statements:

def foo(n) {
    if(n == 1) {
        "Roshan"
    } else {
        "Dawrani"
    }
}

assert foo(1) == "Roshan"
assert foo(2) == "Dawrani"

Def and type

当我们谈论到 deftypes, 我们会看到开发人员同时使用 def 和响应的类型。但是在这里 def 是多余的。你可以选择是使用 def 或 具体类型。

不要这样写:

def String name = "Guillaume"

可以写成:

String name = "Guillaume"

当使用 def 时,其实际对应 Object 类型(这样你可以使用 def 定义任何变量,如果方法声明返回 def ,其可以返回任意类型对象)。

对于方法中无类型的参数,我们可以使用 def 定义,虽然这也不是必须的,并且我们更倾向于忽略类型:

例如:

void doSomething(def param1, def param2) { }

我们更喜欢写成:

void doSomething(param1, param2) { }

上一章节我们提到过,明确方法参数的类型是比较好的,可以帮助完善代码的记录, IDEs 的代码补全,以及有利用 Groovy 中静态类型检查及静态编译的能力。

当在定义构造方法时, def 是应该避免使用的:

class MyClass {
    def MyClass() {}
}

应当删除 def:

class MyClass {
    MyClass() {}
}

默认为 Public (Public by default)

Groovyclasses 和方法都默认为 public . 你可以不必使用 public 修饰符,如果其不为 public 可以使用其他修饰符:

例如:

public class Server {
    public String toString() { return "a server" }
}

可以替换为:

class Server {
    String toString() { "a server" }
}

你可能希望可见范围在 package-scope ,我们可以使用注解来满足所期望的可见性:

class Server {
    @PackageScope Cluster cluster
}

省略括号

Groovy 允许在顶级表达式中省略括号:

println "Hello"
method a, b
//      vs:

println("Hello")
method(a, b)

当使用闭包作为方法的最后一个参数时,就像 each{} 迭代调用,你可以将闭包放置在右括号之外,甚至省略括号:

list.each( { println it } )
list.each(){ println it }
list.each  { println it }

第三种形式表达更自然,空括号带来无用的句法噪音。

在一些情况下是不可以参数括号。像前面提到的,在顶层的表达式中可以省略括号,但是在嵌套方法调用,赋值的右侧,其括号是不可以省略的:

def foo(n) { n }

println foo 1 // won't work
def m = foo 1

Classes as first-class citizens

.classes 的后缀在 Groovy 中并非必须,有点像 Java 中的 instanceof。

例如:

connection.doPost(BASE_URI + "/modify.hqu", params, ResourcesResponse.class)

Using GStrings we’re going to cover below, and using first class citizens:

connection.doPost("${BASE_URI}/modify.hqu", params, ResourcesResponse)

Getters and Setters

Groovy 中使用 getterssetters 来调用属性,并提供简单符号访问及修改属性。 替代 Java 中使用 getters / setters 的方式,你可以直接访问属性:

resourceGroup.getResourcePrototype().getName() == SERVER_TYPE_NAME
resourceGroup.resourcePrototype.name == SERVER_TYPE_NAME

resourcePrototype.setName("something")
resourcePrototype.name = "something"

Groovy 中编写 beans,其被称为 POGOs (Plain Old Groovy Objects) , 你可以无需自己创建属性的 getter / setter , Groovy 编译器会为 你完成这些:

class Person {
    private String name
    String getName() { return name }
    void setName(String name) { this.name = name }
}

你可以这么写:

class Person {
    String name
}

就像你看到的,一个不带访问修饰符的 fieldGroovy 编译器会为其生成 private field 及 getter 和 setter 方法。

When using such POGOs from Java, the getter and setter are indeed there, and can be used as usual, of course.

尽管编译可以创建 getter/setter 逻辑,如果你需要扩展这些 getter/setter , 你可以重写这些方法,其编译器将会使用你的重构逻辑替代默认的。

使用命名参数初始化 beans

With a bean like:

class Server {
    String name
    Cluster cluster
}

替代调用 setter 方法,我们可以像下面这样:

def server = new Server()
server.name = "Obelix"
server.cluster = aCluster

你可以使用默认的构造函数为参数赋值(首先调用构造方法,通过指定的 map 内容,依次调用对应的 setter 方法)

def server = new Server(name: "Obelix", cluster: aCluster)

Using with() for repeated operations on the same bean

当创建实例时,使用构造函数对变量进行赋值是很容易,但是当你需要更新实例中的变量,你会不断的重复使用 server 前缀,对吗? 不过现在你可以感谢 with() 方法,它将省去前缀的重复,它被引入到 Groovy 中所的对象中:

server.name = application.name
server.status = status
server.sessionCount = 3
server.start()
server.stop()

vs:

server.with {
    name = application.name
    status = status
    sessionCount = 3
    start()
    stop()
}

Equals and ==

Java’s == 就是 Groovy’s is() , Groovy’s == 较于 equals() 更强大。

比较对象的引用,你可以 a.is(b) 替代 ==

通常使用 equals() 进行比较,在 Groovy 更多使用 ==,其可以很好的避免 NullPointerException:

Instead of:

status != null && status.equals(ControlConstants.STATUS_COMPLETED)

Do:

status == ControlConstants.STATUS_COMPLETED

GStrings (插值,多行赋值)

在 Java 中我们使用字符串及多变量结合,会使用大量的双引号,加号以及 \n 字符用于换行。

比较以下方式:

throw new Exception("Unable to convert resource: " + resource)

vs:

throw new Exception("Unable to convert resource: ${resource}")

在花括号中,你可以添加任何表达式,而不仅仅是变量。 对于简单的变量或 variable.property ,你甚至可以不使用花括号:

throw new Exception("Unable to convert resource: $resource")

你甚至可以延迟执行表达式,使用闭包的符号 ${-> resource} . 当 GString 被转化为字符串时,开始执行闭包并返回 toString

Example:

int i = 3

def s1 = "i's value is: ${i}"
def s2 = "i's value is: ${-> i}"

i++

assert s1 == "i's value is: 3" // eagerly evaluated, takes the value on creation
assert s2 == "i's value is: 4" // lazily evaluated, takes the new value into account

在 Java 中字符串及其连接表达式非常冗长:

throw new PluginException("Failed to execute command list-applications:" +
    " The group with name " +
    parameterMap.groupname[0] +
    " is not compatible group of type " +
    SERVER_TYPE_NAME)

You can use the continuation character (this is not a multiline string): 你可以使用 \ 延续字符(这里并不是多行字符串):

throw new PluginException("Failed to execute command list-applications: \
The group with name ${parameterMap.groupname[0]} \
is not compatible group of type ${SERVER_TYPE_NAME}")

或使用三引号来使用多行字符串:

throw new PluginException("""Failed to execute command list-applications:
    The group with name ${parameterMap.groupname[0]}
    is not compatible group of type ${SERVER_TYPE_NAME)}""")

你可以使用 .stripIndent() 删除多行字符串左侧缩进。

请注意 Groovy 中单引号与双引号的差别: 单引号创建 Java 字符串,不允许插入变量,双引号在插入变量时创建 GStrings ,否则创建 Java 字符串。

你可使用三引号来创建多行字符串:三双引号用于 GStrings ,三单引号用于 Strings.

如果你需要使用正则表达式,你需要使用 slashy 符号:

assert "foooo/baaaaar" ==~ /fo+\/ba+r/

slashy 的优势不使用双反斜杠,并使正则表达式使用更为简单。 (The advantage of the “slashy” notation is that you don’t need to double escape backslashes, making working with regex a bit simpler.)

通常使用单引号来创建字符串常量,使用双引号来处理需要插值字符串。

Native syntax for data structures

Groovy 提供原生句法构建数据结构,如:lists, maps, regex, ranges。 请确保在编程过程中是这么使用的。

这里有一些例子:

def list = [1, 4, 6, 9]

// by default, keys are Strings, no need to quote them
// you can wrap keys with () like [(variableStateAcronym): stateName] to insert a variable or object as a key.
def map = [CA: 'California', MI: 'Michigan']

def range = 10..20
def pattern = ~/fo*/

// equivalent to add()
list << 5

// call contains()
assert 4 in list
assert 5 in list
assert 15 in range

// subscript notation
assert list[1] == 4

// add a new key value pair
map << [WA: 'Washington']
// subscript notation
assert map['CA'] == 'California'
// property notation
assert map.WA == 'Washington'

// matches() strings against patterns
assert 'foo' =~ pattern

The Groovy Development Kit

继续以上数据结构,当在集合上使用迭代器,Groovy 提供了多种扩展方法,包装了 Java’s 核心数据结构, 例如 : each(), find(), findAll(), every(), collect(), inject(). 这些方法引入到编程语言中,是复杂的计算变得更为容易。 由于动态语言的特性,大量的新方法才能被运用到各种类型中。 你会发现很多有用的方法在 String, Files, Streams, Collections 等上:

The Power of switch

Groovy 中 switch 较之 C-ish 语言更为强大,后者只能处理原始类型。 Groovy 中可以处理的几乎所有类型:

def x = 1.23
def result = ""
switch (x) {
    case "foo": result = "found foo"
    // lets fall through
    case "bar": result += "bar"
    case [4, 5, 6, 'inList']:
        result = "list"
        break
    case 12..30:
        result = "range"
        break
    case Integer:
        result = "integer"
        break
    case Number:
        result = "number"
        break
    case { it > 3 }:
        result = "number > 3"
        break
    default: result = "default"
}
assert result == "number"

一般,在类型上 isCase() 方法可以判断是否符合条件。

Import aliasing

在 Java 中,当使用名字相同的两个类,可以通过识别其来自不同的包,例如: java.util.Listjava.awt.List,你可以引入一个类,另一个则需要全路径名称。

在一些情况下,你的代码中使用很长的类名称,会使得代码变得很累赘。

为了改进这种情况,Groovy 中引入了别名:

import java.util.List as juList
import java.awt.List as aList

import java.awt.WindowConstants as WC

也可以引入静态方法:

import static pkg.SomeClass.foo
foo()

Groovy Truth

所有对象都可以转化为布尔值:任何 null, void , 等于零或为空将返回 false, 否则返回 true.

So instead of writing:

if (name != null && name.length > 0) {}

You can just do:

if (name) {}

同样适用于集合。

Thus, you can use some shortcuts in things like while(), if(), the ternary operator, the Elvis operator (see below), etc.

It’s even possible to customize the Groovy Truth, by adding an boolean asBoolean() method to your classes!

Safe graph navigation

Groovy 支持一种进化的 . 操作符,用于安全的获取对象的画像。

Java 中当我们获取对象的节点时,需要去检查其是否为 null,你通常需要编写一些复杂的 if 语句,如:

if (order != null) {
    if (order.getCustomer() != null) {
        if (order.getCustomer().getAddress() != null) {
            System.out.println(order.getCustomer().getAddress());
        }
    }
}

使用 ?. 操作符,你可以将代码简化成这样:

println order?.customer?.address

Nulls 通过调用链检查,当其中任何元素为 null,不会抛出 NullPointerException , 而会返回 null.

Assert

可以使用断言声明检查参数,返回值等等。

Contrary to Java’s assert, assert`s don’t need to be activated to be working, so assert`s are always checked.

def check(String name) {
    // name non-null and non-empty according to Groovy Truth
    assert name
    // safe navigation + Groovy Truth to check
    assert name?.size() > 3
}

你将会注意到 GroovyPower Assert 输出内容相当丰富,其描述 每个子表达式的断言视图。

Elvis operator for default values

Elvis operator 是一个比较特殊的三元操作符,其使用非常便利:

We often have to write code like:

def result = name != null ? name : "Unknown"

Thanks to Groovy Truth, the null check can be simplified to just ‘name’.

可以这样使用 Elvis operator :

def result = name ?: "Unknown"

Catch any exception

如果你并不关心你 try block 中的异常类型,你可以简单的捕获他们,并忽略其捕获的异常类型: If you don’t really care about the type of the exception which is thrown inside your try block, you can simply catch any of them and simply omit the type of the caught exception. So instead of catching the exceptions like in:

try {
    // ...
} catch (Exception t) {
    // something bad happens
}

Then catch anything (‘any’ or ‘all’, or whatever makes you think it’s anything):

try {
    // ...
} catch (any) {
    // something bad happens
}

请注意这里捕获的是 Exceptions 而不是 Throwable‘s。 如果你需要捕获 everything , 你需要明确你所期望捕获的 Throwable's.

Optional typing advice

I’ll finish on some words on when and how to use optional typing. Groovy lets you decide whether you use explicit strong typing, or when you use def.

I’ve got a rather simple rule of thumb: whenever the code you’re writing is going to be used by others as a public API, you should always favor the use of strong typing, it helps making the contract stronger, avoids possible passed arguments type mistakes, gives better documentation, and also helps the IDE with code completion. Whenever the code is for your use only, like private methods, or when the IDE can easily infer the type, then you’re more free to decide when to type or not.

Groovy 模块指南

JSON 处理方式

Groovy 中集成 JSON 与 对象之间转换的功能。 在 groovy.json 包中的类专门用于 JSON 序列化及解析工作.

JsonSlurper

JsonSlurper 用于将 JSON 字符串转换为 Groovy 数据结构,如:maps, lists, 以及原始类型 Integer , Double, BooleanString.

其类中有大量的 parse 以及如: parseTextparseFile 等方法。 下面的例子中将使用 parseText 方法来解析 JSON 字符串,并将其转换为 list 或 map 对象。 The class comes with a bunch of overloaded parse methods plus some special methods such as parseText, parseFile and others. For the next example we will use the parseText method. It parses a JSON String and recursively converts it to a list or map of objects. The other parse* methods are similar in that they return a JSON String but for different parameter types.

def jsonSlurper = new JsonSlurper()
def object = jsonSlurper.parseText('{ "name": "John Doe" } /* some comment */')

assert object instanceof Map
assert object.name == 'John Doe'

这里的返回结果为 map , 可以像普通 Groovy 对象一样使用。 JsonSlurper 解析的 Json 字符串遵循 ECMA-404 JSON 交换标准 , 增加了对于注解及日期的支持。

In addition to maps JsonSlurper supports JSON arrays which are converted to lists. 除了 map,JsonSlurper 也支持将 JSON 数组转换为 lists .

def jsonSlurper = new JsonSlurper()
def object = jsonSlurper.parseText('{ "myList": [4, 8, 15, 16, 23, 42] }')

assert object instanceof Map
assert object.myList instanceof List
assert object.myList == [4, 8, 15, 16, 23, 42]

JSON 标准支持以下原始数据类型:string, number, object, true, false, null. JsonSlurper 会将它们转换为对应的 Groovy 类型。

def jsonSlurper = new JsonSlurper()
def object = jsonSlurper.parseText '''
    { "simple": 123,
      "fraction": 123.66,
      "exponential": 123e12
    }'''

assert object instanceof Map
assert object.simple.class == Integer
assert object.fraction.class == BigDecimal
assert object.exponential.class == BigDecimal

JsonSlurper 返回 Groovy 对象, 其返回结果符合 GPath 表达式。GPath 是一种功能很强的表达式语言,并通过 multiple slurpers 可以支持不同的 数据格式,例如:XmlSlurper 用于支持 xml

想了解更多的细节,可以查看 GPath表达式 这一章节。

下面是 JSON 类型与对应的 Groovy 数据类型,对照表:

JSON Groovy
Stirng java.lang.String
number java.lang.BigDecimal or java.lang.Integer
object java.util.LinkedHashMap
array java.util.ArrayList
true true
false false
null null
date java.util.Date yyyy-MM-dd’T’HH:mm:ssZ

Whenever a value in JSON is null, JsonSlurper supplements it with the Groovy null value. This is in contrast to other JSON parsers that represent a null value with a library-provided singleton object.

Parser Variants

JsonSlurper 附带了一些解析器实现。每种解析器适用于不同的需求,在特定的场景中,默认的解析器并不一定是最好的解决方案, 这里有一些解析器的简单介绍:

  • JsonParserCharArray 解析器,将 JSON 字符串,基于字符数组方式处理,在处理过程中拷贝字符的子数组并处理(这种机制称为:chopping).

The JsonFastParser is a special variant of the JsonParserCharArray and is the fastest parser. However, it is not the default parser for a reason. JsonFastParser is a so-called index-overlay parser. During parsing of the given JSON String it tries as hard as possible to avoid creating new char arrays or String instances. It keeps pointers to the underlying original character array only. In addition, it defers object creation as late as possible. If parsed maps are put into long-term caches care must be taken as the map objects might not be created and still consist of pointer to the original char buffer only. However, JsonFastParser comes with a special chop mode which dices up the char buffer early to keep a small copy of the original buffer. Recommendation is to use the JsonFastParser for JSON buffers under 2MB and keeping the long-term cache restriction in mind. (JsonFastParser 是基于 JsonParserCharArray 的扩展,它是最快速的解析器。其不是默认解析器,也是有一定原因。JsonFastParser 是所谓指数覆盖解析器。在解析 JSON 字符串的时候,其尽可能避免创建新的字符数组或字符串。仅保持其指针在愿字符数组上。此外,也尽可能推迟对象的创建,)

  • The JsonParserLax is a special variant of the JsonParserCharArray parser. It has similar performance characteristics as JsonFastParser but differs in that it isn’t exclusively relying on the ECMA-404 JSON grammar. For example it allows for comments, no quote strings etc.
  • The JsonParserUsingCharacterSource is a special parser for very large files. It uses a technique called “character windowing” to parse large JSON files (large means files over 2MB size in this case) with constant performance characteristics.

JsonSlurper 的默认解析器实现是 JsonParserCharArray. 下表中列举了 JsonParserType 与 其实现的对照关系:

Implementation Constant
JsonParserCharArray JsonParserType#CHAR_BUFFER
JsonFastParser JsonParserType#INDEX_OVERLAY
JsonParserLax JJsonParserType#LAX
JsonParserUsingCharacterSource JsonParserType#CHARACTER_SOURCE

通过调用 JsonSlurper#setType() 来设置 JsonParserType 可以很方便的修改解析器的实现。

def jsonSlurper = new JsonSlurper(type: JsonParserType.INDEX_OVERLAY)
def object = jsonSlurper.parseText('{ "myList": [4, 8, 15, 16, 23, 42] }')

assert object instanceof Map
assert object.myList instanceof List
assert object.myList == [4, 8, 15, 16, 23, 42]

JsonOutput

JsonOutput 主要用于将 Groovy 对象序列化为 JSON 字符串。

JsonOutput is responsible for serialising Groovy objects into JSON strings. It can be seen as companion object to JsonSlurper, being a JSON parser.

JsonOutput 中有一些重载,静态 toJson 方法。每种 toJson 实现都带有不同的参数。静态可以直接使用,或通过静态引入后使用。

toJson 返回结果返回 JSON 格式的字符串。

def json = JsonOutput.toJson([name: 'John Doe', age: 42])

assert json == '{"name":"John Doe","age":42}'

JsonOutput 不仅仅支持原始类型,maps,lists,还可以支持 POGOs 序列化。

class Person { String name }

def json = JsonOutput.toJson([ new Person(name: 'John'), new Person(name: 'Max') ])

assert json == '[{"name":"John"},{"name":"Max"}]'

上面例子中,JSON 的默认输出没有使用 pretty printed.

def json = JsonOutput.toJson([name: 'John Doe', age: 42])

assert json == '{"name":"John Doe","age":42}'

assert JsonOutput.prettyPrint(json) == '''\
{
    "name": "John Doe",
    "age": 42
}'''.stripIndent()

prettyPrint 方法只有一个字符串输入参数,它不与 JsonOutput 中的其它方法结合使用,其可以处理任何的 JSON 格式字符串。

Groovy 中还可以使用 JsonBuilder 或 StreamingJsonBuilder 创建 JSON. 这两种方式提供 DSL 通过其定制对象的图谱,并将其转化为 JSON 。

For more details on builders, have a look at the builders chapter which covers both JsonBuilder and StreamingJsonBuilder. 如希望了解更多的细节,可以查看它们对应的章节,JsonBuilderStreamingJsonBuilder

Working with a relational database

Groovy 中的 groovy-sql 模块相较于 Java JDBC 技术提供了更高层的抽象。 JDBC 其自身提供了更低层,并且相当全面的 API,其提供对于所有种类的关系型数据库系统统一访问方式。 这里例子中,我们将使用 HSQLDB ,你也可以替换使用 Oracle , SQL Server, MySQL 或其他数据库系统。 groovy-sql 模块中使用频率最高的类是 groovy.sql.Sql.

Connecting to the database

使用 Groovy 的 Sql 类连接数据库,需要4部分信息:

  • The database uniform resource locator (URL)
  • Username
  • Password
  • The driver class name (which can be derived automatically in some situations)

HSQLDB 为例,参考下面表格:

Property Value
url jdbc:hsqldb:mem:yourdb
user sa (or your username)
password yourPassword
driver org.hsqldb.jdbcDriver

参考 JDBC 驱动文档,来确定你所需要配置的具体值。

Sql 类中有一个 newInstance 工厂方法,来接收以上参数,你可以通常向下面这样使用:

Connecting to HSQLDB

import groovy.sql.Sql

def url = 'jdbc:hsqldb:mem:yourDB'
def user = 'sa'
def password = ''
def driver = 'org.hsqldb.jdbcDriver'
def sql = Sql.newInstance(url, user, password, driver)

// use 'sql' instance ...

sql.close()

If you don’t want to have to handle resource handling yourself (i.e. call close() manually) then you can use the withInstance variation as shown here: 如果你不想手动的管理资源(即:手动调用 close()), 你可以使用 withInstance 替换。

Connecting to HSQLDB (withInstance variation)

Sql.withInstance(url, user, password, driver) { sql ->
  // use 'sql' instance ...
}
Connecting with a DataSource

数据源相对来说,更为常用。你可以从连接池中获取一个有效的连接。这里我们使用 HSQLDB 中提供的一个数据源:

Connecting to HSQLDB with a DataSource

import groovy.sql.Sql
import org.hsqldb.jdbc.JDBCDataSource

def dataSource = new JDBCDataSource(
    database: 'jdbc:hsqldb:mem:yourDB', user: 'sa', password: '')
def sql = new Sql(dataSource)

// use then close 'sql' instance ...

If you have your own connection pooling, the details will be different, e.g. for Apache Commons DBCP: 如果使用你自己的连接池,一些代码细节上就会有些不同,例如使用 Apache Commons DBCP:

Connecting to HSQLDB with a DataSource using Apache Commons DBCP

@Grab('commons-dbcp:commons-dbcp:1.4')
import groovy.sql.Sql
import org.apache.commons.dbcp.BasicDataSource

def ds = new BasicDataSource(driverClassName: "org.hsqldb.jdbcDriver",
    url: 'jdbc:hsqldb:mem:yourDB', username: 'sa', password: '')
def sql = new Sql(ds)
// use then close 'sql' instance ...
Connecting using @Grab

The previous examples assume that the necessary database driver jar is already on your classpath. For a self-contained script you can add @Grab statements to the top of the script to automatically download the necessary jar as shown here: 上面例子中都假设所需要的数据库驱动 jar 都在 classpath 中。 在你的脚本中,可以在脚本头部通过 @Grab 语句,动态的下载所依赖的 jar ,例如:

Connecting to HSQLDB using @Grab

@Grab('org.hsqldb:hsqldb:2.3.2')
@GrabConfig(systemClassLoader=true)
// create, use, and then close sql instance ...

这里 @GrabConfig 语句是必须的,并确认 system classloader 被使用。 这样可以确保,驱动类和 java.sql.DriverManager 这样的系统类都在一个 classloader 中。

执行 SQL (Executing SQL)

你可以通过 execute() 执行任何 SQL 命令。 下面我们来看看,使用它来创建一张表。

创建表

The simplest way to execute SQL is to call the execute() method passing the SQL you wish to execute as a String as shown here: 最简单的执行 SQL 的方式,就是将 SQL 语句传递给 execute() :

Creating a table

// ... create 'sql' instance
sql.execute '''
  CREATE TABLE Author (
    id          INTEGER GENERATED BY DEFAULT AS IDENTITY,
    firstname   VARCHAR(64),
    lastname    VARCHAR(64)
  );
'''
// close 'sql' instance ...

There is a variant of this method which takes a GString and another with a list of parameters. There are also other variants with similar names: executeInsert and executeUpdate. We’ll see examples of these variants in other examples in this section. 这里还有一些方法来支持 GString 和 参数列表。 还有一些名字类似的方法,如: executeInsertexecuteUpdate . 在这章节中我们也会看到相关的一些例子。

Basic CRUD operations

数据库上的基础操作:create, Read, update, Delete (简称为:CRUD). 这里将一一来验证。

创建/写入 数据

你可以使用 insert SQL 语句,调用 execute() 方法,来写入一条记录: You can use the same execute() statement we saw earlier but to insert a row by using a SQL insert statement as follows:

Inserting a row

sql.execute "INSERT INTO Author (firstname, lastname) VALUES ('Dierk', 'Koenig')"

You can use a special executeInsert method instead of execute. This will return a list of all keys generated. Both the execute and executeInsert methods allow you to place ‘?’ placeholders into your SQL string and supply a list of parameters. In this case a PreparedStatement is used which avoids any risk of SQL injection. The following example illustrates executeInsert using placeholders and parameters: 你可以使用 executeInsert 方法来替代 execute. 这个方法将返回写入数据主键的列表结构。 executeInsertexecute 方法都可以 在 SQL 语句中使用 ? 占位符,以及匹配的参数列表。这种预编译方法是可以避免 SQL 的注入风险。 下面的例子,将说明如何使用 executeInsert ,占位符,以及参数列表:

Inserting a row using executeInsert with placeholders and parameters

def insertSql = 'INSERT INTO Author (firstname, lastname) VALUES (?,?)'
def params = ['Jon', 'Skeet']
def keys = sql.executeInsert insertSql, params
assert keys[0] == [1]

此外, executeexecuteInsert 中都可以使用 GString. SQL 中 $ 被假定为占位符。如果你在 SQL 中非常规的位置使用占位符, 可以通过 GString 附带其变量实现。可以在 GroovyDoc 中查看更详细的描述。executeInsert 中,允许你提供主键列名称,来指定复合主键的返回值。 下面的代码片段,将说明以上的使用规范:

Inserting a row using executeInsert with a GString and specifying key names

def first = 'Guillaume'
def last = 'Laforge'
def myKeyNames = ['ID']
def myKeys = sql.executeInsert """
  INSERT INTO Author (firstname, lastname)
  VALUES (${first}, ${last})
""", myKeyNames
assert myKeys[0] == [ID: 2]
读取数据

可以通过这些方法来读取数据:query, eachRow, firstRow and rows

如果你希望使用 JDBC API 提供的 ResultSet , 你可以使用 query 方法:

Reading data using query

def expected = ['Dierk Koenig', 'Jon Skeet', 'Guillaume Laforge']

def rowNum = 0
sql.query('SELECT firstname, lastname FROM Author') { resultSet ->
  while (resultSet.next()) {
    def first = resultSet.getString(1)
    def last = resultSet.getString('lastname')
    assert expected[rowNum++] == "$first $last"
  }
}

如果你想使用略微高级的抽象,其通过 Groovy map 方式来抽象 ResultSet ,你可以使用 eachRow 方法。

Reading data using eachRow

rowNum = 0
sql.eachRow('SELECT firstname, lastname FROM Author') { row ->
  def first = row[0]
  def last = row.lastname
  assert expected[rowNum++] == "$first $last"
}

你可以使用 list-stylemap-style 来访问数据。

如果你想使用与 eachRow 类似的功能,并只返回一条记录,你可以调用 firstRow 方法。

Reading data using firstRow

def first = sql.firstRow('SELECT lastname, firstname FROM Author')
assert first.values().sort().join(',') == 'Dierk,Koenig'

使用 rows 方法将返回数据结构的列表:

Reading data using rows

List authors = sql.rows('SELECT firstname, lastname FROM Author')
assert authors.size() == 3
assert authors.collect { "$it.FIRSTNAME ${it[-1]}" } == expected

注意 map-like 抽象是大小写不敏感的 keys ( 如: 可以使用 ‘FIRSTNAME’ or ‘firstname’ )并且能够使用负数作为索引。

You can also use any of the above methods to return scalar values, though typically firstRow is all that is required in such cases. An example returning the count of rows is shown here: 你可以使用上面的方法返回纯数值,通常使用 firstRow , 这里例子返回计算当前数据的条数:

Reading scalar values

assert sql.firstRow('SELECT COUNT(*) AS num FROM Author').num == 3
更新数据

使用 execute 方法也可以用来更新数据。 你可以先 insert authorlastname , 其后再更新 firstname :

Updating a row

sql.execute "INSERT INTO Author (lastname) VALUES ('Thorvaldsson')"
sql.execute "UPDATE Author SET firstname='Erik' where lastname='Thorvaldsson'"

这里同样有一个扩展的方法 executeUpdate , 可以返回更新数据行数:

Using executeUpdate

def updateSql = "UPDATE Author SET lastname='Pragt' where lastname='Thorvaldsson'"
def updateCount = sql.executeUpdate updateSql
assert updateCount == 1

def row = sql.firstRow "SELECT * FROM Author where firstname = 'Erik'"
assert "${row.firstname} ${row.lastname}" == 'Erik Pragt'
删除数据

execute 方法,同样可以用来删除数据:

Deleting rows

assert sql.firstRow('SELECT COUNT(*) as num FROM Author').num == 3
sql.execute "DELETE FROM Author WHERE lastname = 'Skeet'"
assert sql.firstRow('SELECT COUNT(*) as num FROM Author').num == 2

高级 SQL 操作

事务控制

使用事务最简单的一种方式,就是使用 withTransaction 必包:

A successful transaction

assert sql.firstRow('SELECT COUNT(*) as num FROM Author').num == 0
sql.withTransaction {
  sql.execute "INSERT INTO Author (firstname, lastname) VALUES ('Dierk', 'Koenig')"
  sql.execute "INSERT INTO Author (firstname, lastname) VALUES ('Jon', 'Skeet')"
}
assert sql.firstRow('SELECT COUNT(*) as num FROM Author').num == 2

这里数据库初始没有数据,当操作完成后,有两条数据。

If something goes wrong, any earlier operations within the withTransaction block are rolled back. We can see that in operation in the following example where we use database metadata (more details coming up shortly) to find the maximum allowable size of the firstname column and then attempt to enter a firstname one larger than that maximum value as shown here: 如果有任何错误出现,withTransaction 中的操作都讲回滚。 我们将在下面的例子中看到,通过给 firstname 列上一个超出最大长度的值,来构造这种操作异常:

A failed transaction will cause a rollback

def maxFirstnameLength
def metaClosure = { meta -> maxFirstnameLength = meta.getPrecision(1) }
def rowClosure = {}
def rowCountBefore = sql.firstRow('SELECT COUNT(*) as num FROM Author').num
try {
  sql.withTransaction {
    sql.execute "INSERT INTO Author (firstname) VALUES ('Dierk')"
    sql.eachRow "SELECT firstname FROM Author WHERE firstname = 'Dierk'", metaClosure, rowClosure
    sql.execute "INSERT INTO Author (firstname) VALUES (?)", 'X' * (maxFirstnameLength + 1)
  }
} catch(ignore) { println ignore.message }
def rowCountAfter = sql.firstRow('SELECT COUNT(*) as num FROM Author').num
assert rowCountBefore == rowCountAfter

尽管第一条语句已经执行成功,但是其也将回滚,数据行数前后也是一致的。

批量处理
当处理大量数据,特别是在写入大量数据时,分段批量处理将更加高效。 这里通过使用 withBatch 来操作:

Batching SQL statements

sql.withBatch(3) { stmt ->
  stmt.addBatch "INSERT INTO Author (firstname, lastname) VALUES ('Dierk', 'Koenig')"
  stmt.addBatch "INSERT INTO Author (firstname, lastname) VALUES ('Paul', 'King')"
  stmt.addBatch "INSERT INTO Author (firstname, lastname) VALUES ('Guillaume', 'Laforge')"
  stmt.addBatch "INSERT INTO Author (firstname, lastname) VALUES ('Hamlet', 'D''Arcy')"
  stmt.addBatch "INSERT INTO Author (firstname, lastname) VALUES ('Cedric', 'Champeau')"
  stmt.addBatch "INSERT INTO Author (firstname, lastname) VALUES ('Erik', 'Pragt')"
  stmt.addBatch "INSERT INTO Author (firstname, lastname) VALUES ('Jon', 'Skeet')"
}

执行以上语句,数据库中将添加 7 条记录。 如果你想弄清楚这中间到底发生了什么,可以添加一些日志在你的程序中,例如:

Logging additional SQL information

import java.util.logging.*

// next line will add fine logging
Logger.getLogger('groovy.sql').level = Level.FINE
// also adjust logging.properties file in JRE_HOME/lib to have:
// java.util.logging.ConsoleHandler.level = FINE

With this extra logging turned on, and the changes made as per the above comment for the logging.properties file, you should see output such as: 这样打开日志,使用注释里的方法也能达到同样效果:

SQL logging output with batching enable

FINE: Successfully executed batch with 3 command(s)
Apr 19, 2015 8:38:42 PM groovy.sql.BatchingStatementWrapper processResult

FINE: Successfully executed batch with 3 command(s)
Apr 19, 2015 8:38:42 PM groovy.sql.BatchingStatementWrapper processResult

FINE: Successfully executed batch with 1 command(s)
Apr 19, 2015 8:38:42 PM groovy.sql.Sql getStatement

我们还应该注意,任何 SQL 语句都可以加入到批量处理中,并不是只能向同一张表中写入数据.

在之前的提示中,为了避免 SQL 注入,我们尽可能使用预编译语句:

Batching prepared statements

def qry = 'INSERT INTO Author (firstname, lastname) VALUES (?,?)'
sql.withBatch(3, qry) { ps ->
  ps.addBatch('Dierk', 'Koenig')
  ps.addBatch('Paul', 'King')
  ps.addBatch('Guillaume', 'Laforge')
  ps.addBatch('Hamlet', "D'Arcy")
  ps.addBatch('Cedric', 'Champeau')
  ps.addBatch('Erik', 'Pragt')
  ps.addBatch('Jon', 'Skeet')
}

如果数据来自脚本或 web 表单,这提供更加安全选择,这里已经使用预编译语句,我们的批处理语句也就限制为相同的 SQL 操作。

分页

When presenting large tables of data to a user, it is often convenient to present information a page at a time. Many of Groovy’s SQL retrieval methods have extra parameters which can be used to select a particular page of interest. The starting position and page size are specified as integers as shown in the following example using rows: 当给用户展示大表数据,通常都会选择分页展示。Groovy 中很多获取数据方法都扩展可相关参数,可以用于选择分页操作。 起始位置,单页大小,被作为参数指定:

Retrieving pages of data

def qry = 'SELECT * FROM Author'
assert sql.rows(qry, 1, 3)*.firstname == ['Dierk', 'Paul', 'Guillaume']
assert sql.rows(qry, 4, 3)*.firstname == ['Hamlet', 'Cedric', 'Erik']
assert sql.rows(qry, 7, 3)*.firstname == ['Jon']
获取元数据

JDBC 中的元数据,可以通过多种方式获取。 最基础的方式如下面例子中从数据行中获取:

Using row metadata

sql.eachRow("SELECT * FROM Author WHERE firstname = 'Dierk'") { row ->
  def md = row.getMetaData()
  assert md.getTableName(1) == 'AUTHOR'
  assert (1..md.columnCount).collect{ md.getColumnName(it) } == ['ID', 'FIRSTNAME', 'LASTNAME']
  assert (1..md.columnCount).collect{ md.getColumnTypeName(it) } == ['INTEGER', 'VARCHAR', 'VARCHAR']
}

和上面例子有略微不同,这里查找列名称:

Also using row metadata

sql.eachRow("SELECT firstname AS first FROM Author WHERE firstname = 'Dierk'") { row ->
  def md = row.getMetaData()
  assert md.getColumnName(1) == 'FIRSTNAME'
  assert md.getColumnLabel(1) == 'FIRST'
}

Accessing metadata is quite common, so Groovy also provides variants to many of its methods that let you supply a closure that will be called once with the row metadata in addition to the normal row closure which is called for each row. The following example illustrates the two closure variant for eachRow:

Using row and metadata closures

def metaClosure = { meta -> assert meta.getColumnName(1) == 'FIRSTNAME' }
def rowClosure = { row -> assert row.FIRSTNAME == 'Dierk' }
sql.eachRow("SELECT firstname FROM Author WHERE firstname = 'Dierk'", metaClosure, rowClosure)

注意,我们的 SQL 这里只返回一条数据,可以使用 firstRow 达到同样的效果。

最后, JDBC 还提供了连接上的元数据(不仅仅在数据行上):

Using connection metadata

def md = sql.connection.metaData
assert md.driverName == 'HSQL Database Engine Driver'
assert md.databaseProductVersion == '2.3.2'
assert ['JDBCMajorVersion', 'JDBCMinorVersion'].collect{ md[it] } == [4, 0]
assert md.stringFunctions.tokenize(',').contains('CONCAT')
def rs = md.getTables(null, null, 'AUTH%', null)
assert rs.next()
assert rs.getString('TABLE_NAME') == 'AUTHOR'

参考 JavaDoc 可以找到驱动上可以找到可以访问的元数据信息。

Named and named-ordinal parameters

Groovy 中提供了一些扩展替代占位符的语法变体。通常 GString 变量会优于其他的替代方案。在一些 JAVA 集成或使用模版场景,这些替代方案也是非常有用的。 命名参数变量就像字符串加上参数变量列表,将 ? 占位符替换为一组参数名称,占位符的形式变为::propName? placeholders ,其参数为 map,命名的参数,或领域对象。 map 和 对象中其属性名称需要与占位符相对应。

Here is an example using the colon form:

Named parameters (colon form)

sql.execute "INSERT INTO Author (firstname, lastname) VALUES (:first, :last)", first: 'Dierk', last: 'Koenig'

And another example using the question mark form:

Named parameters (question mark form)

sql.execute "INSERT INTO Author (firstname, lastname) VALUES (?.first, ?.last)", first: 'Jon', last: 'Skeet'

If the information you need to supply is spread across multiple maps or domain objects you can use the question mark form with an additional ordinal index as shown here: 如果你的信息需要通过多个 map 或 对象来传递,你可以使用问号标记形式,使用带有顺序的索引号:

Named-ordinal parameters

class Rockstar { String first, last }
def pogo = new Rockstar(first: 'Paul', last: 'McCartney')
def map = [lion: 'King']
sql.execute "INSERT INTO Author (firstname, lastname) VALUES (?1.first, ?2.lion)", pogo, map
存储过程

在不同数据库之间, 存储过程或其函数的差别比较细微。 在 HSQLDB 中,我们通过创建存储函数返回表中所有 authors 的缩写。

Creating a stored function

sql.execute """
  CREATE FUNCTION SELECT_AUTHOR_INITIALS()
  RETURNS TABLE (firstInitial VARCHAR(1), lastInitial VARCHAR(1))
  READS SQL DATA
  RETURN TABLE (
    SELECT LEFT(Author.firstname, 1) as firstInitial, LEFT(Author.lastname, 1) as lastInitial
    FROM Author
  )
"""

我们可以使用 Groovy’s 普通的查询方法来调用 CALL 语句。 这里是 eachRow 例子:

Creating a stored procedure or function

def result = []
sql.eachRow('CALL SELECT_AUTHOR_INITIALS()') {
  result << "$it.firstInitial$it.lastInitial"
}
assert result == ['DK', 'JS', 'GL']

这下面的代码创建另一个存储函数,并使用 lastname 作为参数:

Creating a stored function with a parameter

sql.execute """
  CREATE FUNCTION FULL_NAME (p_lastname VARCHAR(64))
  RETURNS VARCHAR(100)
  READS SQL DATA
  BEGIN ATOMIC
    DECLARE ans VARCHAR(100);
    SELECT CONCAT(firstname, ' ', lastname) INTO ans
    FROM Author WHERE lastname = p_lastname;
    RETURN ans;
  END
"""

We can use the placeholder syntax to specify where the parameter belongs and note the special placeholder position to indicate the result:

Using a stored function with a parameter

def result = sql.firstRow("{? = call FULL_NAME(?)}", ['Koenig'])
assert result[0] == 'Dierk Koenig'

Finally, here is a stored procedure with input and output parameters:

Creating a stored procedure with input and output parameters

sql.execute """
  CREATE PROCEDURE CONCAT_NAME (OUT fullname VARCHAR(100),
    IN first VARCHAR(50), IN last VARCHAR(50))
  BEGIN ATOMIC
    SET fullname = CONCAT(first, ' ', last);
  END
"""

To use the CONCAT_NAME stored procedure parameter, we make use of a special call method. Any input parameters are simply provided as parameters to the method call. For output parameters, the resulting type must be specified as shown here:

Using a stored procedure with input and output parameters

sql.call("{call CONCAT_NAME(?, ?, ?)}", [Sql.VARCHAR, 'Dierk', 'Koenig']) {
  fullname -> assert fullname == 'Dierk Koenig'
}

Using DataSets

待续 (TBD)

XML 处理

  1. Parsing XML

1.1. XmlParser and XmlSlurper

The most commonly used approach for parsing XML with Groovy is to use one of:

groovy.util.XmlParser

groovy.util.XmlSlurper

Both have the same approach to parse an xml. Both come with a bunch of overloaded parse methods plus some special methods such as parseText, parseFile and others. For the next example we will use the parseText method. It parses a XML String and recursively converts it to a list or map of objects.

XmlSlurper def text = ‘’‘

<list>
<technology>
<name>Groovy</name>

</technology>

</list>

‘’‘

def list = new XmlSlurper().parseText(text)

assert list instanceof groovy.util.slurpersupport.GPathResult assert list.technology.name == ‘Groovy’ Parsing the XML an returning the root node as a GPathResult Checking we’re using a GPathResult Traversing the tree in a GPath style XmlParser def text = ‘’‘

<list>
<technology>
<name>Groovy</name>

</technology>

</list>

‘’‘

def list = new XmlParser().parseText(text)

assert list instanceof groovy.util.Node assert list.technology.name.text() == ‘Groovy’ Parsing the XML an returning the root node as a Node Checking we’re using a Node Traversing the tree in a GPath style Let’s see the similarities between XMLParser and XMLSlurper first:

Both are based on SAX so they both are low memory footprint

Both can update/transform the XML

But they have key differences:

XmlSlurper evaluates the structure lazily. So if you update the xml you’ll have to evaluate the whole tree again.

XmlSlurper returns GPathResult instances when parsing XML

XmlParser returns Node objects when parsing XML

When to use one or the another?

There is a discussion at StackOverflow. The conclusions written here are based partially on this entry. If you want to transform an existing document to another then XmlSlurper will be the choice

If you want to update and read at the same time then XmlParser is the choice.

The rationale behind this is that every time you create a node with XmlSlurper it won’t be available until you parse the document again with another XmlSlurper instance. Need to read just a few nodes XmlSlurper is for you ”.

If you just have to read a few nodes XmlSlurper should be your choice, since it will not have to create a complete structure in memory”

In general both classes perform similar way. Even the way of using GPath expressions with them are the same (both use breadthFirst() and depthFirst() expressions). So I guess it depends on the write/read frequency.

1.2. DOMCategory

There is another way of parsing XML documents with Groovy with the used of groovy.xml.dom.DOMCategory which is a category class which adds GPath style operations to Java’s DOM classes.

Java has in-built support for DOM processing of XML using classes representing the various parts of XML documents, e.g. Document, Element, NodeList, Attr etc. For more information about these classes, refer to the respective JavaDocs. Having a XML like the following:

static def CAR_RECORDS = ‘’’ <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>

‘’’ You can parse it using ‘groovy.xml.DOMBuilder` and groovy.xml.dom.DOMCategory.

def reader = new StringReader(CAR_RECORDS) def doc = DOMBuilder.parse(reader) def records = doc.documentElement

use(DOMCategory) {
assert records.car.size() == 3

} Parsing the XML Creating DOMCategory scope to be able to use helper method calls 2. GPath

The most common way of querying XML in Groovy is using GPath:

GPath is a path expression language integrated into Groovy which allows parts of nested structured data to be identified. In this sense, it has similar aims and scope as XPath does for XML. The two main places where you use GPath expressions is when dealing with nested POJOs or when dealing with XML

It is similar to XPath expressions and you can use it not only with XML but also with POJO classes. As an example, you can specify a path to an object or element of interest:

a.b.c → for XML, yields all the <c> elements inside <b> inside <a>

a.b.c → all POJOs, yields the <c> properties for all the <b> properties of <a> (sort of like a.getB().getC() in JavaBeans)

For XML, you can also specify attributes, e.g.:

a[“@href”] → the href attribute of all the a elements

a.'@href‘ → an alternative way of expressing this

a.@href → an alternative way of expressing this when using XmlSlurper

Let’s illustrate this with an example:

static final String books = ‘’‘
<response version-api=”2.0”>
<value>
<books>
<book available=”20” id=”1”>
<title>Don Xijote</title> <author id=”1”>Manuel De Cervantes</author>

</book> <book available=”14” id=”2”>

<title>Catcher in the Rye</title>

<author id=”2”>JD Salinger</author>

</book> <book available=”13” id=”3”>

<title>Alice in Wonderland</title> <author id=”3”>Lewis Carroll</author>

</book> <book available=”5” id=”4”>

<title>Don Xijote</title> <author id=”4”>Manuel De Cervantes</author>

</book>

</books>

</value>

</response>

‘’’ 2.1. Simply traversing the tree

First thing we could do is to get a value using POJO’s notation. Let’s get the first book’s author’s name

Getting node value def response = new XmlSlurper().parseText(books) def authorResult = response.value.books.book[0].author

assert authorResult.text() == ‘Manuel De Cervantes’ First we parse the document with XmlSlurper and the we have to consider the returning value as the root of the XML document, so in this case is “response”.

That’s why we start traversing the document from response and then value.books.book[0].author. Note that in XPath the node arrays starts in [1] instead of [0], but because GPath is Java-based it begins at index 0.

In the end we’ll have the instance of the author node and because we wanted the text inside that node we should be calling the text() method. The author node is an instance of GPathResult type and text() a method giving us the content of that node as a String.

When using GPath with an xml parsed with XmlSlurper we’ll have as a result a GPathResult object. GPathResult has many other convenient methods to convert the text inside a node to any other type such as:

toInteger()

toFloat()

toBigInteger()

…​

All these methods try to convert a String to the appropriate type.

If we were using a XML parsed with XmlParser we could be dealing with instances of type Node. But still all the actions applied to GPathResult in these examples could be applied to a Node as well. Creators of both parsers took into account GPath compatibility.

Next step is to get the some values from a given node’s attribute. In the following sample we want to get the first book’s author’s id. We’ll be using two different approaches. Let’s see the code first:

Getting an attribute’s value def response = new XmlSlurper().parseText(books)

def book = response.value.books.book[0] def bookAuthorId1 = book.@id def bookAuthorId2 = book['@id‘]

assert bookAuthorId1 == ‘1’ assert bookAuthorId1.toInteger() == 1 assert bookAuthorId1 == bookAuthorId2 Getting the first book node Getting the book’s id attribute @id Getting the book’s id attribute with map notation ['@id‘] Getting the value as a String Getting the value of the attribute as an Integer As you can see there are to types of notations to get attributes, the

direct notation with @nameoftheattribute

map notation using ['@nameoftheattribute‘]

Both of them are equally valid.

2.2. Speed things up with breadthFirst and depthFirst

If you ever have used XPath you may have used expressions like

// : Look everywhere

/following-sibling::othernode : Look for a node “othernode” in the same level

More or less we have their counterparts in GPath with the methods breadthFirst() and depthFirst().

The first example shows a simple use of breadthFirst(). The creators of this methods created a shorter syntax for it using the symbol *.

breadthFirst() def response = new XmlSlurper().parseText(books)

def catcherInTheRye = response.value.books.’*’.find { node->
/* node.@id == 2 could be expressed as node['@id‘] == 2 */
node.name() == ‘book’ && node.@id == ‘2’

}

assert catcherInTheRye.title.text() == ‘Catcher in the Rye’ This test searches for any node at the same level of the “books” node first, and only if it couldn’t find the node we were looking for, then it will look deeper in the tree, always taking into account the given the expression inside the closure.

The expression says Look for any node with a tag name equals ‘book’ having an id with a value of ‘2’.

But what if we would like to look for a given value without having to know exactly where it is. Let’s say that the only thing we know is the id of the author “Lewis Carroll” . How are we going to be able to find that book? depthFirst() is the solution:

depthFirst() def response = new XmlSlurper().parseText(books)

def bookId = response.’**’.find { book->
book.author.text() == ‘Lewis Carroll’

}.@id

assert bookId == 3 depthFirst() is the same as looking something everywhere in the tree from this point down. In this case we’ve used the method find(Closure cl) to find just the first occurrence.

What if we want to collect all book’s titles?

depthFirst() def response = new XmlSlurper().parseText(books)

def titles = response.’**’.findAll{ node-> node.name() == ‘title’ }*.text()

assert titles.size() == 4 It is worth mentioning again that there are some useful methods converting a node’s value to an integer, float…​ etc. Those methods could be convenient when doing comparisons like this:

helpers def response = new XmlSlurper().parseText(books)

def titles = response.value.books.book.findAll{book->
/* You can use toInteger() over the GPathResult object */
book.@id.toInteger() > 2

}*.title

assert titles.size() == 2 In this case the number 2 has been hardcoded but imagine that value could have come from any other source (database…​ etc.).

  1. Creating XML

The most commonly used approach for creating XML with Groovy is to use a builder, i.e. one of:

groovy.xml.MarkupBuilder

groovy.xml.StreamingMarkupBuilder

3.1. MarkupBuilder

Here is an example of using Groovy’s MarkupBuilder to create a new XML file:

Creating Xml with MarkupBuilder def writer = new StringWriter() def xml = new MarkupBuilder(writer)

xml.records() {
car(name:’HSV Maloo’, make:’Holden’, year:2006) {
country(‘Australia’) record(type:’speed’, ‘Production Pickup Truck with speed of 271kph’)

} car(name:’Royale’, make:’Bugatti’, year:1931) {

country(‘France’) record(type:’price’, ‘Most Valuable Car at $15 million’)

}

}

def records = new XmlSlurper().parseText(writer.toString())

assert records.car.first().name.text() == ‘HSV Maloo’ assert records.car.last().name.text() == ‘Royale’ Create an instance of MarkupBuilder Start creating the XML tree Create an instance of XmlSlurper to traverse and test the generated XML Let’s take a look a little bit closer:

Creating XML elements def xmlString = “<movie>the godfather</movie>”

def xmlWriter = new StringWriter() def xmlMarkup = new MarkupBuilder(xmlWriter)

xmlMarkup.movie(“the godfather”)

assert xmlString == xmlWriter.toString() We’re creating a reference string to compare against The xmlWriter instance is used by MarkupBuilder to convert the xml representation to a String instance eventually The xmlMarkup.movie(…​) call will create a XML node with a tag called movie and with content the godfather. Creating XML elements with attributes def xmlString = “<movie id=‘2’>the godfather</movie>”

def xmlWriter = new StringWriter() def xmlMarkup = new MarkupBuilder(xmlWriter)

xmlMarkup.movie(id: “2”, “the godfather”)

assert xmlString == xmlWriter.toString() This time in order to create both attributes and node content you can create as many map entries as you like and finally add a value to set the node’s content The value could be any Object, the value will be serialized to its String representation. Creating XML nested elements def xmlWriter = new StringWriter() def xmlMarkup = new MarkupBuilder(xmlWriter)

xmlMarkup.movie(id: 2) {
name(“the godfather”)

}

def movie = new XmlSlurper().parseText(xmlWriter.toString())

assert movie.@id == 2 assert movie.name.text() == ‘the godfather’ A closure represents the children elements of a given node. Notice this time instead of using a String for the attribute we’re using a number. Sometimes you may want to use a specific namespace in your xml documents:

Namespace aware def xmlWriter = new StringWriter() def xmlMarkup = new MarkupBuilder(xmlWriter)

xmlMarkup
.’x:movies’(‘xmlns:x’:’http://www.groovy-lang.org’) {
‘x:movie’(id: 1, ‘the godfather’) ‘x:movie’(id: 2, ‘ronin’)

}

def movies =
new XmlSlurper()
.parseText(xmlWriter.toString()) .declareNamespace(x:’http://www.groovy-lang.org’)

assert movies.’x:movie’.last().@id == 2 assert movies.’x:movie’.last().text() == ‘ronin’ Creating a node with a given namespace xmlns:x Creating a XmlSlurper registering the namespace to be able to test the XML we just created What about having some more meaningful example. We may want to generate more elements, to have some logic when creating our XML:

Mix code def xmlWriter = new StringWriter() def xmlMarkup = new MarkupBuilder(xmlWriter)

xmlMarkup
.’x:movies’(‘xmlns:x’:’http://www.groovy-lang.org’) {
(1..3).each { n ->

‘x:movie’(id: n, “the godfather $n”) if (n % 2 == 0) {

‘x:movie’(id: n, “the godfather $n (Extended)”)

}

}

}

def movies =
new XmlSlurper()
.parseText(xmlWriter.toString()) .declareNamespace(x:’http://www.groovy-lang.org’)

assert movies.’x:movie’.size() == 4 assert movies.’x:movie’*.text().every { name -> name.startsWith(‘the’)} Generating elements from a range Using a conditional for creating a given element Of course the instance of a builder can be passed as a parameter to refactor/modularize your code:

Mix code def xmlWriter = new StringWriter() def xmlMarkup = new MarkupBuilder(xmlWriter)

Closure<MarkupBuilder> buildMovieList = { MarkupBuilder builder ->
(1..3).each { n ->

builder.’x:movie’(id: n, “the godfather $n”) if (n % 2 == 0) {

builder.’x:movie’(id: n, “the godfather $n (Extended)”)

}

}

return builder

}

xmlMarkup.’x:movies’(‘xmlns:x’:’http://www.groovy-lang.org’) {
buildMovieList(xmlMarkup)

}

def movies =
new XmlSlurper()
.parseText(xmlWriter.toString()) .declareNamespace(x:’http://www.groovy-lang.org’)

assert movies.’x:movie’.size() == 4 assert movies.’x:movie’*.text().every { name -> name.startsWith(‘the’)} In this case we’ve created a Closure to handle the creation of a list of movies Just using the buildMovieList function when necessary 3.2. StreamingMarkupBuilder

The class groovy.xml.StreamingMarkupBuilder is a builder class for creating XML markup. This implementation uses a groovy.xml.streamingmarkupsupport.StreamingMarkupWriter to handle output.

Using StreamingMarkupBuilder def xml = new StreamingMarkupBuilder().bind {

records {
car(name:’HSV Maloo’, make:’Holden’, year:2006) {
country(‘Australia’) record(type:’speed’, ‘Production Pickup Truck with speed of 271kph’)

} car(name:’P50’, make:’Peel’, year:1962) {

country(‘Isle of Man’) record(type:’size’, ‘Smallest Street-Legal Car at 99cm wide and 59 kg in weight’)

} car(name:’Royale’, make:’Bugatti’, year:1931) {

country(‘France’) record(type:’price’, ‘Most Valuable Car at $15 million’)

}

}

}

def records = new XmlSlurper().parseText(xml.toString())

assert records.car.size() == 3 assert records.car.find { it.@name == ‘P50’ }.country.text() == ‘Isle of Man’ Note that StreamingMarkupBuilder.bind returns a Writable instance that may be used to stream the markup to a Writer We’re capturing the output in a String to parse it again an check the structure of the generated XML with XmlSlurper. 3.3. MarkupBuilderHelper

The groovy.xml.MarkupBuilderHelper is, as its name reflects, a helper for groovy.xml.MarkupBuilder.

This helper normally can be accessed from within an instance of class groovy.xml.MarkupBuilder or an instance of groovy.xml.StreamingMarkupBuilder.

This helper could be handy in situations when you may want to:

Produce a comment in the output

Produce an XML processing instruction in the output

Produce an XML declaration in the output

Print data in the body of the current tag, escaping XML entities

Print data in the body of the current tag

In both MarkupBuilder and StreamingMarkupBuilder this helper is accessed by the property mkp:

Using MarkupBuilder’s ‘mkp’ def xmlWriter = new StringWriter() def xmlMarkup = new MarkupBuilder(xmlWriter).rules {

mkp.comment(‘THIS IS THE MAIN RULE’) rule(sentence: mkp.yield(‘3 > n’))

}

assert xmlWriter.toString().contains(‘3 &gt; n’) assert xmlWriter.toString().contains(‘<!– THIS IS THE MAIN RULE –>’) Using mkp to create a comment in the XML Using mkp to generate an escaped value Checking both assumptions were true Here is another example to show the use of mkp property accessible from within the bind method scope when using StreamingMarkupBuilder:

Using StreamingMarkupBuilder’s ‘mkp’ def xml = new StreamingMarkupBuilder().bind {

records {
car(name: mkp.yield(‘3 < 5’)) car(name: mkp.yieldUnescaped(‘1 < 3’))

}

}

assert xml.toString().contains(‘3 &lt; 5’) assert xml.toString().contains(‘1 < 3’) If we want to generate a escaped value for the name attribute with mkp.yield Checking the values later on with XmlSlurper 3.4. DOMToGroovy

Suppose we have an existing XML document and we want to automate generation of the markup without having to type it all in? We just need to use org.codehaus.groovy.tools.xml.DOMToGroovy as shown in the following example:

Building MarkupBuilder from DOMToGroovy def songs = “”“

<songs>
<song>
<title>Here I go</title> <band>Whitesnake</band>

</song>

</songs>

“”“

def builder =

javax.xml.parsers.DocumentBuilderFactory.newInstance().newDocumentBuilder()

def inputStream = new ByteArrayInputStream(songs.bytes) def document = builder.parse(inputStream) def output = new StringWriter() def converter = new DomToGroovy(new PrintWriter(output))

converter.print(document)

String xmlRecovered =

new GroovyShell() .evaluate(“”“

def writer = new StringWriter() def builder = new groovy.xml.MarkupBuilder(writer) builder.${output}

return writer.toString()

“””)

assert new XmlSlurper().parseText(xmlRecovered).song.title.text() == ‘Here I go’

Creating DOMToGroovy instance Converts the XML to MarkupBuilder calls which are available in the output StringWriter Using output variable to create the whole MarkupBuilder Back to XML string 4. Manipulating XML

In this chapter you’ll see the different ways of adding / modifying / removing nodes using XmlSlurper or XmlParser. The xml we are going to be handling is the following:

def xml = “”” <response version-api=”2.0”>

<value>
<books>
<book id=”2”>
<title>Don Xijote</title> <author id=”1”>Manuel De Cervantes</author>

</book>

</books>

</value>

</response> “”” 4.1. Adding nodes

The main difference between XmlSlurper and XmlParser is that when former creates the nodes they won’t be available until the document’s been evaluated again, so you should parse the transformed document again in order to be able to see the new nodes. So keep that in mind when choosing any of both approaches.

If you needed to see a node right after creating it then XmlParser should be your choice, but if you’re planning to do many changes to the XML and send the result to another process maybe XmlSlurper would be more efficient.

You can’t create a new node directly using the XmlSlurper instance, but you can with XmlParser. The way of creating a new node from XmlParser is through its method createNode(..)

def parser = new XmlParser() def response = parser.parseText(xml) def numberOfResults = parser.createNode(

response, new QName(“numberOfResults”), [:]

)

numberOfResults.value = “1” assert response.numberOfResults.text() == “1” The createNode() method receives the following parameters:

parent node (could be null)

The qualified name for the tag (In this case we only use the local part without any namespace). We’re using an instance of groovy.xml.QName

A map with the tag’s attributes (None in this particular case)

Anyway you won’t normally be creating a node from the parser instance but from the parsed XML instance. That is from a Node or a GPathResult instance.

Take a look at the next example. We are parsing the xml with XmlParser and then creating a new node from the parsed document’s instance (Notice the method here is slightly different in the way it receives the parameters):

def parser = new XmlParser() def response = parser.parseText(xml)

response.appendNode(
new QName(“numberOfResults”), [:], “1”

)

response.numberOfResults.text() == “1” When using XmlSlurper, GPathResult instances don’t have createNode() method.

4.2. Modifying / Removing nodes

We know how to parse the document, add new nodes, now I want to change a given node’s content. Let’s start using XmlParser and Node. This example changes the first book information to actually another book.

def response = new XmlParser().parseText(xml)
/* Use the same syntax as groovy.xml.MarkupBuilder */
response.value.books.book[0].replaceNode{
book(id:”3”){
title(“To Kill a Mockingbird”) author(id:”3”,”Harper Lee”)

}

}

def newNode = response.value.books.book[0]

assert newNode.name() == “book” assert newNode.@id == “3” assert newNode.title.text() == “To Kill a Mockingbird” assert newNode.author.text() == “Harper Lee” assert newNode.author.@id.first() == “3”

When using replaceNode() the closure we pass as parameter should follow the same rules as if we were using groovy.xml.MarkupBuilder:

Here’s the same example using XmlSlurper:

def response = new XmlSlurper().parseText(books)

/* Use the same syntax as groovy.xml.MarkupBuilder */ response.value.books.book[0].replaceNode{

book(id:”3”){
title(“To Kill a Mockingbird”) author(id:”3”,”Harper Lee”)

}

}

assert response.value.books.book[0].title.text() == “Don Xijote”

/* That mkp is a special namespace used to escape away from the normal building mode
of the builder and get access to helper markup methods ‘yield’, ‘pi’, ‘comment’, ‘out’, ‘namespaces’, ‘xmlDeclaration’ and ‘yieldUnescaped’ */

def result = new StreamingMarkupBuilder().bind { mkp.yield response }.toString() def changedResponse = new XmlSlurper().parseText(result)

assert changedResponse.value.books.book[0].title.text() == “To Kill a Mockingbird” Notice how using XmlSlurper we have to parse the transformed document again in order to find the created nodes. In this particular example could be a little bit annoying isn’t it?

Finally both parsers also use the same approach for adding a new attribute to a given attribute. This time again the difference is whether you want the new nodes to be available right away or not. First XmlParser:

def parser = new XmlParser() def response = parser.parseText(xml)

response.@numberOfResults = “1”

assert response.@numberOfResults == “1” And XmlSlurper:

def response = new XmlSlurper().parseText(books) response.@numberOfResults = “2”

assert response.@numberOfResults == “2” When using XmlSlurper, adding a new attribute does not require you to perform a new evaluation.

4.3. Printing XML

4.3.1. XmlUtil

Sometimes is useful to get not only the value of a given node but the node itself (for instance to add this node to another XML).

For that you can use groovy.xml.XmlUtil class. It has several static methods to serialize the xml fragment from several type of sources (Node, GPathResult, String…​)

Getting a node as a string def response = new XmlParser().parseText(xml) def nodeToSerialize = response.’**’.find {it.name() == ‘author’} def nodeAsText = XmlUtil.serialize(nodeToSerialize)

assert nodeAsText ==
XmlUtil.serialize(‘<?xml version=”1.0” encoding=”UTF-8”?><author id=”1”>Manuel De Cervantes</author>’)

工具

groovyc — the Groovy compiler

groovysh — the Groovy command -like shell

groovyConsole — the Groovy Swing console

IDE integration

这里有一些支持 Groovy 语言的 IDEs 和编辑器。

Editor Syntax highlighting Code completion Refactoring
Groovy Eclipse Plugin Yes Yes Yes
IntelliJ IDEA Yes Yes Yes
Netbeans Yes Yes Yes
Groovy and Grails Toolsuite Yes Yes Yes
Groovy Emacs Modes Yes No No
TextMate Yes No No
vim Yes No No
UltraEdit Yes No No

Indices and tables