欢迎阅读 Groovy 文档¶
原文:http://www.groovy-lang.org/documentation.html
起步¶
Groovy 安装¶
下载¶
在下载区,你可以下载 Groovy 的安装包,源文件,windows 安装文件以及文档。
为了能轻松快速的在 Mac OSX
, Linux
以及 Cygwin
上开始使用,你可以使用 GVM ( Groovy 环境管理器),下载并配置你选择的 Groovy 版本。下面是关于其的基础说明。
稳定版本¶
- 下载 zip:Binary Release | Source Release
- 下载文档:JavaDoc and zipped online documentation
- 安装包 & 源代码 & 文档 : Distribution bundle
你可以从发布记录以及变更日志中了解此版本的更多信息。
如果你计划使用 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 repositories
或 JCenter maven repository。
稳定版本¶
Gradle | Maven | Explanation |
---|---|---|
org.codehaus.groovy:groovy:2.4.5 |
|
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> |
|
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.
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 开发背景的开发人员。
这里我们将列举 Java
与 Groovy
之间所有主要不同之处。
Default imports¶
这些 packages
和 classes
是默认引入,你可以无需明确的引入,即可使用:
java.io.*
java.lang.*
java.math.BigDecimal
java.math.BigInteger
java.net.*
java.util.*
groovy.lang.*
groovy.util.*
Multi-methods¶
在 Groovy 中,在运行时选择需要被执行的方法。这被称为运行时路由 或 multi-methods
。这意味着在运行时,根据参数类型来确定调用方法。
在 Java 中,方法选择是在编译时,依赖声明的类型。
下面代码可以在 Java
和 Groovy
中编译通过,但是执行结果并不相同:
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.
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 等等
特别可以可以看一下,下面类中添加的方法:
- the
java.io.File
class: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/File.html - the
java.io.InputStream
class: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/InputStream.html - the
java.io.OutputStream
class: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/OutputStream.html - the
java.io.Reader
class: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/Reader.html - the
java.io.Writer
class: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/Writer.html - the
java.nio.file.Path
class: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/nio/file/Path.html
下面章节将主要介绍这些辅助方法使用的范例,但并不全面,如果需要了解更全面的内容,可以查看 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 提供各种集合类型,包括 lists, maps, ranges。 其中大部份都是以 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 开发包中,惯用的迭代方法为 each
和 eachWithIndex
。
值得注意的是,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.List
。
Ranges
使用 ..
定义闭区间。
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 , maps 和 ranges , Groovy 提供了大量扩展方法用于过滤,收集,分组以及计算等等,其可以在自身上执行并且更容易使用迭代处理。
具体的内容可以阅读 Groovy 开发包 API 文档:
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 将执行更多步骤,如下图

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¶
当访问的属性不存在时,会调用 propertyMissing
。
propertyMissing
方法只有一个字符串类型的入参,其参数为调用的属性名称。
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
, 这是最好方式在运行时动态注册属性并能提升整体的查找性能。
methodMissing
和 propertyMissing
处理的方法和属性,都可以通过 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
方法来达到同样的效果。
这种方法对于 POGOs
和 POJOs
都适用,看看下面的例子:
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.Integer
或 java.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)
Per instance metaclass¶
待续 (TBD)
ExpandoMetaClass¶
Groovy
中自带特殊的 MetaClass
称为 ExpandoMetaClass
。
特殊之处就在于,可以通过使用闭包方式动态的添加或改变方法,构造器,属性以及静态方法。
这些动态的处理,对于在 Test guide 章节中看到的 moking
和 stubbing
应用场景非常有用。
Groovy 对每个 java.lang.Class
都提供了一个 metaClass
特殊属性,其返回一个 ExpandoMetaClass
实例的引用。
在这实例上可以添加方法或修改已存在的方法内容。
接下来介绍 ExpandoMetaClass
在各种场景中的使用。
通过调用 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
并使用 <<
或 =
操作符, 赋值闭包代码块的方式向类中添加新增方法。
无参数方法可以使用 {-> ...}
方式添加。
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 中也有相同的应用。
构造器添加使用特殊属性 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
限定符。
class Book {
String title
}
Book.metaClass.static.create << { String title -> new Book(title:title) }
def b = Book.create("The Stand")
在 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()
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
中将有多个编译器类。
应用启动时将 encodeXXX
和 decodeXXX
方法添加到特定的 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()
在方法执行时,了解存在的其他方法或属性是十分有用的。 当前版本中,ExpandoMetaClass
提供一下方法用于此法:
- getMetaMethod
- hasMetaMethod
- getMetaProperty
- hasMetaProperty
为什么不使用放射? Groovy 中的方法分为原生方法以及运行时动态方法。 这里它们通常表示为元方法(MetaMethods)。元方法可以告诉你在运行时有效的方法。
当在重载 invokeMethod
, getProperty
或 setProperty
时这将会非常有用。
ExpandoMetaClass
的另一个特性是其允许重载 invokeMethod
, getProperty
和 setProperty
方法,这些方法都可以在 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 上的方法,无论是在运行时,还是编译时添加。
对于 setProperty
和 getProperty
同样适用。
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.
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
来获取静态元方法实例。
使用 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)¶
这种类别转换包括抽象语法树转换,能帮助去除代码样板。 这些代码都是些你必须写,但又不附带任何有用信息。 通过自动化生成代码模版,接下来需要写的代码将更加整洁,这样引入错误的可能性也会降低。
@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)
The @EqualsAndHashCode AST transformation aims at generating equals and hashCode methods for you.
@EqualsAndHashCode
用于生成 equals
和 hashcode
方法。
生成 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)
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
类型,或只有唯一一个 Map
,AbstractMap
或 HashMap
参数,那么 map-style
参数设置方式将会无效。
这些构造函数生成,按照其参数定义顺序。根据其属性数量, Groovy 将生成相应的构造函数。
@TupleConstructor
接受多种配置选择:
(TBD)
@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)
@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)
@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).
@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]
@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 时开始执行。
@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
语法。
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
,comparatorByLast
和comparatorByBorn
生成 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
方法。
你可以通过设置 includes
和 excludes
注解属性来配置生成所需要的 compareTo
方法。
includes
中设置的属性顺序决定了属性比较过程中的优先顺序。
为说明这一点,可以看看下面 Person
的定义:
@Sortable(includes='first,born') class Person {
String last
int born
String first
}
这里 Person
中将包含 comparatorByFirst
和 comparatorByBorn
比较方法,其生成的 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']
@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
:
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 结合使用,主要可以用于 includes
或 excludes
特定的属性。
Groovy 内建的构建机制,如果可以满足你的需求,也可以不急于使用 @Builder
方式:
def p2 = new Person(first: 'Keira', last: 'Knightley', born: 1985)
def p3 = new Person().with {
first = 'Geoffrey'
last = 'Rush'
born = 1951
}
创建一个 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()
这里可以使用 prefix
, includes
, excludes
以及 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 中的 buildMethodName
和 builderClassName
并不适用这种模式。
你可以将 ExternalStrategy
与 @Canonical 结合使用,主要可以用于 includes
或 excludes
特定的属性。
下面代码中将展示如何使用 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
, includes
和 excludes
注解属性自定构建过程的各个方面,下面的例子中将使用其中的一部分:
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
的使用方式:
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
使用。
这样可以通过使用 includes
或 excludes
明确特定的属性的使用。
下面代码使用 @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¶
使用这类注解声明方式,简化设计模式(代理,单例等等)的实现方式。
@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)
@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
, equals
和 hashCode
方法,但是如果修改其属性将会抛出 ReadOnlyPropertyException
异常。
@Immutable 依赖于一系列的不可变类型(例如 java.net.URI
或 java.lang.String
),如果需要使用的类型不在其中,@Immutable 将无法使用。
但是通过下面的配置方式,可以指定类型为不可变:
(TBD)
@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: 内存中保存的结果数量上限
默认情况下,缓存结果数量不受限制也不受保护。
@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 , 可以允许自定义构造函数。
弃用,使用 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
@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 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!'
}
}
@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!'
}
}
@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!'
}
}
@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 中提供了一系列注解,用于简化常见的并发模式。
@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
}
}
@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
分别用于实现 Clonable
和 Externalizable
接口。
@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.AutoClone 和 groovy.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
还支持多种配置选项:
@AutoExternalize
用于辅助创建 java.io.Externalizable
类型。其将自动添加接口并生成 writeExternal
和 readExternal
方法。
例如:
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 中提供了一些注解用于保障脚本的安全,例如生成代码允许自动中断执行过程。
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)
@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
当前还无法兼容静态方法。
这最后一个注解可以自定义模式中断脚本。 尤其是在,你希望管理资源(限制调用 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)
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 查询依赖关系,其中会提供 @Grab
的 pom.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
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 Stubbing
。Java
中,动态 mocking
框架非常流行。
这个关键原因使用 Java
手工定制来创建 mock
是一项十分困难的工作。这些框架在 Groovy
中可以很方便的使用,如果你选择自定义一些 mock
在 Groovy
中也是很简单的。
下面章节将介绍使用 Groovy
语言特性来创建 mocks
和 stubs
。
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
。
Classes
或 interfaces
中只有一个方法,可以使用强制转化为闭包块为给定的类型。需要注意,在这个过程中,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
中的 mocking
和 stubbing
类可以在 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
,当组合建立时,可以在每个元素上运行相应功能:
eachCombination
是 GDK
中方法,满足 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.shouldFail
与 GroovyTestCase.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
是针对 Java
和 Groovy
应用的测试及规范框架。使其脱引而出的是其优美和极富表现力的规范 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
描述执行返回结果。
注意,StackSpec
中 setup
方法里有一个描述字符串。描述字符串为可选,可以下在 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
和 $
方法。
领域专属语言 DSL (TBD)¶
Command chains¶
Groovy
允许在方法调用的参数周围省略括号。
在 command chain
中同样也可以如此省略括号,以及方法链之间的点号。
总体的思路是在调用 a b c d
与 a(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
成为可能。
上面例子,我们看到了基于 DSL
的 command chain
的使用。
下面例子中我们将介绍,第一个例子,使用 maps
和 closures
来创建一个 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
中的各种操作符都对应相关的方法调用。
这样可以在 Groovy
或 Java
对象上利用操作符重载。
下表中描述 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
类中只有一个需要实现的抽象方法 run
。 run
方法在脚本引擎中自动执行。在有些情况下,基类中
实现了 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 modules 和 categories
这里我们使用 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).
- 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.
- 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.
- 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 {
}
// 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
关注独立的 Script
, GroovyClassLoader
用于动态编译及加载 Groovy
类, GroovyScriptEngine
在 GroovyClassLoader
之上建立一层用于处理 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 调用脚本语言的 API。 BSF
已经有很长时间没有更新,并且在 JSR-223 中已经废弃。
Groovy
中的 BSF
引擎使用 org.codehaus.groovy.bsf.GroovyEngine
实现。事实上, BSF APIs
已经将其隐藏。通过 BSF API
使用 Groovy
和其他脚本语言一样。
由于 Groovy
其原生的支持 Java
,你所需要关心的 BSF
如何调用其他语言,例如 : JRuby
Getting started¶
将 Groovy
和 BSF
的 jars
加入到 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-223
是 Java
中调用脚本语言框架的标准接口。从 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¶
当我们谈论到 def
和 types
, 我们会看到开发人员同时使用 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)¶
Groovy
中 classes
和方法都默认为 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
中使用 getters
和 setters
来调用属性,并提供简单符号访问及修改属性。
替代 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
}
就像你看到的,一个不带访问修饰符的 field
,Groovy
编译器会为其生成 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.List
和 java.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!
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
}
你将会注意到 Groovy
的 Power 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
, Boolean
和
String
.
其类中有大量的 parse
以及如: parseText
,parseFile
等方法。
下面的例子中将使用 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. 如希望了解更多的细节,可以查看它们对应的章节,JsonBuilder 和 StreamingJsonBuilder
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
和 参数列表。
还有一些名字类似的方法,如: executeInsert
和 executeUpdate
. 在这章节中我们也会看到相关的一些例子。
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
. 这个方法将返回写入数据主键的列表结构。 executeInsert
和 execute
方法都可以
在 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]
此外, execute
和 executeInsert
中都可以使用 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-style
和 map-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 author
的 lastname
, 其后再更新 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 处理¶
- 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->
}
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.).
- 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 > 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 < 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 |