简介
Groovy……
-
是一种用于 Java 虚拟机的敏捷动态语言
-
以 Java 的优势为基础,并拥有受 Python、Ruby 和 Smalltalk 等语言启发而来的强大功能
-
以几乎零学习曲线为 Java 开发者提供现代编程特性
-
提供静态类型检查和静态编译代码以提高健壮性和性能的能力
-
支持领域特定语言和其他紧凑语法,使您的代码易于阅读和维护
-
通过其强大的处理原语、面向对象能力和 Ant DSL,使编写 shell 和构建脚本变得容易
-
通过减少开发 Web、GUI、数据库或控制台应用程序时的脚手架代码,提高开发人员生产力
-
通过开箱即用地支持单元测试和模拟,简化测试
-
与所有现有 Java 类和库无缝集成
-
直接编译为 Java 字节码,因此您可以在任何可以使用 Java 的地方使用它
1. Groovy 语言规范
1.1. 语法
本章介绍 Groovy 编程语言的语法。该语言的语法源自 Java 语法,但通过 Groovy 特有的构造对其进行了增强,并允许进行某些简化。
1.1.1. 注释
单行注释
单行注释以 //
开头,可以在行中的任何位置找到。//
后面的字符,直到行尾,都被视为注释的一部分。
// a standalone single line comment
println "hello" // a comment till the end of the line
多行注释
多行注释以 /*
开头,可以在行中的任何位置找到。/*
后面的字符将被视为注释的一部分,包括换行符,直到第一个 */
关闭注释。因此,多行注释可以放在语句的末尾,甚至可以放在语句内部。
/* a standalone multiline comment
spanning two lines */
println "hello" /* a multiline comment starting
at the end of a statement */
println 1 /* one */ + 2 /* two */
Groovydoc 注释
与多行注释类似,Groovydoc 注释也是多行的,但以 /**
开头,以 */
结尾。第一行 Groovydoc 注释后面的行可以选择以星号 *
开头。这些注释与以下内容相关联:
-
类型定义(类、接口、枚举、注解),
-
字段和属性定义
-
方法定义
尽管编译器不会抱怨 Groovydoc 注释未与上述语言元素相关联,但您应该将这些构造的注释紧接在其之前。
/**
* A Class description
*/
class Person {
/** the name of the person */
String name
/**
* Creates a greeting method for a certain person.
*
* @param otherPerson the person to greet
* @return a greeting message
*/
String greet(String otherPerson) {
"Hello ${otherPerson}"
}
}
Groovydoc 遵循与 Java 自己的 Javadoc 相同的约定。因此,您可以使用与 Javadoc 相同的标签。
此外,Groovy 从 3.0.0 版本开始支持**运行时 Groovydoc**,即 Groovydoc 可以在运行时保留。
运行时 Groovydoc 默认禁用。可以通过添加 JVM 选项 -Dgroovy.attach.runtime.groovydoc=true 来启用它。 |
运行时 Groovydoc 以 /**@
开头,以 */
结尾,例如
/**@
* Some class groovydoc for Foo
*/
class Foo {
/**@
* Some method groovydoc for bar
*/
void bar() {
}
}
assert Foo.class.groovydoc.content.contains('Some class groovydoc for Foo') (1)
assert Foo.class.getMethod('bar', new Class[0]).groovydoc.content.contains('Some method groovydoc for bar') (2)
1 | 获取类 Foo 的运行时 Groovydoc |
2 | 获取方法 bar 的运行时 Groovydoc |
Shebang 行
除了单行注释之外,还有一种特殊的行注释,通常称为 shebang 行,它被 UNIX 系统理解,允许脚本直接从命令行运行,前提是您已经安装了 Groovy 分发版并且 groovy
命令在 PATH
中可用。
#!/usr/bin/env groovy
println "Hello from the shebang line"
# 字符必须是文件的第一个字符。任何缩进都会导致编译错误。 |
1.1.2. 关键字
Groovy 具有以下保留关键字
abstract |
assert |
break |
case |
catch |
class |
const |
continue |
def |
default |
do |
else |
enum |
extends |
final |
finally |
for |
goto |
if |
implements |
import |
instanceof |
interface |
native |
new |
null |
non-sealed |
package |
public |
protected |
private |
return |
static |
strictfp |
super |
switch |
synchronized |
this |
threadsafe |
throw |
throws |
transient |
try |
while |
其中,const
、goto
、strictfp
和 threadsafe
目前未使用。
保留关键字通常不能用于变量名、字段名和方法名。
此外,Groovy 还有以下上下文关键字
as |
in |
permits |
record |
sealed |
trait |
var |
yields |
这些词仅在某些上下文中是关键字,在某些地方可以更自由地使用,特别是用于变量、字段和方法名。
对保留关键字的限制也适用于原始类型、布尔字面量和 null 字面量(所有这些都将在后面讨论)
null |
true |
false |
boolean |
char |
byte |
short |
int |
long |
float |
double |
1.1.3. 标识符
普通标识符
标识符以字母、美元符号或下划线开头。它们不能以数字开头。
字母可以在以下范围
-
'a' 到 'z'(小写 ascii 字母)
-
'A' 到 'Z'(大写 ascii 字母)
-
'\u00C0' 到 '\u00D6'
-
'\u00D8' 到 '\u00F6'
-
'\u00F8' 到 '\u00FF'
-
'\u0100' 到 '\uFFFE'
然后,后续字符可以包含字母和数字。
以下是一些有效标识符(此处为变量名)的示例
def name
def item3
def with_underscore
def $dollarStart
但以下是无效标识符
def 3tier
def a+b
def a#b
所有关键字在点号后也是有效标识符
foo.as
foo.assert
foo.break
foo.case
foo.catch
带引号的标识符
带引号的标识符出现在点表达式的点号之后。例如,person.name
表达式中的 name
部分可以用 person."name"
或 person.'name'
加引号。当某些标识符包含 Java 语言规范禁止但在 Groovy 中加引号允许的非法字符时,这尤其有趣。例如,像破折号、空格、感叹号等字符。
def map = [:]
map."an identifier with a space and double quotes" = "ALLOWED"
map.'with-dash-signs-and-single-quotes' = "ALLOWED"
assert map."an identifier with a space and double quotes" == "ALLOWED"
assert map.'with-dash-signs-and-single-quotes' == "ALLOWED"
正如我们将在字符串的以下部分中看到的那样,Groovy 提供了不同的字符串字面量。所有类型的字符串实际上都允许在点号之后使用
map.'single quote'
map."double quote"
map.'''triple single quote'''
map."""triple double quote"""
map./slashy string/
map.$/dollar slashy string/$
纯字符串和 Groovy 的 GString(插值字符串)之间存在差异,因为在后一种情况下,插值值会插入到最终字符串中以评估整个标识符
def firstname = "Homer"
map."Simpson-${firstname}" = "Homer Simpson"
assert map.'Simpson-Homer' == "Homer Simpson"
1.1.4. 字符串
文本字面量以字符链的形式表示,称为字符串。Groovy 允许您实例化 java.lang.String
对象,以及 GString(groovy.lang.GString
),后者在其他编程语言中也称为插值字符串。
三单引号字符串
三单引号字符串是由三组单引号包围的一系列字符
'''a triple-single-quoted string'''
三单引号字符串是纯粹的 java.lang.String ,不支持插值。 |
三单引号字符串可以跨多行。字符串内容可以跨行边界,而无需将字符串拆分成多个部分,也无需连接或换行转义字符
def aMultilineString = '''line one
line two
line three'''
如果您的代码有缩进,例如在类的方法主体中,您的字符串将包含缩进的空格。Groovy Development Kit 包含使用 String#stripIndent()
方法和使用 String#stripMargin()
方法(该方法接受一个分隔符字符来标识要从字符串开头删除的文本)来去除缩进的方法。
当按如下方式创建字符串时
def startingAndEndingWithANewline = '''
line one
line two
line three
'''
您会注意到结果字符串包含一个换行符作为第一个字符。可以通过用反斜杠转义换行符来去除该字符
def strippedFirstNewline = '''\
line one
line two
line three
'''
assert !strippedFirstNewline.startsWith('\n')
转义特殊字符
您可以用反斜杠字符转义单引号,以避免终止字符串字面量
'an escaped single quote: \' needs a backslash'
您可以用双反斜杠转义转义字符本身
'an escaped escape character: \\ needs a double backslash'
一些特殊字符也使用反斜杠作为转义字符
转义序列 | 字符 |
---|---|
\b |
退格 |
\f |
换页 |
\n |
换行 |
\r |
回车 |
\s |
单个空格 |
\t |
制表符 |
\\ |
反斜杠 |
\' |
单引号字符串中的单引号(三单引号和双引号字符串中可选) |
\" |
双引号字符串中的双引号(三双引号和单引号字符串中可选) |
当我们讨论其他类型的字符串时,我们会看到更多转义细节。
Unicode 转义序列
对于键盘上不存在的字符,您可以使用 unicode 转义序列:反斜杠,后跟 'u',然后是 4 位十六进制数字。
例如,欧元货币符号可以用以下方式表示
'The Euro currency symbol: \u20AC'
双引号字符串
双引号字符串是由双引号包围的一系列字符
"a double-quoted string"
如果没有插值表达式,双引号字符串是普通的 java.lang.String ;如果存在插值,则它们是 groovy.lang.GString 实例。 |
要转义双引号,可以使用反斜杠字符:“一个双引号:\"”。 |
字符串插值
任何 Groovy 表达式都可以插入到所有字符串字面量中,除了单引号和三单引号字符串。插值是指在字符串评估时将字符串中的占位符替换为其值的行为。占位符表达式由 ${}
包围。对于明确的点式表达式,可以省略花括号,即在这些情况下我们可以只使用 $ 前缀。如果 GString 被传递给接受 String 的方法,则占位符内的表达式值会评估为其字符串表示(通过在该表达式上调用 toString()
),并将生成的 String 传递给方法。
这里,我们有一个字符串,其中包含引用局部变量的占位符
def name = 'Guillaume' // a plain string
def greeting = "Hello ${name}"
assert greeting.toString() == 'Hello Guillaume'
任何 Groovy 表达式都是有效的,正如我们在算术表达式的这个示例中看到的
def sum = "The sum of 2 and 3 equals ${2 + 3}"
assert sum.toString() == 'The sum of 2 and 3 equals 5'
不仅表达式允许在 ${} 占位符之间,语句也允许。但是,语句的值只是 null 。因此,如果将多个语句插入到该占位符中,最后一个语句应以某种方式返回一个有意义的值才能插入。例如,“1 和 2 的和等于 ${def a = 1; def b = 2; a + b}” 受支持并按预期工作,但良好的实践通常是只在 GString 占位符内部使用简单表达式。 |
除了 ${}
占位符之外,我们还可以使用单独的 $
符号作为点表达式的前缀
def person = [name: 'Guillaume', age: 36]
assert "$person.name is $person.age years old" == 'Guillaume is 36 years old'
但只有 a.b
、a.b.c
等形式的点表达式是有效的。包含括号(如方法调用)、闭包的花括号、不属于属性表达式的点号或算术运算符的表达式都是无效的。给定以下数字变量定义:
def number = 3.14
以下语句将抛出 groovy.lang.MissingPropertyException
,因为 Groovy 认为您正在尝试访问该数字的 toString
属性,而该属性不存在
shouldFail(MissingPropertyException) {
println "$number.toString()"
}
您可以将 "$number.toString()" 视为由解析器解释为 "${number.toString}()" 。 |
同样,如果表达式不明确,您需要保留花括号
String thing = 'treasure'
assert 'The x-coordinate of the treasure is represented by treasure.x' ==
"The x-coordinate of the $thing is represented by $thing.x" // <= Not allowed: ambiguous!!
assert 'The x-coordinate of the treasure is represented by treasure.x' ==
"The x-coordinate of the $thing is represented by ${thing}.x" // <= Curly braces required
如果您需要在 GString 中转义 $
或 ${}
占位符,以便它们按原样出现而没有插值,您只需使用 \
反斜杠字符来转义美元符号
assert '$5' == "\$5"
assert '${name}' == "\${name}"
闭包表达式插值的特殊情况
到目前为止,我们已经看到可以在 ${}
占位符中插入任意表达式,但对于闭包表达式有特殊情况和表示法。当占位符包含箭头 ${→}
时,该表达式实际上是一个闭包表达式——您可以将其视为一个前面加了美元符号的闭包
def sParameterLessClosure = "1 + 2 == ${-> 3}" (1)
assert sParameterLessClosure == '1 + 2 == 3'
def sOneParamClosure = "1 + 2 == ${ w -> w << 3}" (2)
assert sOneParamClosure == '1 + 2 == 3'
1 | 该闭包是一个无参数闭包,不接受任何参数。 |
2 | 这里,闭包接受一个 java.io.StringWriter 参数,您可以使用 << 左移运算符向其中追加内容。在这两种情况下,两个占位符都是嵌入式闭包。 |
从表面上看,它看起来像是定义要插入的表达式的一种更冗长的方式,但闭包比单纯的表达式有一个有趣的优势:惰性求值。
让我们考虑以下示例
def number = 1 (1)
def eagerGString = "value == ${number}"
def lazyGString = "value == ${ -> number }"
assert eagerGString == "value == 1" (2)
assert lazyGString == "value == 1" (3)
number = 2 (4)
assert eagerGString == "value == 1" (5)
assert lazyGString == "value == 2" (6)
1 | 我们定义了一个包含 1 的 number 变量,然后将其插入到两个 GString 中,一个作为 eagerGString 中的表达式,另一个作为 lazyGString 中的闭包。 |
2 | 我们期望 eagerGString 的结果字符串包含相同的字符串值 1。 |
3 | lazyGString 也类似 |
4 | 然后我们把变量的值改成一个新数字 |
5 | 对于普通插值表达式,值实际上是在创建 GString 时绑定的。 |
6 | 但是对于闭包表达式,每次将 GString 强制转换为 String 时都会调用闭包,从而生成包含新数字值的更新字符串。 |
嵌入的闭包表达式如果包含多个参数,将在运行时生成异常。只允许使用零个或一个参数的闭包。 |
与 Java 的互操作性
当一个方法(无论是在 Java 中实现还是在 Groovy 中实现)期望一个 java.lang.String
,但我们传递一个 groovy.lang.GString
实例时,GString 的 toString()
方法会自动透明地被调用。
String takeString(String message) { (4)
assert message instanceof String (5)
return message
}
def message = "The message is ${'hello'}" (1)
assert message instanceof GString (2)
def result = takeString(message) (3)
assert result instanceof String
assert result == 'The message is hello'
1 | 我们创建一个 GString 变量 |
2 | 我们再次确认它是 GString 的一个实例 |
3 | 然后我们将该 GString 传递给一个接受 String 作为参数的方法 |
4 | takeString() 方法的签名明确指出其唯一参数是一个 String |
5 | 我们还验证了参数确实是 String 而不是 GString。 |
GString 和 String 的哈希码
尽管插值字符串可以代替普通的 Java 字符串使用,但它们在一种特殊方式上与字符串不同:它们的哈希码不同。普通的 Java 字符串是不可变的,而 GString 的结果字符串表示可能会根据其插值值而变化。即使对于相同的最终字符串,GString 和 String 的哈希码也不同。
assert "one: ${1}".hashCode() != "one: 1".hashCode()
GString 和 String 具有不同的哈希码值,因此应避免将 GString 用作 Map 键,特别是当我们尝试使用 String 而不是 GString 检索关联值时。
def key = "a"
def m = ["${key}": "letter ${key}"] (1)
assert m["a"] == null (2)
1 | 该映射以一个初始对创建,其键是 GString |
2 | 当我们尝试使用 String 键获取值时,我们将找不到它,因为 String 和 GString 具有不同的哈希码值 |
三双引号字符串
三双引号字符串的行为与双引号字符串类似,此外它们是多行的,就像三单引号字符串一样。
def name = 'Groovy'
def template = """
Dear Mr ${name},
You're the winner of the lottery!
Yours sincerly,
Dave
"""
assert template.toString().contains('Groovy')
在三双引号字符串中,双引号和单引号都不需要转义。 |
斜线字符串
除了常用的带引号字符串,Groovy 还提供斜线字符串,它使用 /
作为开头和结尾的分隔符。斜线字符串对于定义正则表达式和模式特别有用,因为不需要转义反斜杠。
斜线字符串示例
def fooPattern = /.*foo.*/
assert fooPattern == '.*foo.*'
只有正斜杠需要用反斜杠转义
def escapeSlash = /The character \/ is a forward slash/
assert escapeSlash == 'The character / is a forward slash'
斜线字符串是多行的
def multilineSlashy = /one
two
three/
assert multilineSlashy.contains('\n')
斜线字符串可以被认为是定义 GString 的另一种方式,但具有不同的转义规则。因此它们支持插值
def color = 'blue'
def interpolatedSlashy = /a ${color} car/
assert interpolatedSlashy == 'a blue car'
特殊情况
空斜线字符串不能用双斜线表示,因为 Groovy 解析器会将其理解为行注释。这就是为什么下面的断言实际上不会编译,因为它看起来像一个未终止的语句
assert '' == //
由于斜线字符串主要设计用于简化正则表达式,因此 GString 中的一些错误(如 $()
或 $5
)将适用于斜线字符串。
请记住,不需要转义反斜杠。另一种思考方式是,实际上不支持转义。斜线字符串 /\t/
不会包含制表符,而是包含反斜杠后跟字符 't'。只允许转义斜杠字符,即 /\/folder/
将是包含 '/folder'
的斜线字符串。斜杠转义的后果是斜线字符串不能以反斜杠结尾。否则,那将转义斜线字符串终止符。您可以使用特殊技巧,/ends with slash ${'\'}/
。但最好在这种情况下避免使用斜线字符串。
美元斜线字符串
美元斜线字符串是多行 GString,以起始 $/
和结束 /$
分隔。转义字符是美元符号,它可以转义另一个美元符号,或一个正斜杠。只有当与这些字符的特殊用途发生冲突时,才需要转义美元符号和正斜杠字符。字符 $foo
通常表示 GString 占位符,因此可以通过转义美元符号,即 $$foo
,将这四个字符输入到美元斜线字符串中。同样,如果您希望美元斜线结束分隔符出现在您的字符串中,您需要对其进行转义。
以下是一些示例
def name = "Guillaume"
def date = "April, 1st"
def dollarSlashy = $/
Hello $name,
today we're ${date}.
$ dollar sign
$$ escaped dollar sign
\ backslash
/ forward slash
$/ escaped forward slash
$$$/ escaped opening dollar slashy
$/$$ escaped closing dollar slashy
/$
assert [
'Guillaume',
'April, 1st',
'$ dollar sign',
'$ escaped dollar sign',
'\\ backslash',
'/ forward slash',
'/ escaped forward slash',
'$/ escaped opening dollar slashy',
'/$ escaped closing dollar slashy'
].every { dollarSlashy.contains(it) }
它旨在克服斜线字符串转义规则的一些限制。当其转义规则适合您的字符串内容时(通常如果它包含一些您不想转义的斜线),请使用它。
字符串总结表
字符串名称 |
字符串语法 |
插值 |
多行 |
转义字符 |
单引号 |
|
|
||
三单引号 |
|
|
||
双引号 |
|
|
||
三双引号 |
|
|
||
斜线 |
|
|
||
美元斜线 |
|
|
字符
与 Java 不同,Groovy 没有明确的字符字面量。但是,您可以通过三种不同的方式明确地将 Groovy 字符串转换为实际字符
char c1 = 'A' (1)
assert c1 instanceof Character
def c2 = 'B' as char (2)
assert c2 instanceof Character
def c3 = (char)'C' (3)
assert c3 instanceof Character
1 | 通过在声明字符变量时明确指定 char 类型 |
2 | 通过使用 as 运算符进行类型强制转换 |
3 | 通过使用强制转换为 char 操作 |
第一个选项 1 在字符存储在变量中时很有趣,而其他两个选项(2 和 3)在需要将 char 值作为方法调用的参数传递时更有趣。 |
1.1.5. 数字
Groovy 支持不同类型的整数字面量和十进制字面量,由 Java 的常用 Number
类型支持。
整型字面量
整数文字类型与 Java 中的相同
-
byte
-
char
-
short
-
int
-
long
-
java.math.BigInteger
您可以使用以下声明创建这些类型的整数数字
// primitive types
byte b = 1
char c = 2
short s = 3
int i = 4
long l = 5
// infinite precision
BigInteger bi = 6
如果您使用 def
关键字进行可选类型,则整数的类型将有所不同:它会适应可以容纳该数字的类型的容量。
对于正数
def a = 1
assert a instanceof Integer
// Integer.MAX_VALUE
def b = 2147483647
assert b instanceof Integer
// Integer.MAX_VALUE + 1
def c = 2147483648
assert c instanceof Long
// Long.MAX_VALUE
def d = 9223372036854775807
assert d instanceof Long
// Long.MAX_VALUE + 1
def e = 9223372036854775808
assert e instanceof BigInteger
对于负数也一样
def na = -1
assert na instanceof Integer
// Integer.MIN_VALUE
def nb = -2147483648
assert nb instanceof Integer
// Integer.MIN_VALUE - 1
def nc = -2147483649
assert nc instanceof Long
// Long.MIN_VALUE
def nd = -9223372036854775808
assert nd instanceof Long
// Long.MIN_VALUE - 1
def ne = -9223372036854775809
assert ne instanceof BigInteger
其他非十进制表示
数字也可以用二进制、八进制、十六进制和十进制表示。
二进制数字以 0b
前缀开头
int xInt = 0b10101111
assert xInt == 175
short xShort = 0b11001001
assert xShort == 201 as short
byte xByte = 0b11
assert xByte == 3 as byte
long xLong = 0b101101101101
assert xLong == 2925l
BigInteger xBigInteger = 0b111100100001
assert xBigInteger == 3873g
int xNegativeInt = -0b10101111
assert xNegativeInt == -175
八进制数字以典型的 0
格式后跟八进制数字指定。
int xInt = 077
assert xInt == 63
short xShort = 011
assert xShort == 9 as short
byte xByte = 032
assert xByte == 26 as byte
long xLong = 0246
assert xLong == 166l
BigInteger xBigInteger = 01111
assert xBigInteger == 585g
int xNegativeInt = -077
assert xNegativeInt == -63
十六进制数字以典型的 0x
格式后跟十六进制数字指定。
int xInt = 0x77
assert xInt == 119
short xShort = 0xaa
assert xShort == 170 as short
byte xByte = 0x3a
assert xByte == 58 as byte
long xLong = 0xffff
assert xLong == 65535l
BigInteger xBigInteger = 0xaaaa
assert xBigInteger == 43690g
Double xDouble = new Double('0x1.0p0')
assert xDouble == 1.0d
int xNegativeInt = -0x77
assert xNegativeInt == -119
十进制字面量
十进制字面量类型与 Java 中的相同
-
float
-
double
-
java.math.BigDecimal
您可以使用以下声明创建这些类型的十进制数字
// primitive types
float f = 1.234
double d = 2.345
// infinite precision
BigDecimal bd = 3.456
十进制数可以使用指数,即 e
或 E
指数字母,后跟可选的符号和表示指数的整数
assert 1e3 == 1_000.0
assert 2E4 == 20_000.0
assert 3e+1 == 30.0
assert 4E-2 == 0.04
assert 5e-1 == 0.5
为了方便精确的十进制数计算,Groovy 选择 java.math.BigDecimal
作为其十进制数类型。此外,Groovy 也支持 float
和 double
类型,但需要显式类型声明、类型强制转换或后缀。即使 BigDecimal
是十进制数的默认类型,接受 float
或 double
作为参数类型的方法或闭包也接受此类字面量。
十进制数不能使用二进制、八进制或十六进制表示。 |
字面量中的下划线
在编写长数字字面量时,很难一眼看出数字是如何分组的,例如按千位或单词分组。通过允许您在数字字面量中放置下划线,可以更容易地识别这些分组。
long creditCardNumber = 1234_5678_9012_3456L
long socialSecurityNumbers = 999_99_9999L
double monetaryAmount = 12_345_132.12
long hexBytes = 0xFF_EC_DE_5E
long hexWords = 0xFFEC_DE5E
long maxLong = 0x7fff_ffff_ffff_ffffL
long alsoMaxLong = 9_223_372_036_854_775_807L
long bytes = 0b11010010_01101001_10010100_10010010
数字类型后缀
我们可以通过添加后缀(见下表),无论是大写还是小写,来强制数字(包括二进制、八进制和十六进制)具有特定类型。
类型 | 后缀 |
---|---|
BigInteger |
|
Long |
|
Integer |
|
BigDecimal |
|
Double |
|
Float |
|
示例
assert 42I == Integer.valueOf('42')
assert 42i == Integer.valueOf('42') // lowercase i more readable
assert 123L == Long.valueOf("123") // uppercase L more readable
assert 2147483648 == Long.valueOf('2147483648') // Long type used, value too large for an Integer
assert 456G == new BigInteger('456')
assert 456g == new BigInteger('456')
assert 123.45 == new BigDecimal('123.45') // default BigDecimal type used
assert .321 == new BigDecimal('.321')
assert 1.200065D == Double.valueOf('1.200065')
assert 1.234F == Float.valueOf('1.234')
assert 1.23E23D == Double.valueOf('1.23E23')
assert 0b1111L.class == Long // binary
assert 0xFFi.class == Integer // hexadecimal
assert 034G.class == BigInteger // octal
数学运算
尽管运算符在其他地方有更详细的介绍,但讨论数学运算的行为及其结果类型很重要。
除法和幂运算的二元操作(下文讨论),
-
byte
、char
、short
和int
之间的二元操作结果为int
-
涉及
long
与byte
、char
、short
和int
的二元操作结果为long
-
涉及
BigInteger
和任何其他整数类型的二元操作结果为BigInteger
-
涉及
BigDecimal
与byte
、char
、short
、int
和BigInteger
的二元操作结果为BigDecimal
-
float
、double
和BigDecimal
之间的二元操作结果为double
-
两个
BigDecimal
之间的二元操作结果为BigDecimal
下表总结了这些规则
byte | char | short | int | long | BigInteger | float | double | BigDecimal | |
---|---|---|---|---|---|---|---|---|---|
byte |
int |
int |
int |
int |
long |
BigInteger |
double |
double |
BigDecimal |
char |
int |
int |
int |
long |
BigInteger |
double |
double |
BigDecimal |
|
short |
int |
int |
long |
BigInteger |
double |
double |
BigDecimal |
||
int |
int |
long |
BigInteger |
double |
double |
BigDecimal |
|||
long |
long |
BigInteger |
double |
double |
BigDecimal |
||||
BigInteger |
BigInteger |
double |
double |
BigDecimal |
|||||
float |
double |
double |
double |
||||||
double |
double |
double |
|||||||
BigDecimal |
BigDecimal |
得益于 Groovy 的运算符重载,常用的算术运算符也适用于 BigInteger 和 BigDecimal ,这与 Java 不同,在 Java 中您必须使用显式方法来操作这些数字。 |
除法运算符的情况
如果任一操作数是 float
或 double
,则除法运算符 /
(以及用于除法和赋值的 /=
)会产生 double
结果;否则(当两个操作数是整数类型 short
、char
、byte
、int
、long
、BigInteger
或 BigDecimal
的任意组合时)会产生 BigDecimal
结果。
如果除法是精确的(即产生的结果可以在相同精度和范围的限制内表示),则使用 divide()
方法执行 BigDecimal
除法,或者使用 MathContext
,其精度为两个操作数精度的最大值加上额外的 10 精度,以及范围为 10 和操作数范围的最大值。
对于像 Java 中那样的整数除法,您应该使用 intdiv() 方法,因为 Groovy 没有提供专用的整数除法运算符符号。 |
幂运算符的情况
幂运算由 **
运算符表示,有两个参数:底数和指数。幂运算的结果取决于其操作数以及运算的结果(特别是结果是否可以表示为整数值)。
Groovy 的幂运算使用以下规则来确定结果类型
-
如果指数是小数
-
如果结果可以表示为
Integer
,则返回Integer
-
否则,如果结果可以表示为
Long
,则返回Long
-
否则返回
Double
-
-
如果指数是整数
-
如果指数严格为负,则返回
Integer
、Long
或Double
(如果结果值适合该类型) -
如果指数为正或为零
-
如果底数是
BigDecimal
,则返回BigDecimal
结果值 -
如果底数是
BigInteger
,则返回BigInteger
结果值 -
如果底数是
Integer
,则如果结果值适合它,则返回Integer
,否则返回BigInteger
-
如果底数是
Long
,则如果结果值适合它,则返回Long
,否则返回BigInteger
-
-
我们可以用几个例子来说明这些规则
// base and exponent are ints and the result can be represented by an Integer
assert 2 ** 3 instanceof Integer // 8
assert 10 ** 9 instanceof Integer // 1_000_000_000
// the base is a long, so fit the result in a Long
// (although it could have fit in an Integer)
assert 5L ** 2 instanceof Long // 25
// the result can't be represented as an Integer or Long, so return a BigInteger
assert 100 ** 10 instanceof BigInteger // 10e20
assert 1234 ** 123 instanceof BigInteger // 170515806212727042875...
// the base is a BigDecimal and the exponent a negative int
// but the result can be represented as an Integer
assert 0.5 ** -2 instanceof Integer // 4
// the base is an int, and the exponent a negative float
// but again, the result can be represented as an Integer
assert 1 ** -0.3f instanceof Integer // 1
// the base is an int, and the exponent a negative int
// but the result will be calculated as a Double
// (both base and exponent are actually converted to doubles)
assert 10 ** -1 instanceof Double // 0.1
// the base is a BigDecimal, and the exponent is an int, so return a BigDecimal
assert 1.2 ** 10 instanceof BigDecimal // 6.1917364224
// the base is a float or double, and the exponent is an int
// but the result can only be represented as a Double value
assert 3.4f ** 5 instanceof Double // 454.35430372146965
assert 5.6d ** 2 instanceof Double // 31.359999999999996
// the exponent is a decimal value
// and the result can only be represented as a Double value
assert 7.8 ** 1.9 instanceof Double // 49.542708423868476
assert 2 ** 0.1f instanceof Double // 1.0717734636432956
1.1.6. 布尔值
布尔值是一种特殊的数据类型,用于表示真值:true
和 false
。将此数据类型用于跟踪真/假条件的简单标志。
布尔值可以像任何其他数据类型一样存储在变量中,分配给字段
def myBooleanVariable = true
boolean untypedBooleanVar = false
booleanField = true
true
和 false
是仅有的两个原始布尔值。但是,可以使用逻辑运算符表示更复杂的布尔表达式。
此外,Groovy 有特殊规则(通常称为 Groovy Truth)用于将非布尔对象强制转换为布尔值。
1.1.7. 列表
Groovy 使用逗号分隔的值列表,用方括号括起来,表示列表。Groovy 列表是普通的 JDK java.util.List
,因为 Groovy 没有定义自己的集合类。定义列表字面量时使用的具体列表实现默认是 java.util.ArrayList
,除非您决定另行指定,我们稍后会看到。
def numbers = [1, 2, 3] (1)
assert numbers instanceof List (2)
assert numbers.size() == 3 (3)
1 | 我们定义一个由逗号分隔并用方括号括起来的数字列表,并将该列表分配给一个变量 |
2 | 该列表是 Java 的 java.util.List 接口的一个实例 |
3 | 列表的大小可以通过 size() 方法查询,结果显示我们的列表包含 3 个元素 |
在上面的例子中,我们使用了同质列表,但您也可以创建包含异质类型值的列表
def heterogeneous = [1, "a", true] (1)
1 | 这里的列表包含一个数字、一个字符串和一个布尔值 |
我们提到过,默认情况下,列表字面量实际上是 java.util.ArrayList
的实例,但是,由于使用了 as
运算符进行类型强制转换,或者对变量进行显式类型声明,因此可以使用不同的支持类型来创建列表
def arrayList = [1, 2, 3]
assert arrayList instanceof java.util.ArrayList
def linkedList = [2, 3, 4] as LinkedList (1)
assert linkedList instanceof java.util.LinkedList
LinkedList otherLinked = [3, 4, 5] (2)
assert otherLinked instanceof java.util.LinkedList
1 | 我们使用 as 运算符进行强制转换,以显式请求 java.util.LinkedList 实现 |
2 | 我们可以说保存列表字面量的变量类型是 java.util.LinkedList |
您可以使用 []
下标运算符(用于读取和设置值)访问列表元素,使用正索引或负索引从列表末尾访问元素,以及使用范围,并使用 <<
左移运算符将元素附加到列表
def letters = ['a', 'b', 'c', 'd']
assert letters[0] == 'a' (1)
assert letters[1] == 'b'
assert letters[-1] == 'd' (2)
assert letters[-2] == 'c'
letters[2] = 'C' (3)
assert letters[2] == 'C'
letters << 'e' (4)
assert letters[ 4] == 'e'
assert letters[-1] == 'e'
assert letters[1, 3] == ['b', 'd'] (5)
assert letters[2..4] == ['C', 'd', 'e'] (6)
1 | 访问列表的第一个元素(从零开始计数) |
2 | 使用负索引访问列表的最后一个元素:-1 是列表末尾的第一个元素 |
3 | 使用赋值为列表的第三个元素设置新值 |
4 | 使用 << 左移运算符将元素添加到列表末尾 |
5 | 一次访问两个元素,返回包含这两个元素的新列表 |
6 | 使用范围访问列表中的一系列值,从起始元素位置到结束元素位置 |
由于列表的性质可以是异构的,列表还可以包含其他列表以创建多维列表
def multi = [[0, 1], [2, 3]] (1)
assert multi[1][0] == 2 (2)
1 | 定义一个数字列表 |
2 | 访问最顶层列表的第二个元素和内部列表的第一个元素 |
1.1.8. 数组
Groovy 重用列表表示法来表示数组,但要使此类字面量成为数组,您需要通过强制转换或类型声明显式定义数组的类型。
String[] arrStr = ['Ananas', 'Banana', 'Kiwi'] (1)
assert arrStr instanceof String[] (2)
assert !(arrStr instanceof List)
def numArr = [1, 2, 3] as int[] (3)
assert numArr instanceof int[] (4)
assert numArr.size() == 3
1 | 使用显式变量类型声明定义字符串数组 |
2 | 断言我们创建了一个字符串数组 |
3 | 使用 as 运算符创建 int 数组 |
4 | 断言我们创建了一个基本 int 数组 |
您还可以创建多维数组
def matrix3 = new Integer[3][3] (1)
assert matrix3.size() == 3
Integer[][] matrix2 (2)
matrix2 = [[1, 2], [3, 4]]
assert matrix2 instanceof Integer[][]
1 | 您可以定义新数组的界限 |
2 | 或声明数组而不指定其界限 |
访问数组元素遵循与列表相同的表示法
String[] names = ['Cédric', 'Guillaume', 'Jochen', 'Paul']
assert names[0] == 'Cédric' (1)
names[2] = 'Blackdrag' (2)
assert names[2] == 'Blackdrag'
1 | 检索数组的第一个元素 |
2 | 将数组的第三个元素的值设置为新值 |
Java 风格数组初始化
Groovy 始终支持使用方括号定义字面量列表/数组,并避免使用 Java 风格的花括号,以免与闭包定义冲突。但是,在花括号紧随数组类型声明之后的情况下,与闭包定义没有歧义,因此 Groovy 3 及更高版本支持 Java 数组初始化表达式的这种变体。
示例
def primes = new int[] {2, 3, 5, 7, 11}
assert primes.size() == 5 && primes.sum() == 28
assert primes.class.name == '[I'
def pets = new String[] {'cat', 'dog'}
assert pets.size() == 2 && pets.sum() == 'catdog'
assert pets.class.name == '[Ljava.lang.String;'
// traditional Groovy alternative still supported
String[] groovyBooks = [ 'Groovy in Action', 'Making Java Groovy' ]
assert groovyBooks.every{ it.contains('Groovy') }
1.1.9. 映射
有时在其他语言中称为字典或关联数组,Groovy 具有映射。映射将键与值关联起来,用冒号分隔键和值,用逗号分隔每个键/值对,并用方括号括起整个键和值。
def colors = [red: '#FF0000', green: '#00FF00', blue: '#0000FF'] (1)
assert colors['red'] == '#FF0000' (2)
assert colors.green == '#00FF00' (3)
colors['pink'] = '#FF00FF' (4)
colors.yellow = '#FFFF00' (5)
assert colors.pink == '#FF00FF'
assert colors['yellow'] == '#FFFF00'
assert colors instanceof java.util.LinkedHashMap
1 | 我们定义一个字符串颜色名称的映射,与它们的十六进制编码的 html 颜色相关联 |
2 | 我们使用下标表示法检查与 red 键关联的内容 |
3 | 我们还可以使用属性表示法断言绿色颜色的十六进制表示 |
4 | 同样,我们可以使用下标表示法添加新的键/值对 |
5 | 或者使用属性表示法,添加 yellow 颜色 |
当使用名称作为键时,我们实际上在映射中定义了字符串键。 |
Groovy 创建的映射实际上是 java.util.LinkedHashMap 的实例。 |
如果您尝试访问映射中不存在的键
assert colors.unknown == null
def emptyMap = [:]
assert emptyMap.anyKey == null
您将检索到 null
结果。
在上面的示例中,我们使用了字符串键,但您也可以使用其他类型的值作为键
def numbers = [1: 'one', 2: 'two']
assert numbers[1] == 'one'
在这里,我们使用数字作为键,因为数字可以明确地识别为数字,所以 Groovy 不会像我们前面的例子那样创建字符串键。但是,考虑您想传递一个变量而不是键的情况,以便该变量的值成为键
def key = 'name'
def person = [key: 'Guillaume'] (1)
assert !person.containsKey('name') (2)
assert person.containsKey('key') (3)
1 | 与 'Guillaume' 名称关联的 key 实际上是字符串 "key" ,而不是与 key 变量关联的值 |
2 | 该映射不包含 'name' 键 |
3 | 相反,该映射包含一个 'key' 键 |
您也可以将带引号的字符串作为键传递:["name": "Guillaume"]。如果您的键字符串不是有效标识符,例如如果您想创建一个包含破折号的字符串键,例如:["street-name": "Main street"],则这是强制性的。 |
当您需要在映射定义中将变量值作为键传递时,必须用括号将变量或表达式括起来
person = [(key): 'Guillaume'] (1)
assert person.containsKey('name') (2)
assert !person.containsKey('key') (3)
1 | 这次,我们用括号将 key 变量括起来,以指示解析器我们正在传递一个变量,而不是定义一个字符串键 |
2 | 该映射确实包含 name 键 |
3 | 但该映射不像以前那样包含 key 键 |
1.2. 运算符
本章介绍 Groovy 编程语言的运算符。
1.2.1. 算术运算符
Groovy 支持您在数学和其他编程语言(如 Java)中找到的常用算术运算符。所有 Java 算术运算符都受支持。让我们通过以下示例来了解它们。
普通算术运算符
Groovy 中提供了以下二元算术运算符
运算符 | 目的 | 备注 |
---|---|---|
|
加法 |
|
|
减法 |
|
|
乘法 |
|
|
除法 |
对于整数除法,请使用 |
|
求余 |
|
|
乘方 |
有关运算返回类型的更多信息,请参阅关于乘方运算的部分。 |
以下是这些运算符的一些使用示例
assert 1 + 2 == 3
assert 4 - 3 == 1
assert 3 * 5 == 15
assert 3 / 2 == 1.5
assert 10 % 3 == 1
assert 2 ** 3 == 8
一元运算符
+
和 -
运算符也可用作一元运算符
assert +3 == 3
assert -4 == 0 - 4
assert -(-1) == 1 (1)
1 | 注意使用括号将表达式括起来,以便将一元负号应用于被括起来的表达式。 |
在一元算术运算符方面,++
(增量)和 --
(减量)运算符可用,包括前缀和后缀表示法
def a = 2
def b = a++ * 3 (1)
assert a == 3 && b == 6
def c = 3
def d = c-- * 2 (2)
assert c == 2 && d == 6
def e = 1
def f = ++e + 3 (3)
assert e == 2 && f == 5
def g = 4
def h = --g + 1 (4)
assert g == 3 && h == 4
1 | 后缀增量将在表达式求值并赋值给 b 之后递增 a |
2 | 后缀减量将在表达式求值并赋值给 d 之后递减 c |
3 | 前缀增量将在表达式求值并赋值给 f 之前递增 e |
4 | 前缀减量将在表达式求值并赋值给 h 之前递减 g |
有关布尔值的一元非运算符,请参见条件运算符。
赋值算术运算符
我们上面看到的二元算术运算符也以赋值形式提供
-
+=
-
-=
-
*=
-
/=
-
%=
-
**=
让我们看看它们的实际应用
def a = 4
a += 3
assert a == 7
def b = 5
b -= 3
assert b == 2
def c = 5
c *= 3
assert c == 15
def d = 10
d /= 2
assert d == 5
def e = 10
e %= 3
assert e == 1
def f = 3
f **= 2
assert f == 9
1.2.2. 关系运算符
关系运算符允许对象之间进行比较,以了解两个对象是否相同或不同,或者一个是否大于、小于或等于另一个。
以下运算符可用
运算符 | 目的 |
---|---|
|
等于 |
|
不等于 |
|
小于 |
|
小于或等于 |
|
大于 |
|
大于或等于 |
|
相同 (自 Groovy 3.0.0 起) |
|
不相同 (自 Groovy 3.0.0 起) |
以下是使用这些运算符进行简单数字比较的一些示例
assert 1 + 2 == 3
assert 3 != 4
assert -2 < 3
assert 2 <= 2
assert 3 <= 4
assert 5 > 1
assert 5 >= -2
===
和 !==
都受支持,它们分别与调用 is()
方法和否定调用 is()
方法相同。
import groovy.transform.EqualsAndHashCode
@EqualsAndHashCode
class Creature { String type }
def cat = new Creature(type: 'cat')
def copyCat = cat
def lion = new Creature(type: 'cat')
assert cat.equals(lion) // Java logical equality
assert cat == lion // Groovy shorthand operator
assert cat.is(copyCat) // Groovy identity
assert cat === copyCat // operator shorthand
assert cat !== lion // negated operator shorthand
1.2.3. 逻辑运算符
Groovy 为布尔表达式提供了三个逻辑运算符
-
&&
:逻辑“与” -
||
:逻辑“或” -
!
:逻辑“非”
让我们用以下示例来说明它们
assert !false (1)
assert true && true (2)
assert true || false (3)
1 | “非”假为真 |
2 | 真“与”真为真 |
3 | 真“或”假为真 |
优先级
逻辑“非”的优先级高于逻辑“与”。
assert (!false && false) == false (1)
1 | 在这里,断言为真(因为括号中的表达式为假),因为“非”的优先级高于“与”,所以它只适用于第一个“假”项;否则,它将适用于“与”的结果,将其变为真,并且断言将失败 |
逻辑“与”的优先级高于逻辑“或”。
assert true || true && false (1)
1 | 在这里,断言为真,因为“与”的优先级高于“或”,因此“或”最后执行并返回真,因为它有一个真参数;否则,“与”将最后执行并返回假,因为它有一个假参数,并且断言将失败 |
短路求值
逻辑 ||
运算符支持短路求值:如果左操作数为 true,它知道结果在任何情况下都将为 true,因此它不会评估右操作数。右操作数仅在左操作数为 false 时才会被评估。
逻辑 &&
运算符也类似:如果左操作数为 false,它知道结果在任何情况下都将为 false,因此它不会评估右操作数。右操作数仅在左操作数为 true 时才会被评估。
boolean checkIfCalled() { (1)
called = true
}
called = false
true || checkIfCalled()
assert !called (2)
called = false
false || checkIfCalled()
assert called (3)
called = false
false && checkIfCalled()
assert !called (4)
called = false
true && checkIfCalled()
assert called (5)
1 | 我们创建一个函数,每当它被调用时,就将 called 标志设置为 true |
2 | 在第一种情况下,重置调用标志后,我们确认如果 || 的左操作数为 true,则不调用该函数,因为 || 会短路右操作数的评估 |
3 | 在第二种情况下,左操作数为 false,因此该函数被调用,如我们的标志现在为 true 所指示 |
4 | 同样对于 && ,我们确认函数不会在左操作数为 false 的情况下被调用 |
5 | 但是函数会在左操作数为 true 的情况下被调用 |
1.2.4. 位运算符和位移运算符
位运算符
Groovy 提供四种位运算符
-
&
: 位“与” -
|
: 位“或” -
^
: 位“异或”(异或) -
~
: 位非
位运算符可以应用于类型为 byte
、short
、int
、long
或 BigInteger
的参数。如果其中一个参数是 BigInteger
,则结果类型将是 BigInteger
;否则,如果其中一个参数是 long
,则结果类型将是 long
;否则,结果类型将是 int
。
int a = 0b00101010
assert a == 42
int b = 0b00001000
assert b == 8
assert (a & a) == a (1)
assert (a & b) == b (2)
assert (a | a) == a (3)
assert (a | b) == a (4)
int mask = 0b11111111 (5)
assert ((a ^ a) & mask) == 0b00000000 (6)
assert ((a ^ b) & mask) == 0b00100010 (7)
assert ((~a) & mask) == 0b11010101 (8)
1 | 位与 |
2 | 位与返回共同位 |
3 | 位或 |
4 | 位或返回所有“1”位 |
5 | 设置一个掩码以仅检查最后 8 位 |
6 | 自身进行位异或返回 0 |
7 | 位异或 |
8 | 位非 |
值得注意的是,原始类型的内部表示遵循 Java 语言规范。特别是,原始类型是有符号的,这意味着对于位否定,始终最好使用掩码来仅检索必要的位。
在 Groovy 中,位运算符是可重载的,这意味着您可以为任何类型的对象定义这些运算符的行为。
位移运算符
Groovy 提供三种位移运算符
-
<<
: 左移 -
>>
: 右移 -
>>>
: 无符号右移
所有这三个运算符都适用于左参数类型为 byte
, short
, int
或 long
的情况。前两个运算符也适用于左参数类型为 BigInteger
的情况。如果左参数是 BigInteger
,则结果类型将是 BigInteger
;否则,如果左参数是 long
,则结果类型将是 long
;否则,结果类型将是 int
。
assert 12.equals(3 << 2) (1)
assert 24L.equals(3L << 3) (1)
assert 48G.equals(3G << 4) (1)
assert 4095 == -200 >>> 20
assert -1 == -200 >> 20
assert 2G == 5G >> 1
assert -3G == -5G >> 1
1 | equals 方法用于代替 == 来确认结果类型 |
在 Groovy 中,位移运算符是可重载的,这意味着您可以为任何类型的对象定义这些运算符的行为。
1.2.5. 条件运算符
非运算符
“非”运算符用感叹号(!
)表示,并反转基础布尔表达式的结果。特别是,可以将 not
运算符与 Groovy truth 结合使用
assert (!true) == false (1)
assert (!'foo') == false (2)
assert (!'') == true (3)
1 | true 的否定是 false |
2 | “foo”是一个非空字符串,求值为 true ,因此否定返回 false |
3 | '' 是一个空字符串,求值为 false ,因此否定返回 true |
三元运算符
三元运算符是一种快捷表达式,等同于将某个值赋给变量的 if/else 分支。
而不是
if (string!=null && string.length()>0) {
result = 'Found'
} else {
result = 'Not found'
}
你可以写
result = (string!=null && string.length()>0) ? 'Found' : 'Not found'
三元运算符也与Groovy truth兼容,因此您可以使其更简单
result = string ? 'Found' : 'Not found'
Elvis 运算符
“Elvis 运算符”是三元运算符的缩写。它方便的一个例子是,如果表达式解析为“假”值(如 Groovy truth 中),则返回一个“合理的默认”值。一个简单的例子可能如下所示
displayName = user.name ? user.name : 'Anonymous' (1)
displayName = user.name ?: 'Anonymous' (2)
1 | 使用三元运算符,您必须重复要分配的值 |
2 | 使用 Elvis 运算符,如果被测试的值不是“假”值,则使用该值 |
使用 Elvis 运算符可以减少代码的冗余性,并通过消除在条件和正返回值中重复被测试表达式的需要,从而减少重构时出错的风险。
Elvis 赋值运算符
Groovy 3.0.0 引入了 Elvis 运算符,例如
import groovy.transform.ToString
@ToString(includePackage = false)
class Element {
String name
int atomicNumber
}
def he = new Element(name: 'Helium')
he.with {
name = name ?: 'Hydrogen' // existing Elvis operator
atomicNumber ?= 2 // new Elvis assignment shorthand
}
assert he.toString() == 'Element(Helium, 2)'
1.2.6. 对象运算符
安全导航运算符
安全导航运算符用于避免 NullPointerException
。通常,当您引用一个对象时,您可能需要验证它不是 null
,然后才能访问该对象的方法或属性。为了避免这种情况,安全导航运算符将简单地返回 null
而不是抛出异常,如下所示
def person = Person.find { it.id == 123 } (1)
def name = person?.name (2)
assert name == null (3)
1 | find 将返回一个 null 实例 |
2 | 使用空安全运算符可以防止 NullPointerException |
3 | 结果是 null |
直接字段访问运算符
通常在 Groovy 中,当您编写如下代码时
class User {
public final String name (1)
User(String name) { this.name = name}
String getName() { "Name: $name" } (2)
}
def user = new User('Bob')
assert user.name == 'Name: Bob' (3)
1 | 公共字段 name |
2 | 返回自定义字符串的 name 的 getter |
3 | 调用 getter |
user.name
调用会触发对同名属性的调用,也就是说,这里是对 name
的 getter 的调用。如果您想检索字段而不是调用 getter,可以使用直接字段访问运算符
assert user.@name == 'Bob' (1)
1 | 使用 .@ 强制使用字段而不是 getter |
方法指针运算符
方法指针运算符(.&
)可用于将方法引用存储在变量中,以便稍后调用
def str = 'example of method reference' (1)
def fun = str.&toUpperCase (2)
def upper = fun() (3)
assert upper == str.toUpperCase() (4)
1 | str 变量包含一个 String |
2 | 我们将 str 实例上的 toUpperCase 方法的引用存储在一个名为 fun 的变量中 |
3 | fun 可以像普通方法一样调用 |
4 | 我们可以检查结果是否与我们直接在 str 上调用它相同 |
使用方法指针有很多优点。首先,这种方法指针的类型是 groovy.lang.Closure
,因此它可以在任何使用闭包的地方使用。特别是,它适合于根据策略模式的需要转换现有方法
def transform(List elements, Closure action) { (1)
def result = []
elements.each {
result << action(it)
}
result
}
String describe(Person p) { (2)
"$p.name is $p.age"
}
def action = this.&describe (3)
def list = [
new Person(name: 'Bob', age: 42),
new Person(name: 'Julia', age: 35)] (4)
assert transform(list, action) == ['Bob is 42', 'Julia is 35'] (5)
1 | transform 方法获取列表中的每个元素,并在其上调用 action 闭包,返回一个新列表 |
2 | 我们定义一个函数,它接受一个 Person 并返回一个 String |
3 | 我们对该函数创建了一个方法指针 |
4 | 我们创建要收集描述符的元素列表 |
5 | 方法指针可以在期望 Closure 的地方使用 |
方法指针由接收器和方法名称绑定。参数在运行时解析,这意味着如果您有多个同名方法,语法没有区别,只有在运行时才会解析要调用的适当方法
def doSomething(String str) { str.toUpperCase() } (1)
def doSomething(Integer x) { 2*x } (2)
def reference = this.&doSomething (3)
assert reference('foo') == 'FOO' (4)
assert reference(123) == 246 (5)
1 | 定义一个接受 String 作为参数的重载 doSomething 方法 |
2 | 定义一个接受 Integer 作为参数的重载 doSomething 方法 |
3 | 在 doSomething 上创建一个方法指针,而不指定参数类型 |
4 | 将方法指针与 String 一起使用会调用 doSomething 的 String 版本 |
5 | 将方法指针与 Integer 一起使用会调用 doSomething 的 Integer 版本 |
为了与 Java 8 方法引用预期保持一致,在 Groovy 3 及更高版本中,您可以使用 new
作为方法名来获取指向构造函数的方法指针
def foo = BigInteger.&new
def fortyTwo = foo('42')
assert fortyTwo == 42G
同样在 Groovy 3 及更高版本中,您可以获取指向类的实例方法的方法指针。此方法指针会多一个参数,即要调用方法的接收器实例
def instanceMethod = String.&toUpperCase
assert instanceMethod('foo') == 'FOO'
为了向后兼容,任何恰好具有正确调用参数的静态方法在此情况下将优先于实例方法。
方法引用运算符
Groovy 3+ 中的 Parrot 解析器支持 Java 8+ 方法引用运算符。方法引用运算符(::
)可用于在需要函数式接口的上下文中引用方法或构造函数。这与 Groovy 的方法指针运算符提供的功能有些重叠。实际上,对于动态 Groovy,方法引用运算符只是方法指针运算符的别名。对于静态 Groovy,该运算符会生成类似于 Java 在相同上下文中生成的字节码。
以下脚本显示了一些突出显示各种受支持方法引用案例的示例
import groovy.transform.CompileStatic
import static java.util.stream.Collectors.toList
@CompileStatic
void methodRefs() {
assert 6G == [1G, 2G, 3G].stream().reduce(0G, BigInteger::add) (1)
assert [4G, 5G, 6G] == [1G, 2G, 3G].stream().map(3G::add).collect(toList()) (2)
assert [1G, 2G, 3G] == [1L, 2L, 3L].stream().map(BigInteger::valueOf).collect(toList()) (3)
assert [1G, 2G, 3G] == [1L, 2L, 3L].stream().map(3G::valueOf).collect(toList()) (4)
}
methodRefs()
1 | 类实例方法引用:add(BigInteger val) 是 BigInteger 中的一个实例方法 |
2 | 对象实例方法引用:add(BigInteger val) 是对象 3G 的一个实例方法 |
3 | 类静态方法引用:valueOf(long val) 是 BigInteger 类的一个静态方法 |
4 | 对象静态方法引用:valueOf(long val) 是对象 3G 的一个静态方法(在正常情况下,有些人认为这是不良风格) |
以下脚本显示了一些突出显示各种受支持构造函数引用案例的示例
@CompileStatic
void constructorRefs() {
assert [1, 2, 3] == ['1', '2', '3'].stream().map(Integer::valueOf).collect(toList()) (1)
def result = [1, 2, 3].stream().toArray(Integer[]::new) (2)
assert result instanceof Integer[]
assert result.toString() == '[1, 2, 3]'
}
constructorRefs()
1 | 类构造函数引用 |
2 | 数组构造函数引用 |
1.2.7. 正则表达式运算符
模式运算符
模式运算符(~
)提供了一种创建 java.util.regex.Pattern
实例的简单方法
def p = ~/foo/
assert p instanceof Pattern
虽然通常,您会在斜线字符串中找到带有表达式的模式运算符,但它可以在 Groovy 中与任何类型的 String
一起使用
p = ~'foo' (1)
p = ~"foo" (2)
p = ~$/dollar/slashy $ string/$ (3)
p = ~"${pattern}" (4)
1 | 使用单引号字符串 |
2 | 使用双引号字符串 |
3 | 美元斜线字符串允许您使用斜线和美元符号,而无需转义它们 |
4 | 您也可以使用 GString! |
虽然您可以将大多数字符串形式与模式、查找和匹配运算符一起使用,但我们建议大多数时候使用斜线字符串,以省去记住否则所需的转义要求。 |
查找运算符
除了构建模式之外,您还可以使用查找运算符 =~
直接创建 java.util.regex.Matcher
实例
def text = "some text to match"
def m = text =~ /match/ (1)
assert m instanceof Matcher (2)
if (!m) { (3)
throw new RuntimeException("Oops, text not found!")
}
1 | =~ 使用右侧的模式针对 text 变量创建匹配器 |
2 | =~ 的返回类型是 Matcher |
3 | 等同于调用 if (!m.find(0)) |
由于 Matcher
通过调用其 find
方法强制转换为 boolean
,因此 =~
运算符与 Perl 的 =~
运算符的简单用法一致,当它作为谓词(在 if
, ?:
等中)出现时。当目的是迭代指定模式的匹配项(在 while
等中)时,直接在匹配器上调用 find()
或使用 iterator
DGM。
匹配运算符
匹配运算符(==~
)是查找运算符的细微变体,它不返回 Matcher
,而是返回布尔值并要求严格匹配输入字符串
m = text ==~ /match/ (1)
assert m instanceof Boolean (2)
if (m) { (3)
throw new RuntimeException("Should not reach that point!")
}
1 | ==~ 将主题与正则表达式匹配,但匹配必须严格 |
2 | 因此 ==~ 的返回类型是 boolean |
3 | 等同于调用 if (text ==~ /match/) |
比较查找运算符和匹配运算符
通常,当模式涉及单个精确匹配时,使用匹配运算符;否则,查找运算符可能更有用。
assert 'two words' ==~ /\S+\s+\S+/
assert 'two words' ==~ /^\S+\s+\S+$/ (1)
assert !(' leading space' ==~ /\S+\s+\S+/) (2)
def m1 = 'two words' =~ /^\S+\s+\S+$/
assert m1.size() == 1 (3)
def m2 = 'now three words' =~ /^\S+\s+\S+$/ (4)
assert m2.size() == 0 (5)
def m3 = 'now three words' =~ /\S+\s+\S+/
assert m3.size() == 1 (6)
assert m3[0] == 'now three'
def m4 = ' leading space' =~ /\S+\s+\S+/
assert m4.size() == 1 (7)
assert m4[0] == 'leading space'
def m5 = 'and with four words' =~ /\S+\s+\S+/
assert m5.size() == 2 (8)
assert m5[0] == 'and with'
assert m5[1] == 'four words'
1 | 等效,但不鼓励显式使用 ^ 和 $,因为它们不需要 |
2 | 由于前导空格,不匹配 |
3 | 一次匹配 |
4 | ^ 和 $ 表示需要精确匹配 |
5 | 零匹配 |
6 | 一个匹配,贪婪地从第一个单词开始 |
7 | 一个匹配,忽略前导空格 |
8 | 两次匹配 |
1.2.8. 其他运算符
展开运算符
展开点运算符(*.
),通常简称为展开运算符,用于在聚合对象的所有项上调用操作。它等同于在每个项上调用操作并将结果收集到列表中
class Car {
String make
String model
}
def cars = [
new Car(make: 'Peugeot', model: '508'),
new Car(make: 'Renault', model: 'Clio')] (1)
def makes = cars*.make (2)
assert makes == ['Peugeot', 'Renault'] (3)
1 | 构建一个 Car 列表。该列表是对象的聚合。 |
2 | 对列表调用展开运算符,访问每个项的 make 属性 |
3 | 返回一个字符串列表,对应于 make 项的集合 |
表达式 cars*.make
等同于 cars.collect{ it.make }
。Groovy 的 GPath 符号允许在引用属性不是包含列表的属性时使用快捷方式,在这种情况下它会自动展开。在前面提到的情况下,可以使用表达式 cars.make
,尽管通常建议保留显式展开点运算符。
展开运算符是空安全的,这意味着如果集合的一个元素为 null,它将返回 null 而不是抛出 NullPointerException
cars = [
new Car(make: 'Peugeot', model: '508'),
null, (1)
new Car(make: 'Renault', model: 'Clio')]
assert cars*.make == ['Peugeot', null, 'Renault'] (2)
assert null*.make == null (3)
1 | 构建一个其中一个元素为 null 的列表 |
2 | 使用展开运算符将不会抛出 NullPointerException |
3 | 接收器也可能为 null,在这种情况下返回值为 null |
展开运算符可用于实现 Iterable
接口的任何类
class Component {
Integer id
String name
}
class CompositeObject implements Iterable<Component> {
def components = [
new Component(id: 1, name: 'Foo'),
new Component(id: 2, name: 'Bar')]
@Override
Iterator<Component> iterator() {
components.iterator()
}
}
def composite = new CompositeObject()
assert composite*.id == [1,2]
assert composite*.name == ['Foo','Bar']
当处理本身包含聚合的数据结构聚合时,使用展开点运算符的多次调用(此处为 cars*.models*.name
)
class Make {
String name
List<Model> models
}
@Canonical
class Model {
String name
}
def cars = [
new Make(name: 'Peugeot',
models: [new Model('408'), new Model('508')]),
new Make(name: 'Renault',
models: [new Model('Clio'), new Model('Captur')])
]
def makes = cars*.name
assert makes == ['Peugeot', 'Renault']
def models = cars*.models*.name
assert models == [['408', '508'], ['Clio', 'Captur']]
assert models.sum() == ['408', '508', 'Clio', 'Captur'] // flatten one level
assert models.flatten() == ['408', '508', 'Clio', 'Captur'] // flatten all levels (one in this case)
对于集合的集合,考虑使用 collectNested
DGM 方法而不是展开点运算符
class Car {
String make
String model
}
def cars = [
[
new Car(make: 'Peugeot', model: '408'),
new Car(make: 'Peugeot', model: '508')
], [
new Car(make: 'Renault', model: 'Clio'),
new Car(make: 'Renault', model: 'Captur')
]
]
def models = cars.collectNested{ it.model }
assert models == [['408', '508'], ['Clio', 'Captur']]
展开方法参数
在某些情况下,方法调用的参数可能存在于需要适应方法参数的列表中。在这种情况下,您可以使用展开运算符来调用方法。例如,假设您有以下方法签名
int function(int x, int y, int z) {
x*y+z
}
那么,如果您有以下列表
def args = [4,5,6]
您可以直接调用该方法,无需定义中间变量
assert function(*args) == 26
甚至可以将普通参数与展开参数混合使用
args = [4]
assert function(*args,5,6) == 26
展开列表元素
当在列表字面量中使用时,展开运算符的作用如同展开元素的内容内联到列表中一样
def items = [4,5] (1)
def list = [1,2,3,*items,6] (2)
assert list == [1,2,3,4,5,6] (3)
1 | items 是一个列表 |
2 | 我们希望将 items 列表的内容直接插入到 list 中,而无需调用 addAll |
3 | items 的内容已内联到 list 中 |
展开映射元素
展开映射运算符与展开列表运算符类似,但适用于映射。它允许您将映射的内容内联到另一个映射字面量中,如下例所示
def m1 = [c:3, d:4] (1)
def map = [a:1, b:2, *:m1] (2)
assert map == [a:1, b:2, c:3, d:4] (3)
1 | m1 是我们想要内联的映射 |
2 | 我们使用 *:m1 表示法将 m1 的内容展开到 map 中 |
3 | map 包含 m1 的所有元素 |
展开映射运算符的位置是相关的,如下例所示
def m1 = [c:3, d:4] (1)
def map = [a:1, b:2, *:m1, d: 8] (2)
assert map == [a:1, b:2, c:3, d:8] (3)
1 | m1 是我们想要内联的映射 |
2 | 我们使用 *:m1 符号将 m1 的内容展开到 map 中,但在展开之后重新定义了键 d |
3 | map 包含所有预期的键,但 d 已被重新定义 |
范围运算符
Groovy 支持范围的概念,并提供了一种表示法(..
)来创建对象范围
def range = 0..5 (1)
assert (0..5).collect() == [0, 1, 2, 3, 4, 5] (2)
assert (0..<5).collect() == [0, 1, 2, 3, 4] (3)
assert (0<..5).collect() == [1, 2, 3, 4, 5] (4)
assert (0<..<5).collect() == [1, 2, 3, 4] (5)
assert (0..5) instanceof List (6)
assert (0..5).size() == 6 (7)
1 | 一个简单的整数范围,存储在局部变量中 |
2 | 一个 IntRange ,包含上下界 |
3 | 一个 IntRange ,具有独占上限 |
4 | 一个 IntRange ,具有独占下限 |
5 | 一个 IntRange ,具有独占下限和上限 |
6 | 一个 groovy.lang.Range 实现 List 接口 |
7 | 这意味着您可以在其上调用 size 方法 |
Ranges 的实现是轻量级的,这意味着只存储下限和上限。您可以从任何具有 next()
和 previous()
方法的 Comparable
对象创建范围,以确定范围中的下一个/上一个项目。例如,您可以通过这种方式创建字符范围
assert ('a'..'d').collect() == ['a','b','c','d']
飞船运算符
飞船运算符(<=>
)委托给 compareTo
方法
assert (1 <=> 1) == 0
assert (1 <=> 2) == -1
assert (2 <=> 1) == 1
assert ('a' <=> 'z') == -1
下标运算符
下标运算符是 getAt
或 putAt
的简写表示法,具体取决于它出现在赋值的左侧还是右侧
def list = [0,1,2,3,4]
assert list[2] == 2 (1)
list[2] = 4 (2)
assert list[0..2] == [0,1,4] (3)
list[0..2] = [6,6,6] (4)
assert list == [6,6,6,3,4] (5)
1 | [2] 可以用来代替 getAt(2) |
2 | 如果在赋值的左侧,将调用 putAt |
3 | getAt 也支持范围 |
4 | putAt 也是如此 |
5 | 列表被修改了 |
下标运算符与 getAt
/putAt
的自定义实现相结合,是解构对象的便捷方式
class User {
Long id
String name
def getAt(int i) { (1)
switch (i) {
case 0: return id
case 1: return name
}
throw new IllegalArgumentException("No such element $i")
}
void putAt(int i, def value) { (2)
switch (i) {
case 0: id = value; return
case 1: name = value; return
}
throw new IllegalArgumentException("No such element $i")
}
}
def user = new User(id: 1, name: 'Alex') (3)
assert user[0] == 1 (4)
assert user[1] == 'Alex' (5)
user[1] = 'Bob' (6)
assert user.name == 'Bob' (7)
1 | User 类定义了一个自定义的 getAt 实现 |
2 | User 类定义了一个自定义的 putAt 实现 |
3 | 创建一个示例用户 |
4 | 使用下标运算符,索引为 0 允许检索用户 ID |
5 | 使用下标运算符,索引为 1 允许检索用户名 |
6 | 我们可以使用下标运算符写入属性,这得益于对 putAt 的委托 |
7 | 并检查确实是 name 属性被更改了 |
安全索引运算符
Groovy 3.0.0 引入了安全索引运算符,即 ?[]
,它类似于 ?.
。例如
String[] array = ['a', 'b']
assert 'b' == array?[1] // get using normal array index
array?[1] = 'c' // set using normal array index
assert 'c' == array?[1]
array = null
assert null == array?[1] // return null for all index values
array?[1] = 'c' // quietly ignore attempt to set value
assert null == array?[1]
def personInfo = [name: 'Daniel.Sun', location: 'Shanghai']
assert 'Daniel.Sun' == personInfo?['name'] // get using normal map index
personInfo?['name'] = 'sunlan' // set using normal map index
assert 'sunlan' == personInfo?['name']
personInfo = null
assert null == personInfo?['name'] // return null for all map values
personInfo?['name'] = 'sunlan' // quietly ignore attempt to set value
assert null == personInfo?['name']
成员运算符
成员运算符(in
)等同于调用 isCase
方法。在 List
的上下文中,它等同于调用 contains
,如下例所示
def list = ['Grace','Rob','Emmy']
assert ('Emmy' in list) (1)
assert ('Alex' !in list) (2)
1 | 等同于调用 list.contains('Emmy') 或 list.isCase('Emmy') |
2 | 成员否定等同于调用 !list.contains('Emmy') 或 !list.isCase('Emmy') |
身份运算符
在 Groovy 中,使用 ==
测试相等性与在 Java 中使用相同的运算符不同。在 Groovy 中,它调用 equals
。如果您想比较引用相等性,您应该使用 is
,如下例所示
def list1 = ['Groovy 1.8','Groovy 2.0','Groovy 2.3'] (1)
def list2 = ['Groovy 1.8','Groovy 2.0','Groovy 2.3'] (2)
assert list1 == list2 (3)
assert !list1.is(list2) (4)
assert list1 !== list2 (5)
1 | 创建字符串列表 |
2 | 创建另一个包含相同元素的字符串列表 |
3 | 使用 == ,我们测试对象相等性,等同于 Java 中的 list1.equals(list2) |
4 | 使用 is ,我们可以检查引用是否不同,等同于 Java 中的 list1 == list2 |
5 | 使用 === 或 !== (自 Groovy 3.0.0 起支持并推荐),我们也可以检查引用是否不同,等同于 Java 中的 list1 == list2 和 list1 != list2 |
强制转换运算符
强制转换运算符(as
)是类型转换的一种变体。强制转换将对象从一种类型转换为另一种类型,而无需它们在赋值上兼容。让我们举个例子
String input = '42'
Integer num = (Integer) input (1)
1 | String 无法赋值给 Integer ,因此在运行时会产生 ClassCastException |
这可以通过使用强制转换来修复
String input = '42'
Integer num = input as Integer (1)
1 | String 无法赋值给 Integer ,但使用 as 将其强制转换为 Integer |
当一个对象被强制转换为另一个对象时,除非目标类型与源类型相同,否则强制转换将返回一个新对象。强制转换的规则因源类型和目标类型而异,如果没有找到转换规则,强制转换可能会失败。可以通过 asType
方法实现自定义转换规则
class Identifiable {
String name
}
class User {
Long id
String name
def asType(Class target) { (1)
if (target == Identifiable) {
return new Identifiable(name: name)
}
throw new ClassCastException("User cannot be coerced into $target")
}
}
def u = new User(name: 'Xavier') (2)
def p = u as Identifiable (3)
assert p instanceof Identifiable (4)
assert !(p instanceof User) (5)
1 | User 类定义了从 User 到 Identifiable 的自定义转换规则 |
2 | 我们创建一个 User 实例 |
3 | 我们将 User 实例强制转换为 Identifiable |
4 | 目标是 Identifiable 的一个实例 |
5 | 目标不再是 User 的实例 |
菱形运算符
菱形运算符(<>
)只是一个语法糖运算符,旨在支持与 Java 7 中同名运算符的兼容性。它用于指示泛型类型应从声明中推断出来
List<String> strings = new LinkedList<>()
在动态 Groovy 中,这完全未使用。在静态类型检查的 Groovy 中,它也是可选的,因为 Groovy 类型检查器无论此运算符是否存在都会执行类型推断。
调用运算符
调用运算符 ()
用于隐式调用名为 call
的方法。对于任何定义了 call
方法的对象,您可以省略 .call
部分,转而使用调用运算符
class MyCallable {
int call(int x) { (1)
2*x
}
}
def mc = new MyCallable()
assert mc.call(2) == 4 (2)
assert mc(2) == 4 (3)
1 | MyCallable 定义了一个名为 call 的方法。请注意,它不需要实现 java.util.concurrent.Callable |
2 | 我们可以使用经典方法调用语法调用该方法 |
3 | 或者我们可以借助于调用运算符省略 .call |
1.2.9. 运算符优先级
下表列出了所有 Groovy 运算符的优先级顺序。
级别 | 运算符 | 名称 |
---|---|---|
1 |
|
对象创建,显式括号 |
|
方法调用,闭包,字面量列表/映射 |
|
|
成员访问,方法闭包,字段/属性访问 |
|
|
安全解引用,展开,展开点,展开映射 |
|
|
位非/模式,非,类型转换 |
|
|
列表/映射/数组(安全)索引,后置增/减 |
|
2 |
|
乘方 |
3 |
|
前置增/减,一元加,一元减 |
4 |
|
乘,除,余 |
5 |
|
加法,减法 |
6 |
|
左/右(无符号)移位,包含/不包含范围 |
7 |
|
小于/大于/或等于,在…中,不在…中,instanceof,not instanceof,类型强制转换 |
8 |
|
等于,不等于,比较,相同,不相同 |
|
正则表达式查找,正则表达式匹配 |
|
9 |
|
二元/位与 |
10 |
|
二元/位异或 |
11 |
|
二元/位或 |
12 |
|
逻辑与 |
13 |
|
逻辑或 |
14 |
|
三元条件 |
|
Elvis 运算符 |
|
15 |
|
各种赋值 |
1.2.10. 运算符重载
Groovy 允许您重载各种运算符,以便它们可以与您自己的类一起使用。考虑这个简单的类
class Bucket {
int size
Bucket(int size) { this.size = size }
Bucket plus(Bucket other) { (1)
return new Bucket(this.size + other.size)
}
}
1 | Bucket 实现了一个名为 plus() 的特殊方法 |
仅通过实现 plus()
方法,Bucket
类现在就可以与 +
运算符一起使用,如下所示
def b1 = new Bucket(4)
def b2 = new Bucket(11)
assert (b1 + b2).size == 15 (1)
1 | 两个 Bucket 对象可以使用 + 运算符相加 |
所有(非比较器)Groovy 运算符都有一个对应的方法,您可以在自己的类中实现这些方法。唯一的要求是您的方法是公共的,具有正确的名称,并且具有正确数量的参数。参数类型取决于您希望支持运算符右侧的哪些类型。例如,您可以支持以下语句
assert (b1 + 11).size == 15
通过实现具有此签名的 plus()
方法
Bucket plus(int capacity) {
return new Bucket(this.size + capacity)
}
以下是运算符及其对应方法的完整列表
运算符 | 方法 | 运算符 | 方法 |
---|---|---|---|
|
a.plus(b) |
|
a.getAt(b) |
|
a.minus(b) |
|
a.putAt(b, c) |
|
a.multiply(b) |
|
b.isCase(a) |
|
a.div(b) |
|
a.leftShift(b) |
|
a.mod(b) |
|
a.rightShift(b) |
|
a.power(b) |
|
a.rightShiftUnsigned(b) |
|
a.or(b) |
|
a.next() |
|
a.and(b) |
|
a.previous() |
|
a.xor(b) |
|
a.positive() |
|
a.asType(b) |
|
a.negative() |
|
a.call() |
|
a.bitwiseNegate() |
1.3. 程序结构
本章介绍 Groovy 编程语言的程序结构。
1.3.1. 包名
包名与 Java 中的作用完全相同。它们允许我们在不发生任何冲突的情况下分离代码库。Groovy 类必须在类定义之前指定其包,否则将假定为默认包。
定义包与 Java 非常相似
// defining a package named com.yoursite
package com.yoursite
要引用 com.yoursite.com
包中的某个类 Foo
,您需要使用完全限定名称 com.yoursite.com.Foo
,或者可以使用 import
语句,我们将在下面看到。
1.3.2. 导入
为了引用任何类,您需要对其包进行合格引用。Groovy 遵循 Java 的允许 import
语句来解析类引用的概念。
例如,Groovy 提供了几个构建器类,例如 MarkupBuilder
。MarkupBuilder
位于 groovy.xml
包中,因此为了使用此 S 类,您需要导入它,如所示
// importing the class MarkupBuilder
import groovy.xml.MarkupBuilder
// using the imported class to create an object
def xml = new MarkupBuilder()
assert xml != null
默认导入
默认导入是 Groovy 语言默认提供的导入。例如,请看以下代码
new Date()
Java 中的相同代码需要导入 Date
类,像这样:import java.util.Date。Groovy 默认为您导入这些类。
以下导入由 groovy 为您添加
import java.lang.*
import java.util.*
import java.io.*
import java.net.*
import groovy.lang.*
import groovy.util.*
import java.math.BigInteger
import java.math.BigDecimal
这样做是因为这些包中的类最常用。通过导入这些,样板代码得以减少。
简单导入
简单导入是一种导入语句,您在其中完整定义类名以及包。例如,下面代码中的导入语句 import groovy.xml.MarkupBuilder
是一个简单导入,它直接引用包中的一个类。
// importing the class MarkupBuilder
import groovy.xml.MarkupBuilder
// using the imported class to create an object
def xml = new MarkupBuilder()
assert xml != null
星号导入
Groovy 和 Java 一样,提供了一种特殊的方式来使用 *
导入包中的所有类,这被称为星号导入。MarkupBuilder
是 groovy.xml
包中的一个类,旁边还有另一个名为 StreamingMarkupBuilder
的类。如果您需要同时使用这两个类,您可以这样做
import groovy.xml.MarkupBuilder
import groovy.xml.StreamingMarkupBuilder
def markupBuilder = new MarkupBuilder()
assert markupBuilder != null
assert new StreamingMarkupBuilder() != null
这是完全有效的代码。但是有了 *
导入,我们只需一行就可以达到同样的效果。星号导入 groovy.xml
包下的所有类
import groovy.xml.*
def markupBuilder = new MarkupBuilder()
assert markupBuilder != null
assert new StreamingMarkupBuilder() != null
星号导入的一个问题是它们会污染您的本地命名空间。但是,通过 Groovy 提供的别名功能,这个问题可以轻松解决。
静态导入
Groovy 的静态导入功能允许您像引用自己类中的静态方法一样引用导入的类
import static Boolean.FALSE
assert !FALSE //use directly, without Boolean prefix!
这与 Java 的静态导入功能类似,但比 Java 更具动态性,因为它允许您定义与导入方法同名的方法,只要它们的类型不同
import static java.lang.String.format (1)
class SomeClass {
String format(Integer i) { (2)
i.toString()
}
static void main(String[] args) {
assert format('String') == 'String' (3)
assert new SomeClass().format(Integer.valueOf(1)) == '1'
}
}
1 | 方法静态导入 |
2 | 声明与上面静态导入的方法同名但参数类型不同的方法 |
3 | Java 中编译错误,但在 Groovy 中是有效代码 |
如果类型相同,则导入的类优先。
静态导入别名
带 as
关键字的静态导入为命名空间问题提供了优雅的解决方案。假设您想使用 Calendar
实例,调用其 getInstance()
方法。它是一个静态方法,所以我们可以使用静态导入。但是,与其每次都调用 getInstance()
(这在脱离类名时可能会引起误解),不如使用别名导入它,以提高代码可读性
import static Calendar.getInstance as now
assert now().class == Calendar.getInstance().class
现在,这很简洁!
静态星号导入
静态星号导入与常规星号导入非常相似。它将导入给定类的所有静态方法。
例如,假设我们需要为应用程序计算正弦和余弦。java.lang.Math
类有静态方法 sin
和 cos
,它们符合我们的需求。借助静态星号导入,我们可以这样做
import static java.lang.Math.*
assert sin(0) == 0.0
assert cos(0) == 1.0
如您所见,我们能够直接访问 sin
和 cos
方法,无需 Math.
前缀。
导入别名
通过类型别名,我们可以使用我们选择的名称来引用完全限定的类名。这可以通过 as
关键字来完成,如前所述。
例如,我们可以将 java.sql.Date
导入为 SQLDate
,并在同一文件中将其与 java.util.Date
一起使用,而无需使用任何一个类的完全限定名称
import java.util.Date
import java.sql.Date as SQLDate
Date utilDate = new Date(1000L)
SQLDate sqlDate = new SQLDate(1000L)
assert utilDate instanceof java.util.Date
assert sqlDate instanceof java.sql.Date
1.3.3. 脚本与类
public static void main vs 脚本
Groovy 支持脚本和类。请看以下代码示例
class Main { (1)
static void main(String... args) { (2)
println 'Groovy world!' (3)
}
}
1 | 定义一个 Main 类,名称是任意的 |
2 | public static void main(String[]) 方法可用作该类的主方法 |
3 | 方法的主体 |
这是 Java 代码中典型的代码,其中代码必须嵌入到类中才能执行。Groovy 使其更简单,以下代码是等效的
println 'Groovy world!'
脚本可以被视为无需声明的类,但有一些区别。
脚本类
一个 groovy.lang.Script 总是被编译成一个类。Groovy 编译器会为您编译该类,并将脚本的主体复制到 run
方法中。因此,前一个例子被编译成如下形式
import org.codehaus.groovy.runtime.InvokerHelper
class Main extends Script { (1)
def run() { (2)
println 'Groovy world!' (3)
}
static void main(String[] args) { (4)
InvokerHelper.runScript(Main, args) (5)
}
}
1 | Main 类扩展了 groovy.lang.Script 类 |
2 | groovy.lang.Script 需要一个返回值的 run 方法 |
3 | 脚本主体进入 run 方法 |
4 | main 方法是自动生成的 |
5 | 并委托执行脚本到 run 方法 |
如果脚本在一个文件中,则使用该文件的基本名称来确定生成的脚本类的名称。在此示例中,如果文件名为 Main.groovy
,则脚本类将是 Main
。
方法
可以在脚本中定义方法,如下所示
int fib(int n) {
n < 2 ? 1 : fib(n-1) + fib(n-2)
}
assert fib(10)==89
你也可以混合使用方法和代码。生成的脚本类将把所有方法都包含在脚本类中,并将所有脚本主体组装到 run
方法中
println 'Hello' (1)
int power(int n) { 2**n } (2)
println "2^6==${power(6)}" (3)
1 | 脚本开始 |
2 | 在脚本主体中定义了一个方法 |
3 | 脚本继续 |
此代码在内部转换为
import org.codehaus.groovy.runtime.InvokerHelper
class Main extends Script {
int power(int n) { 2** n} (1)
def run() {
println 'Hello' (2)
println "2^6==${power(6)}" (3)
}
static void main(String[] args) {
InvokerHelper.runScript(Main, args)
}
}
1 | power 方法按原样复制到生成的脚本类中 |
2 | 第一条语句复制到 run 方法中 |
3 | 第二条语句复制到 run 方法中 |
即使 Groovy 从您的脚本创建了一个类,对于用户来说也是完全透明的。特别是,脚本被编译为字节码,并且行号得以保留。这意味着如果在脚本中抛出异常,堆栈跟踪将显示与原始脚本对应的行号,而不是我们所示的生成代码。 |
变量
脚本中的变量不需要类型定义。这意味着这个脚本
int x = 1
int y = 2
assert x+y == 3
其行为与
x = 1
y = 2
assert x+y == 3
然而,两者之间存在语义差异
-
如果变量像第一个例子中那样声明,它是一个局部变量。它将在编译器生成的
run
方法中声明,并且在脚本主体之外将不可见。特别是,这样的变量在脚本的其他方法中将不可见。 -
如果变量未声明,则它会进入 groovy.lang.Script#getBinding()。绑定对方法可见,如果您使用脚本与应用程序交互并需要在脚本和应用程序之间共享数据,则尤其重要。读者可以参考集成指南以获取更多信息。
使变量对所有方法可见的另一种方法是使用 @Field 注解。通过这种方式注解的变量将成为生成的脚本类的字段,并且,对于局部变量,访问不会涉及脚本 Binding 。虽然不推荐,但如果您的局部变量或脚本字段与绑定变量同名,您可以使用 binding.varName 来访问绑定变量。 |
1.4. 面向对象
本章介绍 Groovy 编程语言的面向对象方面。
1.4.1. 类型
基本类型
Groovy 支持 Java 语言规范中定义的相同基本类型
-
整数类型:
byte
(8 位)、short
(16 位)、int
(32 位)和long
(64 位) -
浮点类型:
float
(32 位)和double
(64 位) -
boolean
类型(true
或false
之一) -
char
类型(16 位,可用作数字类型,表示 UTF-16 编码)
与 Java 一样,当需要与任何原始类型对应的对象时,Groovy 使用相应的包装类
基本类型 | 包装类 |
---|---|
boolean |
Boolean |
char |
字符 |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
当调用需要包装类的方法并向其传递原始变量作为参数时,或者反之,会自动发生装箱和拆箱。这类似于 Java,但 Groovy 更进一步。
在大多数情况下,您可以将原始类型视为完整的对象包装器。例如,您可以在原始类型上调用 .toString()
或 .equals(other)
。Groovy 会根据需要自动在引用和原始类型之间进行包装和解包装。
这是一个使用 int
的示例,它在类中声明为静态字段(稍后讨论)
class Foo {
static int i
}
assert Foo.class.getDeclaredField('i').type == int.class (1)
assert Foo.i.class != int.class && Foo.i.class == Integer.class (2)
1 | 字节码中保留了原始类型 |
2 | 运行时查看字段显示它已被自动包装 |
引用类型
除了原始类型,其他所有都是对象,并具有定义其类型的关联类。我们将在稍后讨论类、与类相关的或类似的事物,如接口、特性和记录。
我们可能声明两个变量,类型为 String 和 List,如下所示
String movie = 'The Matrix'
List actors = ['Keanu Reeves', 'Hugo Weaving']
泛型
Groovy 在泛型方面沿用了 Java 的相同概念。在定义类和方法时,可以使用类型参数并创建泛型类、接口、方法或构造函数。
泛型类和方法的用法,无论它们是在 Java 还是 Groovy 中定义,都可能涉及提供类型参数。
我们可能声明一个变量,类型为“字符串列表”,如下所示
List<String> roles = ['Trinity', 'Morpheus']
Java 为了向后兼容 Java 的早期版本而采用类型擦除。动态 Groovy 可以被认为是更积极地应用类型擦除。一般来说,在编译时会检查更少的泛型类型信息。Groovy 的静态特性在泛型信息方面采用了与 Java 类似的检查。
1.4.2. 类
Groovy 类与 Java 类非常相似,并且在 JVM 级别与 Java 类兼容。它们可能具有方法、字段和属性(类似于 JavaBeans 属性,但样板代码更少)。类和类成员可以具有与 Java 中相同的修饰符(public、protected、private、static 等),但在源代码级别存在一些细微差别,这些差别将在稍后解释。
Groovy 类与 Java 类之间的主要区别在于
-
没有可见性修饰符的类或方法会自动成为 public(可以使用特殊注解来实现包私有可见性)。
-
没有可见性修饰符的字段会自动转换为属性,从而减少冗余代码,因为不需要显式的 getter 和 setter 方法。关于这方面的更多信息将在字段和属性部分中介绍。
-
类不需要与它们的源文件定义具有相同的基本名称,但在大多数情况下强烈建议这样做(另请参阅关于脚本的下一点)。
-
一个源文件可能包含一个或多个类(但如果文件包含任何不在类中的代码,则它被视为脚本)。脚本只是具有一些特殊约定的类,并且将与它们的源文件具有相同的名称(因此不要在与脚本源文件同名的脚本中包含类定义)。
以下代码提供了一个示例类。
class Person { (1)
String name (2)
Integer age
def increaseAge(Integer years) { (3)
this.age += years
}
}
1 | 类开始,名称为 Person |
2 | 字符串字段和属性名为 name |
3 | 方法定义 |
普通类
普通类是指顶层和具体的类。这意味着它们可以无限制地从任何其他类或脚本实例化。这样,它们只能是公共的(即使 public
关键字可以省略)。类通过调用它们的构造函数,使用 new
关键字实例化,如以下代码片段所示。
def p = new Person()
内部类
内部类定义在另一个类中。外部类可以像往常一样使用内部类。另一方面,内部类可以访问其外部类的成员,即使它们是私有的。除了外部类之外的类不允许访问内部类。这是一个例子
class Outer {
private String privateStr
def callInnerMethod() {
new Inner().methodA() (1)
}
class Inner { (2)
def methodA() {
println "${privateStr}." (3)
}
}
}
1 | 内部类被实例化,其方法被调用 |
2 | 内部类定义,在其外部类内部 |
3 | 即使是私有的,外部类的字段也被内部类访问 |
使用内部类有以下几个原因
-
它们通过向其他类隐藏内部类来增加封装性,这些类不需要了解内部类。这也导致更简洁的包和工作空间。
-
它们通过对仅由一个类使用的类进行分组,提供了良好的组织结构。
-
它们使代码更易于维护,因为内部类靠近使用它们的类。
内部类通常是某个接口的实现,其方法被外部类需要。下面的代码说明了这种典型的使用模式,此处与线程一起使用。
class Outer2 {
private String privateStr = 'some string'
def startThread() {
new Thread(new Inner2()).start()
}
class Inner2 implements Runnable {
void run() {
println "${privateStr}."
}
}
}
请注意,类 Inner2
的定义仅用于为类 Outer2
提供 run
方法的实现。匿名内部类在此情况下有助于消除冗余。该主题将在稍后介绍。
Groovy 3+ 还支持 Java 语法用于非静态内部类实例化,例如
class Computer {
class Cpu {
int coreNumber
Cpu(int coreNumber) {
this.coreNumber = coreNumber
}
}
}
assert 4 == new Computer().new Cpu(4).coreNumber
匿名内部类
前面内部类(Inner2
)的例子可以用匿名内部类简化。相同的功能可以通过以下代码实现
class Outer3 {
private String privateStr = 'some string'
def startThread() {
new Thread(new Runnable() { (1)
void run() {
println "${privateStr}."
}
}).start() (2)
}
}
1 | 与上一节的最后一个例子相比,new Inner2() 被替换为 new Runnable() 以及其所有实现 |
2 | 方法 start 正常调用 |
因此,无需定义一个新类只使用一次。
抽象类
抽象类表示通用概念,因此它们不能被实例化,而是被创建用于子类化。它们的成员包括字段/属性和抽象或具体方法。抽象方法没有实现,必须由具体子类实现。
abstract class Abstract { (1)
String name
abstract def abstractMethod() (2)
def concreteMethod() {
println 'concrete'
}
}
1 | 抽象类必须使用 abstract 关键字声明 |
2 | 抽象方法也必须用 abstract 关键字声明 |
抽象类通常与接口进行比较。选择其中之一至少有两个重要的区别。首先,抽象类可以包含字段/属性和具体方法,而接口只能包含抽象方法(方法签名)。此外,一个类可以实现多个接口,而它只能扩展一个类,无论是抽象的还是非抽象的。
继承
Groovy 中的继承类似于 Java 中的继承。它提供了一种机制,子类(或子类)可以重用父类(或超类)中的代码或属性。通过继承关联的类形成继承层次结构。公共行为和成员被推到层次结构的顶部以减少重复。专业化发生在子类中。
支持不同形式的继承
超类
父类与子类共享可见的字段、属性或方法。一个子类最多可以有一个父类。extends
关键字紧邻在超类类型之前使用。
接口
接口定义了类需要遵循的契约。接口只定义了需要实现的方法列表,但没有定义方法的实现。
interface Greeter { (1)
void greet(String name) (2)
}
1 | 接口需要使用 interface 关键字声明。 |
2 | 接口只定义方法签名。 |
接口中的方法始终是 public。在接口中使用 protected
或 private
方法是错误的。
interface Greeter {
protected void greet(String name) (1)
}
1 | 使用 protected 会导致编译时错误。 |
如果一个类在其 implements
列表中定义了接口,或者其任何超类定义了接口,则该类 实现 了该接口。
class SystemGreeter implements Greeter { (1)
void greet(String name) { (2)
println "Hello $name"
}
}
def greeter = new SystemGreeter()
assert greeter instanceof Greeter (3)
1 | SystemGreeter 使用 implements 关键字声明了 Greeter 接口。 |
2 | 然后实现了所需的 greet 方法。 |
3 | SystemGreeter 的任何实例也是 Greeter 接口的实例。 |
一个接口可以扩展另一个接口。
interface ExtendedGreeter extends Greeter { (1)
void sayBye(String name)
}
1 | ExtendedGreeter 接口使用 extends 关键字扩展了 Greeter 接口。 |
值得注意的是,要使一个类成为接口的实例,它必须是显式的。例如,以下类定义了 greet
方法,就像它在 Greeter
接口中声明的那样,但没有在其接口中声明 Greeter
。
class DefaultGreeter {
void greet(String name) { println "Hello" }
}
greeter = new DefaultGreeter()
assert !(greeter instanceof Greeter)
换句话说,Groovy 不定义结构化类型。但是,可以使用 as
强制转换运算符在运行时使对象实例实现接口。
greeter = new DefaultGreeter() (1)
coerced = greeter as Greeter (2)
assert coerced instanceof Greeter (3)
1 | 创建不实现接口的 DefaultGreeter 实例。 |
2 | 在运行时将实例强制转换为 Greeter 。 |
3 | 强制转换的实例实现了 Greeter 接口。 |
您可以看到有两个不同的对象:一个是源对象,一个 DefaultGreeter
实例,它不实现接口。另一个是 Greeter
的实例,它委托给被强制转换的对象。
Groovy 接口不支持像 Java 8 接口那样的默认实现。如果您正在寻找类似(但不完全相同)的东西,特质 类似于接口,但允许默认实现以及本手册中描述的其他重要功能。 |
1.4.3. 类成员
构造函数
构造函数是用于使用特定状态初始化对象的特殊方法。与普通方法一样,一个类可以声明多个构造函数,只要每个构造函数都有唯一的类型签名。如果对象在构造期间不需要任何参数,它可以使用 无参 构造函数。如果没有提供构造函数,Groovy 编译器将提供一个空的无参构造函数。
Groovy 支持两种调用方式
-
位置参数 的使用方式与 Java 构造函数类似。
-
命名参数 允许您在调用构造函数时指定参数名称。
位置参数
要使用位置参数创建对象,相应的类需要声明一个或多个构造函数。在多个构造函数的情况下,每个构造函数必须具有唯一的类型签名。构造函数也可以使用 groovy.transform.TupleConstructor 注解添加到类中。
通常,一旦声明了至少一个构造函数,就只能通过调用其构造函数之一来实例化该类。值得注意的是,在这种情况下,您通常不能使用命名参数创建类。Groovy 支持命名参数,只要类包含一个无参构造函数,或者提供一个以 Map
参数作为第一个(可能是唯一的)参数的构造函数 - 详情请参阅下一节。
使用已声明构造函数有三种形式。第一种是正常的 Java 方式,使用 new
关键字。其他方式依赖于将列表强制转换为所需类型。在这种情况下,可以使用 as
关键字和通过静态类型化变量进行强制转换。
class PersonConstructor {
String name
Integer age
PersonConstructor(name, age) { (1)
this.name = name
this.age = age
}
}
def person1 = new PersonConstructor('Marie', 1) (2)
def person2 = ['Marie', 2] as PersonConstructor (3)
PersonConstructor person3 = ['Marie', 3] (4)
1 | 构造函数声明 |
2 | 构造函数调用,经典 Java 方式 |
3 | 构造函数使用,使用 as 关键字进行强制转换 |
4 | 构造函数使用,在赋值中进行强制转换 |
命名参数
如果没有声明(或声明了无参)构造函数,则可以通过以 Map(属性/值对)的形式传递参数来创建对象。这在需要允许参数的多种组合的情况下非常方便。否则,通过使用传统的定位参数,将需要声明所有可能的构造函数。也支持以 Map
参数作为第一个(可能是唯一的)参数的构造函数 - 这样的构造函数也可以使用 groovy.transform.MapConstructor 注解添加。
class PersonWOConstructor { (1)
String name
Integer age
}
def person4 = new PersonWOConstructor() (2)
def person5 = new PersonWOConstructor(name: 'Marie') (3)
def person6 = new PersonWOConstructor(age: 1) (4)
def person7 = new PersonWOConstructor(name: 'Marie', age: 2) (5)
1 | 未声明构造函数 |
2 | 实例化时未提供参数 |
3 | 实例化时提供了 name 参数 |
4 | 实例化时提供了 age 参数 |
5 | 实例化时提供了 name 和 age 参数 |
然而,重要的是要强调,这种方法赋予了构造函数调用者更大的权力,同时也增加了调用者正确获取名称和值类型的责任。因此,如果需要更大的控制,可能更倾向于使用位置参数声明构造函数。
注意事项
-
虽然上面的例子没有提供构造函数,但您也可以提供一个无参构造函数,或者一个第一个参数是
Map
的构造函数,通常它是唯一的参数。 -
当没有声明(或声明了无参)构造函数时,Groovy 会将命名构造函数调用替换为对无参构造函数的调用,然后对每个提供的命名属性调用 setter。
-
当第一个参数是 Map 时,Groovy 会将所有命名参数组合成一个 Map(无论顺序如何),并将该 Map 作为第一个参数提供。如果您的属性声明为
final
(因为它们将在构造函数中设置,而不是之后通过 setter 设置),这可能是一个很好的方法。 -
您可以通过同时提供位置构造函数以及无参或 Map 构造函数来支持命名和位置构造。
-
您可以通过拥有一个第一个参数是 Map 但也有额外位置参数的构造函数来支持混合构造。请谨慎使用此样式。
方法
Groovy 方法与其他语言非常相似。一些特殊之处将在下一小节中展示。
方法定义
方法使用返回类型或 def
关键字定义,以使返回类型未类型化。方法还可以接收任意数量的参数,这些参数可能没有显式声明它们的类型。Java 修饰符可以正常使用,如果没有提供可见性修饰符,则方法是 public 的。
Groovy 中的方法总是返回一些值。如果没有提供 return
语句,则返回最后一行执行中评估的值。例如,请注意以下方法都没有使用 return
关键字。
def someMethod() { 'method called' } (1)
String anotherMethod() { 'another method called' } (2)
def thirdMethod(param1) { "$param1 passed" } (3)
static String fourthMethod(String param1) { "$param1 passed" } (4)
1 | 未声明返回类型且无参数的方法 |
2 | 具有显式返回类型且无参数的方法 |
3 | 具有未定义类型的参数的方法 |
4 | 带有 String 参数的静态方法 |
命名参数
与构造函数一样,普通方法也可以使用命名参数调用。为了支持这种表示法,使用了一种约定,其中方法的第一个参数是一个 Map
。在方法体中,参数值可以像普通 Map 一样访问(map.key
)。如果方法只有一个 Map 参数,则所有提供的参数都必须是命名参数。
def foo(Map args) { "${args.name}: ${args.age}" }
foo(name: 'Marie', age: 1)
命名参数可以与位置参数混合使用。在这种情况下,同样的约定适用,除了 Map
参数作为第一个参数外,相关方法还将根据需要具有额外的位置参数。调用方法时提供的位置参数必须按顺序排列。命名参数可以位于任何位置。它们被分组到 Map 中并自动作为第一个参数提供。
def foo(Map args, Integer number) { "${args.name}: ${args.age}, and the number is ${number}" }
foo(name: 'Marie', age: 1, 23) (1)
foo(23, name: 'Marie', age: 1) (2)
1 | 调用方法,带有额外的 Integer 类型的 number 参数 |
2 | 调用方法,参数顺序已更改 |
如果第一个参数不是 Map,那么必须为该参数提供一个 Map,而不是命名参数。否则将导致 groovy.lang.MissingMethodException
。
def foo(Integer number, Map args) { "${args.name}: ${args.age}, and the number is ${number}" }
foo(name: 'Marie', age: 1, 23) (1)
1 | 方法调用抛出 groovy.lang.MissingMethodException: No signature of method: foo() is applicable for argument types: (LinkedHashMap, Integer) values: [[name:Marie, age:1], 23] ,因为命名参数 Map 参数未定义为第一个参数。 |
如果我们用显式 Map
参数替换命名参数,则可以避免上述异常。
def foo(Integer number, Map args) { "${args.name}: ${args.age}, and the number is ${number}" }
foo(23, [name: 'Marie', age: 1]) (1)
1 | 用显式 Map 参数代替命名参数使调用有效。 |
尽管 Groovy 允许您混合使用命名参数和位置参数,但这可能会导致不必要的混淆。请谨慎混合使用命名参数和位置参数。 |
默认参数
默认参数使参数变为可选。如果未提供参数,则方法假定一个默认值。
def foo(String par1, Integer par2 = 1) { [name: par1, age: par2] }
assert foo('Marie').age == 1
参数从右侧删除,但强制参数永远不会删除。
def baz(a = 'a', int b, c = 'c', boolean d, e = 'e') { "$a $b $c $d $e" }
assert baz(42, true) == 'a 42 c true e'
assert baz('A', 42, true) == 'A 42 c true e'
assert baz('A', 42, 'C', true) == 'A 42 C true e'
assert baz('A', 42, 'C', true, 'E') == 'A 42 C true E'
同样的规则适用于构造函数和方法。如果使用 @TupleConstructor
,则适用其他配置选项。
可变参数(Varargs)
Groovy 支持具有可变数量参数的方法。它们的定义如下:def foo(p1, …, pn, T… args)
。这里 foo
默认支持 n
个参数,但也可以支持超过 n
个的未指定数量的额外参数。
def foo(Object... args) { args.length }
assert foo() == 0
assert foo(1) == 1
assert foo(1, 2) == 2
此示例定义了一个方法 foo
,它可以接受任意数量的参数,包括零参数。args.length
将返回给定参数的数量。Groovy 允许 T[]
作为 T…
的替代表示法。这意味着任何以数组作为最后一个参数的方法都被 Groovy 视为可以接受可变数量参数的方法。
def foo(Object[] args) { args.length }
assert foo() == 0
assert foo(1) == 1
assert foo(1, 2) == 2
如果用 null
作为可变参数调用可变参数方法,则参数将为 null
,而不是长度为一且只包含 null
作为唯一元素的数组。
def foo(Object... args) { args }
assert foo(null) == null
如果用数组作为参数调用可变参数方法,则参数将是该数组,而不是长度为一且只包含给定数组作为唯一元素的数组。
def foo(Object... args) { args }
Integer[] ints = [1, 2]
assert foo(ints) == [1, 2]
另一个重要点是可变参数与方法重载的组合。在方法重载的情况下,Groovy 将选择最具体的方法。例如,如果方法 foo
接受类型为 T
的可变参数,而另一个方法 foo
也接受一个类型为 T
的参数,则首选第二种方法。
def foo(Object... args) { 1 }
def foo(Object x) { 2 }
assert foo() == 1
assert foo(1) == 2
assert foo(1, 2) == 1
方法选择算法
动态 Groovy 支持多重分派(又称多方法)。调用方法时,实际调用的方法是根据方法参数的运行时类型动态确定的。首先将考虑方法名称和参数数量(包括可变参数的允许),然后是每个参数的类型。考虑以下方法定义
def method(Object o1, Object o2) { 'o/o' }
def method(Integer i, String s) { 'i/s' }
def method(String s, Integer i) { 's/i' }
可能正如预期的那样,用 String
和 Integer
参数调用 method
会调用我们的第三个方法定义。
assert method('foo', 42) == 's/i'
这里更令人感兴趣的是在编译时类型未知的情况。或许参数被声明为 Object
类型(在我们的例子中是这类对象的列表)。Java 会在所有情况下都选择 method(Object, Object)
变体(除非使用强制转换),但正如以下示例所示,Groovy 使用运行时类型,并将调用我们的每个方法一次(通常不需要强制转换)
List<List<Object>> pairs = [['foo', 1], [2, 'bar'], [3, 4]]
assert pairs.collect { a, b -> method(a, b) } == ['s/i', 'i/s', 'o/o']
对于我们三个方法调用中的前两个,找到了参数类型的精确匹配。对于第三个调用,没有找到 method(Integer, Integer)
的精确匹配,但 method(Object, Object)
仍然有效并将被选中。
方法选择的目标是从具有兼容参数类型的有效方法候选中找到 最接近的匹配。因此,method(Object, Object)
对于前两个调用也有效,但不如类型完全匹配的变体更接近。为了确定最接近的匹配,运行时有一个实际参数类型与声明参数类型的 距离 的概念,并尝试最小化所有参数的总距离。
下表说明了影响距离计算的一些因素。
方面 | 示例 |
---|---|
直接实现的接口比继承层次结构中更远的接口更匹配。 |
给定这些接口和方法定义
直接实现的接口将匹配
|
Object 数组优于 Object。 |
|
非可变参数变体优于可变参数变体。 |
|
如果两个可变参数变体都适用,则首选使用最少可变参数的那个。 |
|
接口优先于超类。 |
|
对于基本参数类型,首选相同或稍大的声明参数类型。 |
|
如果两个变体具有完全相同的距离,则认为这会产生歧义并导致运行时异常。
def method(Date d, Object o) { 'd/o' }
def method(Object o, String s) { 'o/s' }
def ex = shouldFail {
println method(new Date(), 'baz')
}
assert ex.message.contains('Ambiguous method overloading')
可以使用类型转换来选择所需的方法
assert method(new Date(), (Object)'baz') == 'd/o'
assert method((Object)new Date(), 'baz') == 'o/s'
异常声明
Groovy 自动允许您像处理未检查异常一样处理已检查异常。这意味着您不需要声明方法可能抛出的任何已检查异常,如下例所示,如果文件未找到,它可能会抛出 FileNotFoundException
def badRead() {
new File('doesNotExist.txt').text
}
shouldFail(FileNotFoundException) {
badRead()
}
您也无需将对上一个示例中 badRead
方法的调用放在 try/catch 块中 - 尽管如果您愿意,可以这样做。
如果您希望声明代码可能抛出的任何异常(已检查或未检查),您可以随意这样做。添加异常不会改变代码从任何其他 Groovy 代码中的使用方式,但可以被视为您代码的人类读者的文档。这些异常将成为字节码中方法声明的一部分,因此如果您的代码可能从 Java 调用,包含它们可能会很有用。以下示例说明了使用显式已检查异常声明
def badRead() throws FileNotFoundException {
new File('doesNotExist.txt').text
}
shouldFail(FileNotFoundException) {
badRead()
}
字段和属性
字段
字段是类、接口或特性(trait)的成员,用于存储数据。在 Groovy 源文件中定义的字段具有
-
强制性的 访问修饰符(
public
、protected
或private
) -
一个或多个可选的 修饰符(
static
、final
、synchronized
) -
一个可选的 类型
-
一个强制性的 名称
class Data {
private int id (1)
protected String description (2)
public static final boolean DEBUG = false (3)
}
1 | 一个名为 id 的 private 字段,类型为 int |
2 | 一个名为 description 的 protected 字段,类型为 String |
3 | 一个名为 DEBUG 的 public static final 字段,类型为 boolean |
字段可以在声明时直接初始化
class Data {
private String id = IDGenerator.next() (1)
// ...
}
1 | 私有字段 id 用 IDGenerator.next() 初始化。 |
可以省略字段的类型声明。然而,这被认为是一种不好的做法,通常建议对字段使用强类型。
class BadPractice {
private mapping (1)
}
class GoodPractice {
private Map<String,String> mapping (2)
}
1 | 字段 mapping 没有声明类型 |
2 | 字段 mapping 具有强类型 |
两者之间的区别很重要,如果您以后想使用可选的类型检查。它也作为文档化类设计的一种方式很重要。然而,在某些情况下,如脚本编写或您想依赖鸭子类型时,省略类型可能很有用。
属性
属性是类的一个外部可见特性。与其仅仅使用公共字段来表示这些特性(这提供了更有限的抽象并会限制重构的可能性),Java 中的典型方法是遵循 JavaBeans 规范 中概述的约定,即使用私有支持字段和 getter/setter 的组合来表示属性。Groovy 遵循相同的约定,但提供了更简单的方法来定义属性。您可以使用以下方式定义属性:
-
不带访问修饰符(无
public
、protected
或private
) -
一个或多个可选的 修饰符(
static
、final
、synchronized
) -
一个可选的 类型
-
一个强制性的 名称
Groovy 将生成相应的 getter/setter。例如
class Person {
String name (1)
int age (2)
}
1 | 创建一个支持的 private String name 字段,一个 getName 和一个 setName 方法 |
2 | 创建一个支持的 private int age 字段,一个 getAge 和一个 setAge 方法 |
如果属性声明为 final
,则不会生成 setter。
class Person {
final String name (1)
final int age (2)
Person(String name, int age) {
this.name = name (3)
this.age = age (4)
}
}
1 | 定义一个 String 类型的只读属性 |
2 | 定义一个 int 类型的只读属性 |
3 | 将 name 参数赋值给 name 字段 |
4 | 将 age 参数赋值给 age 字段 |
属性通过名称访问,并将透明地调用 getter 或 setter,除非代码在定义属性的类中。
class Person {
String name
void name(String name) {
this.name = "Wonder $name" (1)
}
String title() {
this.name (2)
}
}
def p = new Person()
p.name = 'Diana' (3)
assert p.name == 'Diana' (4)
p.name('Woman') (5)
assert p.title() == 'Wonder Woman' (6)
1 | this.name 将直接访问该字段,因为该属性是从定义它的类内部访问的。 |
2 | 类似地,读取访问直接在 name 字段上完成 |
3 | 对属性的写入访问是在 Person 类之外完成的,因此它会隐式调用 setName 。 |
4 | 对属性的读取访问是在 Person 类之外完成的,因此它会隐式调用 getName 。 |
5 | 这将调用 Person 上的 name 方法,该方法直接访问字段。 |
6 | 这将调用 Person 上的 title 方法,该方法直接读取字段。 |
值得注意的是,这种直接访问支持字段的行为是为了防止在定义属性的类内部使用属性访问语法时发生栈溢出。
通过实例的元 properties
字段,可以列出类的属性。
class Person {
String name
int age
}
def p = new Person()
assert p.properties.keySet().containsAll(['name','age'])
根据约定,即使没有提供支持字段,只要有遵循 Java Beans 规范的 getter 或 setter,Groovy 也会识别属性。例如:
class PseudoProperties {
// a pseudo property "name"
void setName(String name) {}
String getName() {}
// a pseudo read-only property "age"
int getAge() { 42 }
// a pseudo write-only property "groovy"
void setGroovy(boolean groovy) { }
}
def p = new PseudoProperties()
p.name = 'Foo' (1)
assert p.age == 42 (2)
p.groovy = true (3)
1 | 允许写入 p.name ,因为存在伪属性 name |
2 | 允许读取 p.age ,因为存在伪只读属性 age |
3 | 允许写入 p.groovy ,因为存在伪只写属性 groovy |
这种语法糖是许多用 Groovy 编写的 DSL 的核心。
通常建议属性名称的前两个字母小写,对于多词属性,建议使用驼峰命名法。在这些情况下,生成的 getter 和 setter 的名称将由属性名称大写并添加 get
或 set
前缀(或对于布尔型 getter 可选的 "is")组成。因此,getLength
将是 length
属性的 getter,setFirstName
将是 firstName
属性的 setter。isEmpty
可能是名为 empty
的属性的 getter 方法名称。
以大写字母开头的属性名称,其 getter/setter 只需添加前缀。因此,即使 |
JavaBeans 规范对通常可能是首字母缩写词的属性做了特殊规定。如果属性名称的前两个字母大写,则不进行大写(或者更重要的是,如果从访问器方法名称生成属性名称,则不进行小写)。因此,getURL
将是 URL
属性的 getter。
由于 JavaBeans 规范中的特殊“首字母缩写处理”属性命名逻辑,属性名称的转换是非对称的。这导致了一些奇怪的边界情况。Groovy 采用了一种命名约定,避免了一种可能看起来有点奇怪但在 Groovy 设计时流行并(到目前为止)出于历史原因而保留的歧义。Groovy 查看属性名称的第二个字母。如果它是大写字母,则该属性被视为首字母缩写样式属性,不进行大写;否则,进行正常大写。尽管我们 *从不* 推荐这样做,但它确实允许您拥有看似“重复命名”的属性,例如,您可以拥有 |
我们已经看到,属性是通过省略可见性修饰符来定义的。通常,任何其他修饰符(例如 transient
)都会复制到字段中。有以下两种特殊情况值得注意:
-
final
,我们前面提到它用于只读属性,它会被复制到支持字段上,但也会导致不定义 setter。 -
static
被复制到支持字段上,但也会导致访问器方法变为静态。
如果您希望将 final
等修饰符也传递给访问器方法,您可以手动编写属性,或者考虑使用 拆分属性定义。
注解,包括那些与 AST 转换相关的注解,都会被复制到属性的支持字段上。这允许将适用于字段的 AST 转换应用于属性,例如:
class Animal {
int lowerCount = 0
@Lazy String name = { lower().toUpperCase() }()
String lower() { lowerCount++; 'sloth' }
}
def a = new Animal()
assert a.lowerCount == 0 (1)
assert a.name == 'SLOTH' (2)
assert a.lowerCount == 1 (3)
1 | 确认没有及早初始化 |
2 | 正常属性访问 |
3 | 确认属性访问时初始化 |
Groovy 的属性语法是一种方便的简写方式,当您的类设计遵循与常见 JavaBean 实践一致的特定约定时。如果您的类不完全符合这些约定,您当然可以像在 Java 中一样手动编写 getter、setter 和支持字段。然而,Groovy 确实提供了拆分定义功能,它仍然提供缩短的语法,同时允许对约定进行微调。对于拆分定义,您编写一个具有相同名称和类型的字段和一个属性。字段或属性中只能有一个具有初始值。
对于拆分属性,字段上的注解保留在属性的支持字段上。定义中属性部分的注解则复制到 getter 和 setter 方法上。
这种机制允许属性用户在标准属性定义不完全符合其需求时使用许多常见的变体。例如,如果支持字段应该是 protected
而不是 private
class HasPropertyWithProtectedField {
protected String name (1)
String name (2)
}
1 | name 属性的 protected 支持字段,而不是正常的 private 支持字段 |
2 | 声明 name 属性 |
或者,同样的例子,但是使用包私有支持字段。
class HasPropertyWithPackagePrivateField {
String name (1)
@PackageScope String name (2)
}
1 | 声明 name 属性 |
2 | name 属性的包私有支持字段,而不是正常的 private 支持字段 |
作为最后一个例子,我们可能希望将与方法相关的 AST 转换或任何注解应用于 setter/getter,例如使访问器同步。
class HasPropertyWithSynchronizedAccessorMethods {
private String name (1)
@Synchronized String name (2)
}
1 | name 属性的支持字段 |
2 | 声明带有 setter/getter 注解的 name 属性 |
如果类中存在 getter 或 setter 的显式定义,则不会自动生成访问器方法。这允许您在需要时修改此类 getter 或 setter 的正常行为。通常不考虑继承的访问器方法,但如果继承的访问器方法被标记为 final,这也将导致不生成额外的访问器方法,以遵守此类方法不可子类化的 final
要求。
1.4.4. 注解
注解定义
注解是一种特殊的接口,用于注解代码元素。注解是一种类型,其超接口是 java.lang.annotation.Annotation 接口。注解的声明方式与接口非常相似,使用 @interface
关键字。
@interface SomeAnnotation {}
注解可以定义成员,形式为没有方法体和可选默认值的方法。可能的成员类型仅限于
-
基本类型
-
或以上任何类型的数组
例如
@interface SomeAnnotation {
String value() (1)
}
@interface SomeAnnotation {
String value() default 'something' (2)
}
@interface SomeAnnotation {
int step() (3)
}
@interface SomeAnnotation {
Class appliesTo() (4)
}
@interface SomeAnnotation {}
@interface SomeAnnotations {
SomeAnnotation[] value() (5)
}
enum DayOfWeek { mon, tue, wed, thu, fri, sat, sun }
@interface Scheduled {
DayOfWeek dayOfWeek() (6)
}
1 | 定义一个 String 类型的 value 成员的注解 |
2 | 定义一个 String 类型的 value 成员,默认值为 something 的注解 |
3 | 定义一个 int 原始类型的 step 成员的注解 |
4 | 定义一个 Class 类型的 appliesTo 成员的注解 |
5 | 定义一个 value 成员的注解,其类型是另一个注解类型的数组 |
6 | 定义一个 DayOfWeek 枚举类型的 dayOfWeek 成员的注解 |
与 Java 语言不同,在 Groovy 中,注解可以用来改变语言的语义。AST 转换尤其如此,它将根据注解生成代码。
注解放置
注解可以应用于代码的各种元素。
@SomeAnnotation (1)
void someMethod() {
// ...
}
@SomeAnnotation (2)
class SomeClass {}
@SomeAnnotation String var (3)
1 | @SomeAnnotation 适用于 someMethod 方法 |
2 | @SomeAnnotation 适用于 SomeClass 类 |
3 | @SomeAnnotation 适用于 var 变量 |
为了限制注解的应用范围,有必要在注解定义上使用 java.lang.annotation.Target 注解来声明。例如,以下是如何声明一个注解可以应用于类或方法:
import java.lang.annotation.ElementType
import java.lang.annotation.Target
@Target([ElementType.METHOD, ElementType.TYPE]) (1)
@interface SomeAnnotation {} (2)
1 | @Target 注解旨在为注解标记一个作用域。 |
2 | 因此,@SomeAnnotation 将只允许在 TYPE 或 METHOD 上使用。 |
可能的 target 列表可在 java.lang.annotation.ElementType 中找到。
Groovy 不支持 Java 8 中引入的 java.lang.annotation.ElementType#TYPE_PARAMETER 和 java.lang.annotation.ElementType#TYPE_PARAMETER 元素类型。 |
注解成员值
使用注解时,要求至少设置所有没有默认值的成员。例如:
@interface Page {
int statusCode()
}
@Page(statusCode=404)
void notFound() {
// ...
}
但是,如果只设置了 value
成员,则在声明注解值时可以省略 value=
。
@interface Page {
String value()
int statusCode() default 200
}
@Page(value='/home') (1)
void home() {
// ...
}
@Page('/users') (2)
void userList() {
// ...
}
@Page(value='error',statusCode=404) (3)
void notFound() {
// ...
}
1 | 我们可以省略 statusCode ,因为它有默认值,但 value 需要设置。 |
2 | 由于 value 是唯一的没有默认值的强制成员,我们可以省略 value= 。 |
3 | 如果 value 和 statusCode 都需要设置,则需要为默认 value 成员使用 value= 。 |
保留策略
注解的可见性取决于其保留策略。注解的保留策略使用 java.lang.annotation.Retention 注解设置。
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
@Retention(RetentionPolicy.SOURCE) (1)
@interface SomeAnnotation {} (2)
1 | @Retention 注解标记了 @SomeAnnotation 注解。 |
2 | 因此 @SomeAnnotation 将具有 SOURCE 保留策略。 |
可能的保留目标列表和描述可在 java.lang.annotation.RetentionPolicy 枚举中找到。选择通常取决于您希望注解在编译时还是运行时可见。
闭包注解参数
Groovy 中注解的一个有趣特性是,您可以将闭包用作注解值。因此,注解可以与各种表达式一起使用,并且仍然支持 IDE。例如,想象一个框架,您希望根据 JDK 版本或操作系统等环境约束执行某些方法。您可以编写以下代码:
class Tasks {
Set result = []
void alwaysExecuted() {
result << 1
}
@OnlyIf({ jdk>=6 })
void supportedOnlyInJDK6() {
result << 'JDK 6'
}
@OnlyIf({ jdk>=7 && windows })
void requiresJDK7AndWindows() {
result << 'JDK 7 Windows'
}
}
为了使 @OnlyIf
注解接受 Closure
作为参数,您只需将 value
声明为 Class
。
@Retention(RetentionPolicy.RUNTIME)
@interface OnlyIf {
Class value() (1)
}
为了完成这个例子,我们来编写一个使用这些信息的示例运行器。
class Runner {
static <T> T run(Class<T> taskClass) {
def tasks = taskClass.newInstance() (1)
def params = [jdk: 6, windows: false] (2)
tasks.class.declaredMethods.each { m -> (3)
if (Modifier.isPublic(m.modifiers) && m.parameterTypes.length == 0) { (4)
def onlyIf = m.getAnnotation(OnlyIf) (5)
if (onlyIf) {
Closure cl = onlyIf.value().newInstance(tasks,tasks) (6)
cl.delegate = params (7)
if (cl()) { (8)
m.invoke(tasks) (9)
}
} else {
m.invoke(tasks) (10)
}
}
}
tasks (11)
}
}
1 | 创建作为参数传递的类(任务类)的新实例。 |
2 | 模拟 JDK 6 且非 Windows 的环境。 |
3 | 遍历任务类的所有已声明方法。 |
4 | 如果方法是 public 且不带参数 |
5 | 尝试查找 @OnlyIf 注解 |
6 | 如果找到它,获取 value 并从中创建一个新的 Closure 。 |
7 | 将闭包的 delegate 设置为我们的环境变量。 |
8 | 调用闭包,它是注解闭包。它将返回一个 boolean 值。 |
9 | 如果为 true ,则调用方法 |
10 | 如果方法没有被 @OnlyIf 注解,则无论如何都执行该方法。 |
11 | 之后,返回任务对象。 |
然后,运行器可以这样使用:
def tasks = Runner.run(Tasks)
assert tasks.result == [1, 'JDK 6'] as Set
元注解
声明元注解
元注解,也称为注解别名,是在编译时被其他注解替换的注解(一个元注解是一个或多个注解的别名)。元注解可以用来减少涉及多个注解的代码大小。
让我们从一个简单的例子开始。想象您有 @Service
和 @Transactional
注解,并且您想用这两个注解来注解一个类。
@Service
@Transactional
class MyTransactionalService {}
考虑到您可以添加到同一个类的注解数量激增,元注解可以通过将两个注解减少为一个具有相同语义的注解来提供帮助。例如,我们可能希望这样编写:
@TransactionalService (1)
class MyTransactionalService {}
1 | @TransactionalService 是一个元注解 |
元注解被声明为一个常规注解,但用 @AnnotationCollector
和它所收集的注解列表进行注解。在我们的例子中,@TransactionalService
注解可以这样编写:
import groovy.transform.AnnotationCollector
@Service (1)
@Transactional (2)
@AnnotationCollector (3)
@interface TransactionalService {
}
1 | 用 @Service 注解元注解 |
2 | 用 @Transactional 注解元注解 |
3 | 用 @AnnotationCollector 注解元注解 |
元注解的行为
Groovy 支持 预编译 和 源代码形式 的元注解。这意味着您的元注解 可以 预编译,也可以将其与您当前正在编译的源文件放在同一源树中。
信息:元注解是 Groovy 独有的特性。您无法用元注解来注解 Java 类,并期望它在 Groovy 中也能做到同样的事情。同样,您不能用 Java 编写元注解:元注解的定义 和 使用都必须是 Groovy 代码。但是,您可以在元注解中愉快地收集 Java 注解和 Groovy 注解。
当 Groovy 编译器遇到用元注解注解的类时,它会 替换 为收集到的注解。因此,在前面的例子中,它将 @TransactionalService
替换为 @Transactional
和 @Service
。
def annotations = MyTransactionalService.annotations*.annotationType()
assert (Service in annotations)
assert (Transactional in annotations)
从元注解到收集注解的转换在 语义分析 编译阶段执行。
除了用收集到的注解替换别名外,元注解还能够处理它们,包括参数。
元注解参数
元注解可以收集带有参数的注解。为了说明这一点,我们将想象两个注解,每个注解都接受一个参数。
@Timeout(after=3600)
@Dangerous(type='explosive')
假设您要创建一个名为 @Explosive
的元注解。
@Timeout(after=3600)
@Dangerous(type='explosive')
@AnnotationCollector
public @interface Explosive {}
默认情况下,当注解被替换时,它们将获得 在别名中定义 的注解参数值。更重要的是,元注解支持覆盖特定值。
@Explosive(after=0) (1)
class Bomb {}
1 | 作为参数提供给 @Explosive 的 after 值会覆盖 @Timeout 注解中定义的值。 |
如果两个注解定义了相同的参数名,则默认处理器会将注解值复制到所有接受此参数的注解中。
@Retention(RetentionPolicy.RUNTIME)
public @interface Foo {
String value() (1)
}
@Retention(RetentionPolicy.RUNTIME)
public @interface Bar {
String value() (2)
}
@Foo
@Bar
@AnnotationCollector
public @interface FooBar {} (3)
@Foo('a')
@Bar('b')
class Bob {} (4)
assert Bob.getAnnotation(Foo).value() == 'a' (5)
println Bob.getAnnotation(Bar).value() == 'b' (6)
@FooBar('a')
class Joe {} (7)
assert Joe.getAnnotation(Foo).value() == 'a' (8)
println Joe.getAnnotation(Bar).value() == 'a' (9)
1 | @Foo 注解定义了 String 类型的 value 成员。 |
2 | @Bar 注解也定义了 String 类型的 value 成员。 |
3 | @FooBar 元注解聚合了 @Foo 和 @Bar 。 |
4 | 类 Bob 用 @Foo 和 @Bar 注解。 |
5 | Bob 上 @Foo 注解的值是 a 。 |
6 | 而 Bob 上 @Bar 注解的值是 b 。 |
7 | 类 Joe 用 @FooBar 注解。 |
8 | 则 Joe 上 @Foo 注解的值是 a 。 |
9 | 并且 Joe 上 @Bar 注解的值也是 a 。 |
在第二种情况下,元注解的值被复制到 @Foo
和 @Bar
两个注解中。
如果收集到的注解定义了相同但类型不兼容的成员,则会发生编译时错误。例如,如果上一个示例中 @Foo 定义了一个 String 类型的值,但 @Bar 定义了一个 int 类型的值。 |
然而,可以自定义元注解的行为,并描述如何扩展收集到的注解。我们将很快讨论如何做到这一点,但首先需要介绍一个高级处理选项。
处理元注解中的重复注解
@AnnotationCollector
注解支持一个 mode
参数,该参数可用于在存在重复注解的情况下更改默认处理器处理注解替换的方式。
信息:自定义处理器(接下来讨论)可能支持也可能不支持此参数。
例如,假设您创建一个包含 @ToString
注解的元注解,然后将您的元注解放置在一个已经具有显式 @ToString
注解的类上。这应该是一个错误吗?两个注解都应该应用吗?一个是否优先于另一个?没有正确的答案。在某些情况下,任何这些答案都可能非常合适。因此,Groovy 没有尝试预先确定处理重复注解问题的正确方法,而是允许您编写自己的自定义元注解处理器(接下来介绍),并允许您在 AST 转换中编写任何您喜欢的检查逻辑——AST 转换是聚合的常见目标。话虽如此,通过简单地设置 mode
,许多常见预期的场景都会在任何额外的编码中自动为您处理。mode
参数的行为由所选的 AnnotationCollectorMode
枚举值决定,并汇总在下表中。
模式 |
描述 |
复制 |
注解集合中的注解将始终插入。所有转换运行后,如果存在多个注解(不包括那些带有 SOURCE 保留策略的注解),则会发生错误。 |
优先收集器 |
将添加收集器中的注解,并删除任何具有相同名称的现有注解。 |
优先收集器合并 |
将添加收集器中的注解,并删除任何具有相同名称的现有注解,但现有注解中找到的任何新参数将合并到添加的注解中。 |
优先显式 |
如果找到任何具有相同名称的现有注解,则收集器中的注解将被忽略。 |
优先显式合并 |
如果发现任何同名的现有注解,则收集器中的注解将被忽略,但收集器注解上的任何新参数将被添加到现有注解中。 |
自定义元注解处理器
自定义注解处理器将让您选择如何将元注解扩展为收集到的注解。在这种情况下,元注解的行为完全由您决定。为此,您必须:
-
创建一个元注解处理器,扩展 org.codehaus.groovy.transform.AnnotationCollectorTransform。
-
在元注解声明中声明要使用的处理器。
为了说明这一点,我们将探讨 @CompileDynamic
元注解是如何实现的。
@CompileDynamic
是一个元注解,它自身扩展为 @CompileStatic(TypeCheckingMode.SKIP)
。问题是默认的元注解处理器不支持枚举,而注解值 TypeCheckingMode.SKIP
就是一个枚举。
这里天真的实现是行不通的。
@CompileStatic(TypeCheckingMode.SKIP)
@AnnotationCollector
public @interface CompileDynamic {}
相反,我们将这样定义它:
@AnnotationCollector(processor = "org.codehaus.groovy.transform.CompileDynamicProcessor")
public @interface CompileDynamic {
}
您可能首先注意到的是,我们的接口不再用 @CompileStatic
注解。原因是,我们转而依赖 processor
参数,它引用了一个将 生成 注解的类。
自定义处理器的实现方式如下:
@CompileStatic (1)
class CompileDynamicProcessor extends AnnotationCollectorTransform { (2)
private static final ClassNode CS_NODE = ClassHelper.make(CompileStatic) (3)
private static final ClassNode TC_NODE = ClassHelper.make(TypeCheckingMode) (4)
List<AnnotationNode> visit(AnnotationNode collector, (5)
AnnotationNode aliasAnnotationUsage, (6)
AnnotatedNode aliasAnnotated, (7)
SourceUnit source) { (8)
def node = new AnnotationNode(CS_NODE) (9)
def enumRef = new PropertyExpression(
new ClassExpression(TC_NODE), "SKIP") (10)
node.addMember("value", enumRef) (11)
Collections.singletonList(node) (12)
}
}
1 | 我们的自定义处理器是用 Groovy 编写的,为了提高编译性能,我们使用静态编译。 |
2 | 自定义处理器必须扩展 org.codehaus.groovy.transform.AnnotationCollectorTransform |
3 | 创建一个表示 @CompileStatic 注解类型的类节点。 |
4 | 创建一个表示 TypeCheckingMode 枚举类型的类节点。 |
5 | collector 是在元注解中找到的 @AnnotationCollector 节点。通常不使用。 |
6 | aliasAnnotationUsage 是正在扩展的元注解,这里是 @CompileDynamic 。 |
7 | aliasAnnotated 是被元注解标注的节点。 |
8 | sourceUnit 是正在编译的 SourceUnit 。 |
9 | 我们为 @CompileStatic 创建一个新的注解节点。 |
10 | 我们创建一个等同于 TypeCheckingMode.SKIP 的表达式。 |
11 | 我们将该表达式添加到注解节点,现在它变成了 @CompileStatic(TypeCheckingMode.SKIP) 。 |
12 | 返回生成的注解 |
在这个例子中,visit
方法是唯一必须被重写的方法。它的目的是返回一个注解节点列表,这些节点将被添加到用元注解注解的节点中。在这个例子中,我们返回一个对应于 @CompileStatic(TypeCheckingMode.SKIP)
的单个节点。
1.4.5. 特质
特质是语言的结构化构造,允许:
-
行为组合
-
接口的运行时实现
-
行为覆盖
-
与静态类型检查/编译的兼容性
它们可以被视为既带有 默认实现 又带有 状态 的 接口。特质使用 trait
关键字定义。
trait FlyingAbility { (1)
String fly() { "I'm flying!" } (2)
}
1 | 特质的声明 |
2 | 特质内部方法的声明 |
然后它可以像普通接口一样使用 implements
关键字。
class Bird implements FlyingAbility {} (1)
def b = new Bird() (2)
assert b.fly() == "I'm flying!" (3)
1 | 将特质 FlyingAbility 添加到 Bird 类的功能中 |
2 | 实例化一个新的 Bird |
3 | Bird 类自动获得了 FlyingAbility 特质的行为。 |
特质提供了广泛的功能,从简单的组合到测试,本节将详细描述这些功能。
方法
公共方法
在特质中声明方法可以像类中的任何常规方法一样完成。
trait FlyingAbility { (1)
String fly() { "I'm flying!" } (2)
}
1 | 特质的声明 |
2 | 特质内部方法的声明 |
抽象方法
此外,特质也可以声明 抽象 方法,因此需要在实现特质的类中实现它们。
trait Greetable {
abstract String name() (1)
String greeting() { "Hello, ${name()}!" } (2)
}
1 | 实现类将必须声明 name 方法 |
2 | 可以与具体方法混合使用 |
然后特质可以这样使用:
class Person implements Greetable { (1)
String name() { 'Bob' } (2)
}
def p = new Person()
assert p.greeting() == 'Hello, Bob!' (3)
1 | 实现特质 Greetable |
2 | 由于 name 是抽象的,因此需要实现它。 |
3 | 然后可以调用 greeting 。 |
私有方法
特质也可以定义私有方法。这些方法不会出现在特质契约接口中。
trait Greeter {
private String greetingMessage() { (1)
'Hello from a private method!'
}
String greet() {
def m = greetingMessage() (2)
println m
m
}
}
class GreetingMachine implements Greeter {} (3)
def g = new GreetingMachine()
assert g.greet() == "Hello from a private method!" (4)
try {
assert g.greetingMessage() (5)
} catch (MissingMethodException e) {
println "greetingMessage is private in trait"
}
1 | 在特质中定义一个私有方法 greetingMessage |
2 | 公共 greet 消息默认调用 greetingMessage 。 |
3 | 创建一个实现特质的类 |
4 | 可以调用 greet |
5 | 但不能调用 greetingMessage 。 |
特质只支持 public 和 private 方法。不支持 protected 和 package private 作用域。 |
final 方法
如果我们有一个类实现了一个特性,从概念上讲,特性方法中的实现被“继承”到类中。但是,实际上,没有包含此类实现的基类。相反,它们直接编织到类中。方法上的 final 修饰符仅指示编织方法的修饰符。虽然继承和重写或多重继承具有相同签名但混合了 final 和非 final 变体的方法可能被认为是糟糕的风格,但 Groovy 并不禁止这种情况。正常的方法选择适用,并且所使用的修饰符将由结果方法确定。如果您希望特性实现方法不能被重写,您可以考虑创建一个实现所需特性的基类。
“this”的含义
this
表示实现实例。将特质视为超类。这意味着当您编写
trait Introspector {
def whoAmI() { this }
}
class Foo implements Introspector {}
def foo = new Foo()
然后调用
foo.whoAmI()
将返回相同的实例
assert foo.whoAmI().is(foo)
接口
特质可以实现接口,在这种情况下,接口使用 implements
关键字声明。
interface Named { (1)
String name()
}
trait Greetable implements Named { (2)
String greeting() { "Hello, ${name()}!" }
}
class Person implements Greetable { (3)
String name() { 'Bob' } (4)
}
def p = new Person()
assert p.greeting() == 'Hello, Bob!' (5)
assert p instanceof Named (6)
assert p instanceof Greetable (7)
1 | 普通接口的声明 |
2 | 将 Named 添加到已实现的接口列表 |
3 | 声明一个实现 Greetable 特质的类 |
4 | 实现缺失的 name 方法 |
5 | greeting 实现来自特质 |
6 | 确保 Person 实现了 Named 接口 |
7 | 确保 Person 实现了 Greetable 特质 |
属性
特质可以定义属性,例如以下示例:
trait Named {
String name (1)
}
class Person implements Named {} (2)
def p = new Person(name: 'Bob') (3)
assert p.name == 'Bob' (4)
assert p.getName() == 'Bob' (5)
1 | 在特质中声明一个属性 name |
2 | 声明一个实现该特质的类 |
3 | 属性自动可见 |
4 | 它可以使用常规属性访问器进行访问 |
5 | 或者使用常规的 getter 语法 |
字段
私有字段
由于特质允许使用私有方法,因此使用私有字段来存储状态也可能很有趣。特质将允许您这样做。
trait Counter {
private int count = 0 (1)
int count() { count += 1; count } (2)
}
class Foo implements Counter {} (3)
def f = new Foo()
assert f.count() == 1 (4)
assert f.count() == 2
1 | 在特质中声明一个私有字段 count |
2 | 声明一个公共方法 count ,该方法增加计数器并返回它。 |
3 | 声明一个实现 Counter 特质的类 |
4 | count 方法可以使用私有字段来保持状态。 |
这是与 Java 8 虚拟扩展方法 的主要区别。虚拟扩展方法不携带状态,而特质可以。此外,Groovy 中的特质从 Java 6 开始就得到支持,因为它们的实现不依赖于虚拟扩展方法。这意味着即使一个特质可以从 Java 类中看作一个常规接口,该接口也 不会 具有默认方法,只有抽象方法。 |
公共字段
公共字段的工作方式与私有字段相同,但为了避免菱形问题,字段名称在实现类中会被重新映射。
trait Named {
public String name (1)
}
class Person implements Named {} (2)
def p = new Person() (3)
p.Named__name = 'Bob' (4)
1 | 在特质中声明一个公共 字段 |
2 | 声明一个实现该特质的类 |
3 | 创建该类的一个实例 |
4 | 公共字段可用,但已重命名 |
字段的名称取决于特质的完全限定名。包中的所有点(.
)都替换为下划线(_
),最终名称包含双下划线。因此,如果字段的类型是 String
,包名为 my.package
,特质名为 Foo
,字段名为 bar
,则在实现类中,公共字段将显示为
String my_package_Foo__bar
虽然特质支持公共字段,但建议不要使用它们,这被认为是一种不好的做法。 |
行为组合
特质可以用来以受控方式实现多重继承。例如,我们可以有以下特质:
trait FlyingAbility { (1)
String fly() { "I'm flying!" } (2)
}
trait SpeakingAbility {
String speak() { "I'm speaking!" }
}
以及一个实现两个特质的类
class Duck implements FlyingAbility, SpeakingAbility {} (1)
def d = new Duck() (2)
assert d.fly() == "I'm flying!" (3)
assert d.speak() == "I'm speaking!" (4)
1 | Duck 类实现了 FlyingAbility 和 SpeakingAbility 。 |
2 | 创建 Duck 的新实例 |
3 | 我们可以调用 FlyingAbility 中的 fly 方法。 |
4 | 也可以调用 SpeakingAbility 中的 speak 方法。 |
特质鼓励对象之间功能的重用,以及通过现有行为的组合创建新类。
覆盖默认方法
特质提供了方法的默认实现,但可以在实现类中覆盖它们。例如,我们可以稍微修改上面的例子,让鸭子嘎嘎叫。
class Duck implements FlyingAbility, SpeakingAbility {
String quack() { "Quack!" } (1)
String speak() { quack() } (2)
}
def d = new Duck()
assert d.fly() == "I'm flying!" (3)
assert d.quack() == "Quack!" (4)
assert d.speak() == "Quack!" (5)
1 | 定义一个 Duck 特有的方法,名为 quack 。 |
2 | 覆盖 speak 的默认实现,以便我们使用 quack 代替。 |
3 | 鸭子仍然在飞,来自默认实现。 |
4 | quack 来自 Duck 类 |
5 | speak 不再使用 SpeakingAbility 的默认实现。 |
扩展特质
简单继承
特质可以扩展另一个特质,在这种情况下必须使用 extends
关键字。
trait Named {
String name (1)
}
trait Polite extends Named { (2)
String introduce() { "Hello, I am $name" } (3)
}
class Person implements Polite {}
def p = new Person(name: 'Alice') (4)
assert p.introduce() == 'Hello, I am Alice' (5)
1 | Named 特质定义了一个 name 属性。 |
2 | Polite 特质 扩展 了 Named 特质。 |
3 | Polite 添加了一个新方法,该方法可以访问超特质的 name 属性。 |
4 | name 属性从实现 Polite 的 Person 类中可见。 |
5 | introduce 方法也是如此。 |
多重继承
或者,一个特质可以扩展多个特质。在这种情况下,所有超特质都必须在 implements
子句中声明。
trait WithId { (1)
Long id
}
trait WithName { (2)
String name
}
trait Identified implements WithId, WithName {} (3)
1 | WithId 特质定义了 id 属性。 |
2 | WithName 特质定义了 name 属性。 |
3 | Identified 是一个继承了 WithId 和 WithName 的特质。 |
鸭子类型与特质
动态代码
特质可以调用任何动态代码,就像普通的 Groovy 类一样。这意味着您可以在方法体内调用预期存在于实现类中的方法,而无需在接口中显式声明它们。这意味着特质与鸭子类型完全兼容。
trait SpeakingDuck {
String speak() { quack() } (1)
}
class Duck implements SpeakingDuck {
String methodMissing(String name, args) {
"${name.capitalize()}!" (2)
}
}
def d = new Duck()
assert d.speak() == 'Quack!' (3)
1 | SpeakingDuck 期望定义 quack 方法。 |
2 | Duck 类确实使用 methodMissing 实现了该方法。 |
3 | 调用 speak 方法会触发对 quack 的调用,该调用由 methodMissing 处理。 |
特质中的动态方法
特质也可以实现 MOP 方法,例如 methodMissing
或 propertyMissing
,在这种情况下,实现类将从特质继承行为,例如此示例:
trait DynamicObject { (1)
private Map props = [:]
def methodMissing(String name, args) {
name.toUpperCase()
}
def propertyMissing(String name) {
props.get(name)
}
void setProperty(String name, Object value) {
props.put(name, value)
}
}
class Dynamic implements DynamicObject {
String existingProperty = 'ok' (2)
String existingMethod() { 'ok' } (3)
}
def d = new Dynamic()
assert d.existingProperty == 'ok' (4)
assert d.foo == null (5)
d.foo = 'bar' (6)
assert d.foo == 'bar' (7)
assert d.existingMethod() == 'ok' (8)
assert d.someMethod() == 'SOMEMETHOD' (9)
1 | 创建一个实现多个 MOP 方法的特质 |
2 | Dynamic 类定义了一个属性。 |
3 | Dynamic 类定义了一个方法。 |
4 | 调用现有属性将调用 Dynamic 中的方法。 |
5 | 调用不存在的属性将调用特质中的方法。 |
6 | 将调用特质上定义的 setProperty 。 |
7 | 将调用特质上定义的 getProperty 。 |
8 | 调用 Dynamic 上的现有方法。 |
9 | 但由于特质的 methodMissing ,可以调用不存在的方法。 |
多重继承冲突
默认冲突解决
一个类可能实现多个特质。如果某个特质定义了一个与另一个特质中同签名的方法,则会发生冲突。
trait A {
String exec() { 'A' } (1)
}
trait B {
String exec() { 'B' } (2)
}
class C implements A,B {} (3)
1 | 特质 A 定义了一个名为 exec 的方法,返回 String 。 |
2 | 特质 B 定义了完全相同的方法。 |
3 | 类 C 实现了这两个特质。 |
在这种情况下,默认行为是 implements
子句中 最后声明的特质 中的方法获胜。在这里,B
在 A
之后声明,因此将选择 B
中的方法。
def c = new C()
assert c.exec() == 'B'
用户冲突解决
如果这种行为不是您想要的,您可以使用 Trait.super.foo
语法显式选择要调用的方法。在上面的例子中,我们可以通过编写以下代码来确保调用特质 A 中的方法:
class C implements A,B {
String exec() { A.super.exec() } (1)
}
def c = new C()
assert c.exec() == 'A' (2)
1 | 显式调用特质 A 中的 exec 。 |
2 | 调用 A 中的版本,而不是使用默认解决方案(即 B 中的版本)。 |
特质的运行时实现
在运行时实现特质
Groovy 还支持在运行时动态实现特质。它允许您使用特质“装饰”现有对象。例如,让我们从以下特质和类开始:
trait Extra {
String extra() { "I'm an extra method" } (1)
}
class Something { (2)
String doSomething() { 'Something' } (3)
}
1 | Extra 特质定义了一个 extra 方法。 |
2 | Something 类 不 实现 Extra 特质。 |
3 | Something 只定义了一个方法 doSomething 。 |
然后如果我们这样做
def s = new Something()
s.extra()
调用 extra 将失败,因为 Something
没有实现 Extra
。可以使用以下语法在运行时实现它。
def s = new Something() as Extra (1)
s.extra() (2)
s.doSomething() (3)
1 | 使用 as 关键字在 运行时 将对象强制转换为特质。 |
2 | 然后可以在对象上调用 extra 。 |
3 | 并且 doSomething 仍然可以调用。 |
当将对象强制转换为特质时,操作的结果与原始实例不同。可以保证被强制转换的对象将同时实现特质 和 原始对象实现的接口,但结果 不是 原始类的实例。 |
同时实现多个特质
如果您需要同时实现多个特质,可以使用 withTraits
方法而不是 as
关键字。
trait A { void methodFromA() {} }
trait B { void methodFromB() {} }
class C {}
def c = new C()
c.methodFromA() (1)
c.methodFromB() (2)
def d = c.withTraits A, B (3)
d.methodFromA() (4)
d.methodFromB() (5)
1 | 调用 methodFromA 将失败,因为 C 没有实现 A 。 |
2 | 调用 methodFromB 将失败,因为 C 没有实现 B 。 |
3 | withTrait 将把 c 包装成一个实现了 A 和 B 的对象。 |
4 | methodFromA 现在将通过,因为 d 实现了 A 。 |
5 | methodFromB 现在也将通过,因为 d 也实现了 B 。 |
当将对象强制转换为多个特质时,操作的结果与原始实例不同。可以保证被强制转换的对象将同时实现特质 和 原始对象实现的接口,但结果 不是 原始类的实例。 |
链式行为
Groovy 支持 可堆叠特质 的概念。其思想是,如果当前特质无法处理消息,则委托给另一个特质。为了说明这一点,让我们想象一个消息处理程序接口,如下所示:
interface MessageHandler {
void on(String message, Map payload)
}
然后,您可以通过应用小行为来组合消息处理程序。例如,让我们以特质的形式定义一个默认处理程序。
trait DefaultHandler implements MessageHandler {
void on(String message, Map payload) {
println "Received $message with payload $payload"
}
}
然后任何类都可以通过实现该特质来继承默认处理程序的行为。
class SimpleHandler implements DefaultHandler {}
现在,如果您想在默认处理程序之外记录所有消息,该怎么办?一种选择是这样写:
class SimpleHandlerWithLogging implements DefaultHandler {
void on(String message, Map payload) { (1)
println "Seeing $message with payload $payload" (2)
DefaultHandler.super.on(message, payload) (3)
}
}
1 | 显式实现 on 方法 |
2 | 执行日志记录 |
3 | 通过委托给 DefaultHandler 特质继续。 |
这可行,但这种方法有缺点:
-
日志逻辑绑定到一个“具体”的处理程序。
-
我们在
on
方法中显式引用了DefaultHandler
,这意味着如果我们要更改类实现的特质,代码就会被破坏。
作为替代方案,我们可以编写另一个职责仅限于日志记录的特质。
trait LoggingHandler implements MessageHandler { (1)
void on(String message, Map payload) {
println "Seeing $message with payload $payload" (2)
super.on(message, payload) (3)
}
}
1 | 日志处理程序本身就是一个处理程序。 |
2 | 打印收到的消息 |
3 | 然后 super 使其将调用委托给链中的下一个特质。 |
然后我们的类可以重写为这样:
class HandlerWithLogger implements DefaultHandler, LoggingHandler {}
def loggingHandler = new HandlerWithLogger()
loggingHandler.on('test logging', [:])
这将打印
Seeing test logging with payload [:] Received test logging with payload [:]
由于优先级规则表明 LoggerHandler
获胜,因为它最后声明,所以对 on
的调用将使用 LoggingHandler
的实现。但后者有一个对 super
的调用,这意味着链中的下一个特质。这里,下一个特质是 DefaultHandler
,所以 两者 都将被调用。
如果我们添加第三个处理程序,负责处理以 say
开头的消息,这种方法的兴趣就更加明显了。
trait SayHandler implements MessageHandler {
void on(String message, Map payload) {
if (message.startsWith("say")) { (1)
println "I say ${message - 'say'}!"
} else {
super.on(message, payload) (2)
}
}
}
1 | 处理程序特定的前置条件 |
2 | 如果前置条件不满足,则将消息传递给链中的下一个处理程序。 |
那么我们的最终处理程序看起来是这样的:
class Handler implements DefaultHandler, SayHandler, LoggingHandler {}
def h = new Handler()
h.on('foo', [:])
h.on('sayHello', [:])
这意味着
-
消息将首先通过日志处理程序。
-
日志处理程序调用
super
,它将委托给下一个处理程序,即SayHandler
。 -
如果消息以
say
开头,则处理程序会消耗该消息。 -
如果不是,
say
处理程序将委托给链中的下一个处理程序。
这种方法非常强大,因为它允许您编写互不相识的处理程序,并且仍然可以按照您想要的顺序组合它们。例如,如果我们执行代码,它将打印:
Seeing foo with payload [:] Received foo with payload [:] Seeing sayHello with payload [:] I say Hello!
但如果我们将日志处理程序移到链中的第二个位置,输出会不同。
class AlternateHandler implements DefaultHandler, LoggingHandler, SayHandler {}
h = new AlternateHandler()
h.on('foo', [:])
h.on('sayHello', [:])
打印
Seeing foo with payload [:] Received foo with payload [:] I say Hello!
原因是,现在由于 SayHandler
在不调用 super
的情况下消耗了消息,因此不再调用日志处理程序。
特质中 super 的语义
如果一个类实现了多个特质,并且发现对非限定 super
的调用,则:
-
如果该类实现了另一个特质,则调用委托给链中的下一个特质。
-
如果链中没有剩余的特质,
super
指的是实现类(this)的超类。
例如,可以利用这种行为来装饰 final 类。
trait Filtering { (1)
StringBuilder append(String str) { (2)
def subst = str.replace('o','') (3)
super.append(subst) (4)
}
String toString() { super.toString() } (5)
}
def sb = new StringBuilder().withTraits Filtering (6)
sb.append('Groovy')
assert sb.toString() == 'Grvy' (7)
1 | 定义一个名为 Filtering 的特质,假定在运行时应用于 StringBuilder 。 |
2 | 重新定义 append 方法 |
3 | 从字符串中删除所有“o”。 |
4 | 然后委托给 super 。 |
5 | 如果调用 toString ,则委托给 super.toString 。 |
6 | 在 StringBuilder 实例上运行时实现 Filtering 特质 |
7 | 已附加的字符串不再包含字母 o 。 |
在此示例中,当遇到 super.append
时,目标对象没有实现其他特质,因此调用的方法是原始的 append
方法,即 StringBuilder
的方法。同样的技巧也用于 toString
,这样生成的代理对象的字符串表示形式委托给 StringBuilder
实例的 toString
。
高级特性
SAM 类型强制转换
如果一个特质定义了一个抽象方法,它就是 SAM(Single Abstract Method)类型强制转换的候选。例如,想象以下特质:
trait Greeter {
String greet() { "Hello $name" } (1)
abstract String getName() (2)
}
1 | greet 方法不是抽象的,它调用抽象方法 getName 。 |
2 | getName 是一个抽象方法。 |
由于 getName
是 Greeter
特质中 唯一的抽象方法,您可以这样写:
Greeter greeter = { 'Alice' } (1)
1 | 闭包“变成”了 getName 单抽象方法的实现。 |
甚至
void greet(Greeter g) { println g.greet() } (1)
greet { 'Alice' } (2)
1 | greet 方法接受 SAM 类型 Greeter 作为参数。 |
2 | 我们可以直接用闭包调用它。 |
与 Java 8 默认方法的区别
在 Java 8 中,接口可以有方法的默认实现。如果一个类实现了一个接口并且没有为默认方法提供实现,那么将选择接口中的实现。特质的行为方式相同,但有一个主要区别:如果类在其接口列表中声明了特质 并且 它没有提供实现 即使 超类提供了实现,特质中的实现也 总是 被使用。
此功能可用于以非常精确的方式组合行为,以防您想覆盖已实现方法的行为。
为了说明这个概念,让我们从这个简单的例子开始:
import groovy.test.GroovyTestCase
import groovy.transform.CompileStatic
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import org.codehaus.groovy.control.customizers.ImportCustomizer
class SomeTest extends GroovyTestCase {
def config
def shell
void setup() {
config = new CompilerConfiguration()
shell = new GroovyShell(config)
}
void testSomething() {
assert shell.evaluate('1+1') == 2
}
void otherTest() { /* ... */ }
}
在此示例中,我们创建了一个简单的测试用例,它使用两个属性(config 和 shell)并在多个测试方法中使用它们。现在想象您想以另一种不同的编译器配置来测试相同的内容。一个选项是创建 SomeTest
的子类。
class AnotherTest extends SomeTest {
void setup() {
config = new CompilerConfiguration()
config.addCompilationCustomizers( ... )
shell = new GroovyShell(config)
}
}
这可行,但是如果您有多个测试类,并且您希望为所有这些测试类测试新的配置怎么办?那么您将不得不为每个测试类创建一个不同的子类。
class YetAnotherTest extends SomeTest {
void setup() {
config = new CompilerConfiguration()
config.addCompilationCustomizers( ... )
shell = new GroovyShell(config)
}
}
然后您会发现两个测试的 setup
方法是相同的。那么,这个想法就是创建一个特质:
trait MyTestSupport {
void setup() {
config = new CompilerConfiguration()
config.addCompilationCustomizers( new ASTTransformationCustomizer(CompileStatic) )
shell = new GroovyShell(config)
}
}
然后在子类中使用它。
class AnotherTest extends SomeTest implements MyTestSupport {}
class YetAnotherTest extends SomeTest2 implements MyTestSupport {}
...
这将大大减少样板代码,并降低在决定更改 setup 代码时忘记更改的风险。即使 setup
已经在超类中实现,由于测试类在其接口列表中声明了特质,行为将从特质实现中借用!
此功能在您无法访问超类源代码时特别有用。它可以用于模拟方法或强制子类中的特定方法实现。它允许您重构代码,将覆盖的逻辑保留在一个特质中,并通过实现它来继承新的行为。当然,另一种方法是在 所有 您将使用新代码的地方覆盖该方法。
值得注意的是,如果您使用运行时特质,特质中的方法 总是 优先于代理对象的方法。 |
class Person {
String name (1)
}
trait Bob {
String getName() { 'Bob' } (2)
}
def p = new Person(name: 'Alice')
assert p.name == 'Alice' (3)
def p2 = p as Bob (4)
assert p2.name == 'Bob' (5)
1 | Person 类定义了一个 name 属性,这会生成一个 getName 方法。 |
2 | Bob 是一个特质,它将 getName 定义为返回 Bob 。 |
3 | 默认对象将返回 Alice。 |
4 | p2 在运行时将 p 强制转换为 Bob 。 |
5 | getName 返回 Bob,因为 getName 取自 特质。 |
再次强调,不要忘记动态特质强制转换返回一个不同的对象,该对象只实现原始接口和特质。 |
与 Mixin 的区别
与 Groovy 中可用的 Mixin 有几个概念上的区别。请注意,我们谈论的是运行时 Mixin,而不是已弃用并被特质取代的 @Mixin 注解。
首先,特质中定义的方法在字节码中可见。
-
在内部,特质表示为一个接口(不带默认或静态方法)和几个辅助类。
-
这意味着一个实现了特质的对象实际上实现了一个 接口。
-
这些方法在 Java 中可见。
-
它们与类型检查和静态编译兼容。
相反,通过 Mixin 添加的方法仅在运行时可见。
class A { String methodFromA() { 'A' } } (1)
class B { String methodFromB() { 'B' } } (2)
A.metaClass.mixin B (3)
def o = new A()
assert o.methodFromA() == 'A' (4)
assert o.methodFromB() == 'B' (5)
assert o instanceof A (6)
assert !(o instanceof B) (7)
1 | 类 A 定义了 methodFromA 。 |
2 | 类 B 定义了 methodFromB 。 |
3 | 将 B 混合到 A 中 |
4 | 我们可以调用 methodFromA 。 |
5 | 我们也可以调用 methodFromB 。 |
6 | 该对象是 A 的实例。 |
7 | 但它 不是 B 的实例。 |
最后一点实际上非常重要,它说明了 Mixin 优于特质的地方:实例 未 被修改,因此如果您将某个类混入另一个类中,不会生成第三个类,并且响应 A 的方法即使混入后也将继续响应 A。
静态方法、属性和字段
以下说明需要谨慎。静态成员支持仍在进行中且仍处于实验阶段。以下信息仅适用于 4.0.28。 |
在特质中定义静态方法是可能的,但它有许多限制。
-
具有静态方法的特质无法静态编译或进行类型检查。所有静态方法、属性和字段都动态访问(这是 JVM 的限制)。
-
静态方法不会出现在为每个特质生成的接口中。
-
特质被解释为实现类的 模板,这意味着每个实现类将拥有自己的静态方法、属性和字段。因此,特质上声明的静态成员不属于
Trait
,而是属于其实现类。 -
您通常不应混合使用具有相同签名(signature)的静态方法和实例方法。应用特性的常规规则(包括多重继承冲突解决)适用。如果选择的方法是静态的,但某个实现的特性具有实例变体,则会发生编译错误。如果选择的方法是实例变体,则静态变体将被忽略(在这种情况下,行为类似于 Java 接口中的静态方法)。
让我们从一个简单的例子开始
trait TestHelper {
public static boolean CALLED = false (1)
static void init() { (2)
CALLED = true (3)
}
}
class Foo implements TestHelper {}
Foo.init() (4)
assert Foo.TestHelper__CALLED (5)
1 | 静态字段在特性中声明 |
2 | 静态方法也在特性中声明 |
3 | 静态字段在特性**内部**更新 |
4 | 一个静态方法**init**可供实现类使用 |
5 | 静态字段**重新映射**以避免菱形问题 |
像往常一样,不建议使用公共字段。无论如何,如果您想这样做,您必须明白以下代码将失败
Foo.CALLED = true
因为特性本身**没有**定义名为**CALLED**的静态字段。同样,如果您有两个不同的实现类,每个类都会得到一个不同的静态字段
class Bar implements TestHelper {} (1)
class Baz implements TestHelper {} (2)
Bar.init() (3)
assert Bar.TestHelper__CALLED (4)
assert !Baz.TestHelper__CALLED (5)
1 | 类Bar 实现了该特性 |
2 | 类Baz 也实现了该特性 |
3 | init 只在Bar 上调用 |
4 | Bar 上的静态字段CALLED 被更新 |
5 | 但Baz 上的静态字段CALLED 没有被更新,因为它**不同** |
状态继承陷阱
我们已经看到特性是有状态的。特性可以定义字段或属性,但当类实现特性时,它会按特性获取这些字段/属性。所以考虑以下例子
trait IntCouple {
int x = 1
int y = 2
int sum() { x+y }
}
该特性定义了两个属性x
和y
,以及一个sum
方法。现在让我们创建一个实现该特性的类
class BaseElem implements IntCouple {
int f() { sum() }
}
def base = new BaseElem()
assert base.f() == 3
调用f
的结果是3
,因为f
委托给特性中的sum
,而sum
是有状态的。但如果我们这样写会怎么样?
class Elem implements IntCouple {
int x = 3 (1)
int y = 4 (2)
int f() { sum() } (3)
}
def elem = new Elem()
1 | 重写属性x |
2 | 重写属性y |
3 | 从特性调用sum |
如果您调用elem.f()
,预期输出是什么?实际上是
assert elem.f() == 3
原因是sum
方法访问的是特性的**字段**。所以它使用的是特性中定义的x
和y
值。如果您想使用实现类中的值,那么您需要通过使用getter和setter来解除字段的引用,就像最后一个示例中那样
trait IntCouple {
int x = 1
int y = 2
int sum() { getX()+getY() }
}
class Elem implements IntCouple {
int x = 3
int y = 4
int f() { sum() }
}
def elem = new Elem()
assert elem.f() == 7
自类型
特性上的类型约束
有时您会希望编写一个只能应用于某些类型的特性。例如,您可能希望将特性应用于一个扩展了您无法控制的类的类,并且仍然能够调用这些方法。为了说明这一点,让我们从这个例子开始
class CommunicationService {
static void sendMessage(String from, String to, String message) { (1)
println "$from sent [$message] to $to"
}
}
class Device { String id } (2)
trait Communicating {
void sendMessage(Device to, String message) {
CommunicationService.sendMessage(id, to.id, message) (3)
}
}
class MyDevice extends Device implements Communicating {} (4)
def bob = new MyDevice(id:'Bob')
def alice = new MyDevice(id:'Alice')
bob.sendMessage(alice,'secret') (5)
1 | 一个您无法控制的Service 类(在库中,...)定义了一个sendMessage 方法 |
2 | 一个您无法控制的Device 类(在库中,...) |
3 | 为可以调用服务的设备定义一个通信特性 |
4 | 将MyDevice 定义为通信设备 |
5 | 调用特性中的方法,并且解析id |
这里很清楚,Communicating
特性只能应用于Device
。然而,没有明确的契约来表明这一点,因为特性不能扩展类。但是,代码编译并运行得非常好,因为特性方法中的id
将动态解析。问题在于,没有任何东西可以阻止该特性应用于**不是**Device
的任何类。任何具有id
的类都可以工作,而任何不具有id
属性的类都会导致运行时错误。
如果您想启用类型检查或在特性上应用@CompileStatic
,问题会更加复杂:因为特性对自身是Device
一无所知,类型检查器会抱怨找不到id
属性。
一种可能性是在特性中显式添加一个getId
方法,但这并不能解决所有问题。如果一个方法需要this
作为参数,并且实际上要求它是一个Device
呢?
class SecurityService {
static void check(Device d) { if (d.id==null) throw new SecurityException() }
}
如果您希望能够在特性中调用this
,那么您将需要显式地将this
强制转换为Device
。这很快就会因为到处显式强制转换this
而变得难以阅读。
@SelfType注解
为了使此契约显式化,并使类型检查器了解**自身的类型**,Groovy 提供了@SelfType
注解,它将:
-
允许您声明实现此特性的类必须继承或实现的类型
-
如果这些类型约束不满足,则抛出编译时错误
所以在我们之前的例子中,我们可以使用@groovy.transform.SelfType
注解来修复这个特性
@SelfType(Device)
@CompileStatic
trait Communicating {
void sendMessage(Device to, String message) {
SecurityService.check(this)
CommunicationService.sendMessage(id, to.id, message)
}
}
现在,如果您尝试在**不是**设备的类上实现此特性,则会发生编译时错误
class MyDevice implements Communicating {} // forgot to extend Device
错误将是
class 'MyDevice' implements trait 'Communicating' but does not extend self type class 'Device'
总之,自类型是一种强大的方式,可以在不直接在特性中声明契约或不必到处使用强制转换的情况下声明特性上的约束,从而保持关注点分离的紧密性。
与 Sealed 注解的区别 (孵化中)
@Sealed
和@SelfType
都限制使用特性的类,但方式正交。考虑以下示例
interface HasHeight { double getHeight() }
interface HasArea { double getArea() }
@SelfType([HasHeight, HasArea]) (1)
@Sealed(permittedSubclasses=[UnitCylinder,UnitCube]) (2)
trait HasVolume {
double getVolume() { height * area }
}
final class UnitCube implements HasVolume, HasHeight, HasArea {
// for the purposes of this example: h=1, w=1, l=1
double height = 1d
double area = 1d
}
final class UnitCylinder implements HasVolume, HasHeight, HasArea {
// for the purposes of this example: h=1, diameter=1
// radius=diameter/2, area=PI * r^2
double height = 1d
double area = Math.PI * 0.5d**2
}
assert new UnitCube().volume == 1d
assert new UnitCylinder().volume == 0.7853981633974483d
1 | 所有HasVolume 特性的使用都必须实现或扩展HasHeight 和HasArea |
2 | 只有UnitCube 或UnitCylinder 可以使用该特性 |
对于单个类实现特性的退化情况,例如
final class Foo implements FooTrait {}
那么,要么
@SelfType(Foo)
trait FooTrait {}
或者
@Sealed(permittedSubclasses='Foo') (1)
trait FooTrait {}
1 | 或者如果Foo 和FooTrait 在同一个源文件中,则只使用@Sealed |
可以表达这种约束。通常,前者更受欢迎。
限制
与 AST 转换的兼容性
特性与 AST 转换不正式兼容。其中一些,如@CompileStatic ,将应用于特性本身(而不是实现类),而另一些将应用于实现类和特性。绝对不能保证 AST 转换在特性上的运行方式与在常规类上相同,因此请自行承担风险使用! |
前缀和后缀操作
在特性内部,如果前缀和后缀操作更新特性的字段,则不允许使用它们
trait Counting {
int x
void inc() {
x++ (1)
}
void dec() {
--x (2)
}
}
class Counter implements Counting {}
def c = new Counter()
c.inc()
1 | x 在特性中定义,不允许后缀递增 |
2 | x 在特性中定义,不允许前缀递减 |
一种解决方法是使用+=
运算符代替。
1.4.6. 记录类 (孵化中)
记录类,简称**记录**,是一种特殊类型的类,用于建模普通数据聚合。它们提供了紧凑的语法,比普通类更少的繁琐。Groovy 已经有像@Immutable
和@Canonical
这样的 AST 转换,它们已经大大减少了繁琐,但记录已在 Java 中引入,Groovy 中的记录类旨在与 Java 记录类对齐。
例如,假设我们要创建一个表示电子邮件消息的Message
记录。为了本例的目的,让我们将此类消息简化为仅包含**发件人**电子邮件地址、**收件人**电子邮件地址和消息**正文**。我们可以按如下方式定义此类记录
record Message(String from, String to, String body) { }
我们将像使用普通类一样使用记录类,如下所示
def msg = new Message('me@myhost.com', 'you@yourhost.net', 'Hello!')
assert msg.toString() == 'Message[from=me@myhost.com, to=you@yourhost.net, body=Hello!]'
减少的繁琐代码省去了我们定义显式字段、getter 以及toString
、equals
和hashCode
方法的麻烦。实际上,它是以下大致等效代码的简写
final class Message extends Record {
private final String from
private final String to
private final String body
private static final long serialVersionUID = 0
/* constructor(s) */
final String toString() { /*...*/ }
final boolean equals(Object other) { /*...*/ }
final int hashCode() { /*...*/ }
String from() { from }
// other getters ...
}
请注意记录getter的特殊命名约定。它们的名称与字段名称相同(而不是通常常见的JavaBean约定,即大写并带有"get"前缀)。对于记录,通常使用术语**组件**来指代记录的字段或属性。因此,我们的Message
记录具有from
、to
和body
组件。
像在Java中一样,您可以通过编写自己的方法来覆盖通常隐式提供的方法
record Point3D(int x, int y, int z) {
String toString() {
"Point3D[coords=$x,$y,$z]"
}
}
assert new Point3D(10, 20, 30).toString() == 'Point3D[coords=10,20,30]'
您还可以以正常方式将泛型与记录一起使用。例如,考虑以下Coord
记录定义
record Coord<T extends Number>(T v1, T v2){
double distFromOrigin() { Math.sqrt(v1()**2 + v2()**2 as double) }
}
它可以按如下方式使用
def r1 = new Coord<Integer>(3, 4)
assert r1.distFromOrigin() == 5
def r2 = new Coord<Double>(6d, 2.5d)
assert r2.distFromOrigin() == 6.5d
特殊记录功能
紧凑构造函数
记录有一个隐式构造函数。这可以通过提供您自己的构造函数来以正常方式重写 - 如果这样做,您需要确保设置所有字段。但是,为了简洁,可以使用紧凑构造函数语法,其中省略了正常构造函数的参数声明部分。对于这种特殊情况,正常的隐式构造函数仍然提供,但通过紧凑构造函数定义中提供的语句进行增强
public record Warning(String message) {
public Warning {
Objects.requireNonNull(message)
message = message.toUpperCase()
}
}
def w = new Warning('Help')
assert w.message() == 'HELP'
Groovy 增强功能
参数默认值
Groovy 支持构造函数参数的默认值。此功能也适用于记录,如以下记录定义所示,其中y
和color
具有默认值
record ColoredPoint(int x, int y = 0, String color = 'white') {}
当省略参数(从右侧省略一个或多个参数)时,它们将被其默认值替换,如下例所示
assert new ColoredPoint(5, 5, 'black').toString() == 'ColoredPoint[x=5, y=5, color=black]'
assert new ColoredPoint(5, 5).toString() == 'ColoredPoint[x=5, y=5, color=white]'
assert new ColoredPoint(5).toString() == 'ColoredPoint[x=5, y=0, color=white]'
此处理遵循 Groovy 构造函数默认参数的常规约定,本质上是自动提供具有以下签名的构造函数
ColoredPoint(int, int, String)
ColoredPoint(int, int)
ColoredPoint(int)
也可以使用命名参数(默认值也适用于此)
assert new ColoredPoint(x: 5).toString() == 'ColoredPoint[x=5, y=0, color=white]'
assert new ColoredPoint(x: 0, y: 5).toString() == 'ColoredPoint[x=0, y=5, color=white]'
您可以禁用默认参数处理,如下所示
@TupleConstructor(defaultsMode=DefaultsMode.OFF)
record ColoredPoint2(int x, int y, String color) {}
assert new ColoredPoint2(4, 5, 'red').toString() == 'ColoredPoint2[x=4, y=5, color=red]'
这将生成一个与 Java 默认行为相同的单个构造函数。在这种情况下,如果您省略参数,将会出现错误。
您可以强制所有属性都具有默认值,如下所示
@TupleConstructor(defaultsMode=DefaultsMode.ON)
record ColoredPoint3(int x, int y = 0, String color = 'white') {}
assert new ColoredPoint3(y: 5).toString() == 'ColoredPoint3[x=0, y=5, color=white]'
任何没有显式初始值的属性/字段都将获得其参数类型的默认值(null,或基本类型的零/false)。
声明式toString
定制
根据 Java 的规定,您可以通过编写自己的方法来自定义记录的toString
方法。如果您更喜欢声明式风格,则可以改为使用 Groovy 的@ToString
转换来覆盖默认记录的toString
。例如,您可以将三维点记录定义如下
package threed
import groovy.transform.ToString
@ToString(ignoreNulls=true, cache=true, includeNames=true,
leftDelimiter='[', rightDelimiter=']', nameValueSeparator='=')
record Point(Integer x, Integer y, Integer z=null) { }
assert new Point(10, 20).toString() == 'threed.Point[x=10, y=20]'
我们通过包含包名(记录默认排除)和缓存toString
值来自定义toString
,因为对于此不可变记录,它不会改变。我们还忽略了空值(我们定义中z
的默认值)。
我们可以为二维点设置类似的定义
package twod
import groovy.transform.ToString
@ToString(ignoreNulls=true, cache=true, includeNames=true,
leftDelimiter='[', rightDelimiter=']', nameValueSeparator='=')
record Point(Integer x, Integer y) { }
assert new Point(10, 20).toString() == 'twod.Point[x=10, y=20]'
我们可以在这里看到,如果没有包名,它将与我们之前的示例具有相同的 toString。
获取记录组件值的列表
我们可以将记录的组件值作为列表获取,如下所示
record Point(int x, int y, String color) { }
def p = new Point(100, 200, 'green')
def (x, y, c) = p.toList()
assert x == 100
assert y == 200
assert c == 'green'
您可以使用@RecordOptions(toList=false)
来禁用此功能。
获取记录组件值的映射
我们可以将记录的组件值作为映射获取,如下所示
record Point(int x, int y, String color) { }
def p = new Point(100, 200, 'green')
assert p.toMap() == [x: 100, y: 200, color: 'green']
您可以使用@RecordOptions(toMap=false)
来禁用此功能。
获取记录中的组件数量
我们可以像这样获取记录中的组件数量
record Point(int x, int y, String color) { }
def p = new Point(100, 200, 'green')
assert p.size() == 3
您可以使用@RecordOptions(size=false)
来禁用此功能。
获取记录中的第 n个组件
我们可以使用 Groovy 的普通位置索引来获取记录中的特定组件,如下所示
record Point(int x, int y, String color) { }
def p = new Point(100, 200, 'green')
assert p[1] == 200
您可以使用@RecordOptions(getAt=false)
来禁用此功能。
可选的 Groovy 功能
复制
复制记录并更改某些组件可能很有用。这可以使用一个可选的copyWith
方法来完成,该方法接受命名参数。记录组件从提供的参数中设置。对于未提及的组件,使用原始记录组件的(浅)副本。以下是如何为Fruit
记录使用copyWith
@RecordOptions(copyWith=true)
record Fruit(String name, double price) {}
def apple = new Fruit('Apple', 11.6)
assert 'Apple' == apple.name()
assert 11.6 == apple.price()
def orange = apple.copyWith(name: 'Orange')
assert orange.toString() == 'Fruit[name=Orange, price=11.6]'
可以通过将RecordOptions#copyWith
注解属性设置为false
来禁用copyWith
功能。
深度不可变性
与 Java 一样,记录默认提供浅层不可变性。Groovy 的@Immutable
转换对一系列可变数据类型执行防御性复制。记录可以利用这种防御性复制来获得深度不可变性,如下所示
@ImmutableProperties
record Shopping(List items) {}
def items = ['bread', 'milk']
def shop = new Shopping(items)
items << 'chocolate'
assert shop.items() == ['bread', 'milk']
这些示例阐明了 Groovy 记录功能提供三个便利级别的原则
-
使用
record
关键字以实现最大简洁性 -
支持使用声明性注解进行低仪式化定制
-
需要完全控制时允许正常的 методы 实现
将记录的组件作为类型化元组获取
您可以将记录的组件作为类型化元组获取
import groovy.transform.*
@RecordOptions(components=true)
record Point(int x, int y, String color) { }
@CompileStatic
def method() {
def p1 = new Point(100, 200, 'green')
def (int x1, int y1, String c1) = p1.components()
assert x1 == 100
assert y1 == 200
assert c1 == 'green'
def p2 = new Point(10, 20, 'blue')
def (x2, y2, c2) = p2.components()
assert x2 * 10 == 100
assert y2 ** 2 == 400
assert c2.toUpperCase() == 'BLUE'
def p3 = new Point(1, 2, 'red')
assert p3.components() instanceof Tuple3
}
method()
Groovy 的TupleN
类数量有限。如果您的记录中有大量组件,您可能无法使用此功能。
与 Java 的其他差异
Groovy 支持创建**类记录**以及原生记录。类记录不扩展 Java 的Record
类,此类不会被 Java 视为记录,但否则将具有相似的属性。
@RecordOptions
注解(@RecordType
的一部分)支持mode
注解属性,该属性可以取以下三个值之一(AUTO
是默认值)
- NATIVE
-
生成与 Java 相似的类。在 JDK16 之前的 JDK 上编译时会产生错误。
- EMULATE
-
为所有 JDK 版本生成类记录。
- AUTO
-
为 JDK16+ 生成原生记录,否则模拟记录。
您是否使用record
关键字或@RecordType
注解与模式无关。
1.4.7. 密封层次结构 (孵化中)
密封类、接口和特性限制了哪些子类可以扩展/实现它们。在密封类之前,类层次结构设计者有两个主要选择
-
将类声明为 final 以禁止扩展。
-
将类声明为 public 且非 final 以允许任何人扩展。
密封类在这些全有或全无的选择之间提供了一种中间地带。
密封类也比以前尝试实现中间地带的其他技巧更灵活。例如,对于类层次结构,protected 和 package-private 等访问修饰符在某种程度上限制了继承层次结构,但通常以牺牲这些层次结构的灵活使用为代价。
密封层次结构在已知的类、接口和特性层次结构中提供完全继承,但在层次结构之外禁用或仅提供受控继承。
例如,假设我们要创建一个只包含圆形和正方形的形状层次结构。我们还希望一个形状接口能够引用我们层次结构中的实例。我们可以按如下方式创建层次结构
sealed interface ShapeI permits Circle,Square { }
final class Circle implements ShapeI { }
final class Square implements ShapeI { }
Groovy 还支持另一种注解语法。我们认为关键字风格更好,但如果您的编辑器尚不支持 Groovy 4,您可能会选择注解风格。
@Sealed(permittedSubclasses=[Circle,Square]) interface ShapeI { }
final class Circle implements ShapeI { }
final class Square implements ShapeI { }
我们可以有一个类型为ShapeI
的引用,由于permits
子句,它可以指向Circle
或Square
,并且由于我们的类是final
的,我们知道将来不会向我们的层次结构添加额外的类。至少在不更改permits
子句并重新编译的情况下不会。
一般来说,我们可能希望将类层次结构中的某些部分立即锁定,就像我们在这里所做的那样,我们将子类标记为final
,但有时我们可能希望允许进一步的受控继承。
sealed class Shape permits Circle,Polygon,Rectangle { }
final class Circle extends Shape { }
class Polygon extends Shape { }
non-sealed class RegularPolygon extends Polygon { }
final class Hexagon extends Polygon { }
sealed class Rectangle extends Shape permits Square{ }
final class Square extends Rectangle { }
<点击查看替代注解语法>
@Sealed(permittedSubclasses=[Circle,Polygon,Rectangle]) class Shape { }
final class Circle extends Shape { }
class Polygon extends Shape { }
@NonSealed class RegularPolygon extends Polygon { }
final class Hexagon extends Polygon { }
@Sealed(permittedSubclasses=Square) class Rectangle extends Shape { }
final class Square extends Rectangle { }
在此示例中,我们允许Shape
的子类是Circle
、Polygon
和Rectangle
。Circle
是final
的,因此层次结构的该部分无法扩展。Polygon
是隐式非密封的,而RegularPolygon
则被显式标记为non-sealed
。这意味着我们的层次结构可以通过子类化进行进一步扩展,如Polygon → RegularPolygon
和RegularPolygon → Hexagon
所示。Rectangle
本身是密封的,这意味着层次结构的该部分可以扩展,但只能以受控方式(只允许Square
)。
密封类对于创建需要包含实例特定数据的枚举样式的相关类很有用。例如,我们可能有一个以下枚举
enum Weather { Rainy, Cloudy, Sunny }
def forecast = [Weather.Rainy, Weather.Sunny, Weather.Cloudy]
assert forecast.toString() == '[Rainy, Sunny, Cloudy]'
但我们现在还希望向天气预报添加天气特定实例数据。我们可以按如下方式更改我们的抽象
sealed abstract class Weather { }
@Immutable(includeNames=true) class Rainy extends Weather { Integer expectedRainfall }
@Immutable(includeNames=true) class Sunny extends Weather { Integer expectedTemp }
@Immutable(includeNames=true) class Cloudy extends Weather { Integer expectedUV }
def forecast = [new Rainy(12), new Sunny(35), new Cloudy(6)]
assert forecast.toString() == '[Rainy(expectedRainfall:12), Sunny(expectedTemp:35), Cloudy(expectedUV:6)]'
密封层次结构在指定代数或抽象数据类型(ADT)时也很有用,如下例所示
import groovy.transform.*
sealed interface Tree<T> {}
@Singleton final class Empty implements Tree {
String toString() { 'Empty' }
}
@Canonical final class Node<T> implements Tree<T> {
T value
Tree<T> left, right
}
Tree<Integer> tree = new Node<>(42, new Node<>(0, Empty.instance, Empty.instance), Empty.instance)
assert tree.toString() == 'Node(42, Node(0, Empty, Empty), Empty)'
密封层次结构与记录配合良好,如下例所示
sealed interface Expr {}
record ConstExpr(int i) implements Expr {}
record PlusExpr(Expr e1, Expr e2) implements Expr {}
record MinusExpr(Expr e1, Expr e2) implements Expr {}
record NegExpr(Expr e) implements Expr {}
def threePlusNegOne = new PlusExpr(new ConstExpr(3), new NegExpr(new ConstExpr(1)))
assert threePlusNegOne.toString() == 'PlusExpr[e1=ConstExpr[i=3], e2=NegExpr[e=ConstExpr[i=1]]]'
与 Java 的差异
-
Java 对密封类的子类没有提供默认修饰符,并且要求指定
final
、sealed
或non-sealed
之一。Groovy 默认为**非密封**,但如果您愿意,仍然可以使用non-sealed/@NonSealed
。我们预计样式检查工具 CodeNarc 最终会有一个规则来查找non-sealed
的存在,因此希望采用更严格样式的开发人员将能够使用 CodeNarc 和该规则。 -
目前,Groovy 不会检查
permittedSubclasses
中提及的所有类是否在编译时可用并与基础密封类一起编译。这在 Groovy 的未来版本中可能会改变。
Groovy 支持将类注解为密封以及“原生”密封类。
@SealedOptions
注解支持一个mode
注解属性,它可以取以下三个值之一(默认值为AUTO
)
- NATIVE
-
生成一个与 Java 类似类。在 JDK17 之前的 JDK 上编译时会产生错误。
- EMULATE
-
表示使用
@Sealed
注解密封类。此机制适用于 JDK8+ 的 Groovy 编译器,但 Java 编译器不识别。 - AUTO
-
对于 JDK17+ 生成原生记录,否则模拟记录。
您是否使用sealed
关键字或@Sealed
注解与模式无关。
1.5. 闭包
本章介绍 Groovy 闭包。Groovy 中的闭包是一个开放的、匿名的代码块,可以接受参数、返回值并赋值给变量。闭包可以引用其周围作用域中声明的变量。与闭包的形式定义相反,Groovy 语言中的Closure
也可以包含在其周围作用域之外定义的自由变量。虽然这打破了闭包的形式概念,但它提供了本章中描述的各种优势。
1.5.1. 语法
定义闭包
闭包定义遵循此语法
{ [closureParameters -> ] statements }
其中[closureParameters->]
是可选的逗号分隔参数列表,而statements是0个或更多Groovy语句。参数看起来类似于方法参数列表,这些参数可以是类型化或非类型化的。
当指定参数列表时,需要->
字符,用于将参数与闭包体分开。**语句**部分由0个、1个或多个Groovy语句组成。
一些有效闭包定义的示例
{ item++ } (1)
{ -> item++ } (2)
{ println it } (3)
{ it -> println it } (4)
{ name -> println name } (5)
{ String x, int y -> (6)
println "hey ${x} the value is ${y}"
}
{ reader -> (7)
def line = reader.readLine()
line.trim()
}
1 | 一个引用名为item 变量的闭包 |
2 | 可以通过添加箭头(-> )来显式地将闭包参数与代码分开 |
3 | 一个使用隐式参数(it )的闭包 |
4 | it 作为显式参数的替代版本 |
5 | 在这种情况下,最好使用显式的参数名称 |
6 | 一个接受两个类型化参数的闭包 |
7 | 一个闭包可以包含多个语句 |
闭包作为对象
闭包是groovy.lang.Closure
类的一个实例,尽管它是一个代码块,但可以像其他变量一样赋值给变量或字段
def listener = { e -> println "Clicked on $e.source" } (1)
assert listener instanceof Closure
Closure callback = { println 'Done!' } (2)
Closure<Boolean> isTextFile = {
File it -> it.name.endsWith('.txt') (3)
}
1 | 您可以将闭包分配给变量,它是groovy.lang.Closure 的一个实例 |
2 | 如果未使用def 或var ,请使用groovy.lang.Closure 作为类型 |
3 | 可选地,您可以通过使用groovy.lang.Closure 的泛型类型来指定闭包的返回类型 |
调用闭包
闭包,作为一个匿名代码块,可以像任何其他方法一样被调用。如果您定义了一个不带参数的闭包,如下所示
def code = { 123 }
那么闭包内部的代码只会在您**调用**闭包时执行,这可以通过将变量当作常规方法来完成
assert code() == 123
或者,您可以明确地使用call
方法
assert code.call() == 123
如果闭包接受参数,原理是相同的
def isOdd = { int i -> i%2 != 0 } (1)
assert isOdd(3) == true (2)
assert isOdd.call(2) == false (3)
def isEven = { it%2 == 0 } (4)
assert isEven(3) == false (5)
assert isEven.call(2) == true (6)
1 | 定义一个接受int 作为参数的闭包 |
2 | 它可以直接调用 |
3 | 或使用call 方法 |
4 | 隐式参数(it )的闭包也是如此 |
5 | 可以直接使用(arg) 调用 |
6 | 或使用call |
与方法不同,闭包在调用时**总是**返回一个值。下一节讨论如何声明闭包参数,何时使用它们以及什么是隐式“it”参数。
1.5.2. 参数
普通参数
闭包的参数遵循与常规方法参数相同的原则
-
可选类型
-
一个名字
-
一个可选的默认值
参数用逗号分隔
def closureWithOneArg = { str -> str.toUpperCase() }
assert closureWithOneArg('groovy') == 'GROOVY'
def closureWithOneArgAndExplicitType = { String str -> str.toUpperCase() }
assert closureWithOneArgAndExplicitType('groovy') == 'GROOVY'
def closureWithTwoArgs = { a,b -> a+b }
assert closureWithTwoArgs(1,2) == 3
def closureWithTwoArgsAndExplicitTypes = { int a, int b -> a+b }
assert closureWithTwoArgsAndExplicitTypes(1,2) == 3
def closureWithTwoArgsAndOptionalTypes = { a, int b -> a+b }
assert closureWithTwoArgsAndOptionalTypes(1,2) == 3
def closureWithTwoArgAndDefaultValue = { int a, int b=2 -> a+b }
assert closureWithTwoArgAndDefaultValue(1) == 3
隐式参数
当闭包不显式定义参数列表(使用->
)时,闭包**总是**定义一个名为it
的隐式参数。这意味着这段代码
def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
严格等同于这个
def greeting = { it -> "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
如果您想声明一个不接受任何参数且必须限制为无参数调用的闭包,那么您**必须**使用显式空参数列表声明它
def magicNumber = { -> 42 }
// this call will fail because the closure doesn't accept any argument
magicNumber(11)
可变参数 (Varargs)
闭包可以像任何其他方法一样声明可变参数。可变参数方法是指如果最后一个参数是可变长度(或数组)的,则可以接受可变数量参数的方法,如下一个示例所示
def concat1 = { String... args -> args.join('') } (1)
assert concat1('abc','def') == 'abcdef' (2)
def concat2 = { String[] args -> args.join('') } (3)
assert concat2('abc', 'def') == 'abcdef'
def multiConcat = { int n, String... args -> (4)
args.join('')*n
}
assert multiConcat(2, 'abc','def') == 'abcdefabcdef'
1 | 一个闭包,接受可变数量的字符串作为第一个参数 |
2 | 它可以接受任意数量的参数调用,**无需**显式地将它们包装成数组 |
3 | 如果将args 参数声明为数组,则直接获得相同的行为 |
4 | 只要**最后一个**参数是数组或显式可变参数类型 |
1.5.3. 委托策略
Groovy 闭包 vs Lambda 表达式
Groovy 将闭包定义为Closure
类的实例。这使得它与Java 8 中的 lambda 表达式非常不同。委托是 Groovy 闭包中的一个关键概念,在 lambda 中没有等价物。**更改闭包的委托**或**更改闭包的委托策略**使得在 Groovy 中设计漂亮的领域特定语言 (DSL) 成为可能。
Owner、Delegate 和 this
要理解委托的概念,我们首先必须解释闭包中this
的含义。闭包实际上定义了 3 件不同的事情
-
this
对应于定义闭包的**封闭类** -
owner
对应于定义闭包的**封闭对象**,可以是类或闭包 -
delegate
对应于第三方对象,当消息的接收者未定义时,方法调用或属性将在该对象上解析
this 的含义
在闭包中,调用getThisObject
将返回定义闭包的封闭类。它等同于使用显式this
class Enclosing {
void run() {
def whatIsThisObject = { getThisObject() } (1)
assert whatIsThisObject() == this (2)
def whatIsThis = { this } (3)
assert whatIsThis() == this (4)
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { this } (5)
}
void run() {
def inner = new Inner()
assert inner.cl() == inner (6)
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { this } (7)
cl()
}
assert nestedClosures() == this (8)
}
}
1 | 一个闭包在Enclosing 类中定义,并返回getThisObject |
2 | 调用闭包将返回定义闭包的Enclosing 实例 |
3 | 通常,您只想使用快捷方式this 表示法 |
4 | 它返回**完全**相同的对象 |
5 | 如果闭包定义在内部类中 |
6 | 闭包中的this **将**返回内部类,而不是顶层类 |
7 | 在嵌套闭包的情况下,例如这里在nestedClosures 作用域内定义的cl |
8 | 那么this 对应于最近的外层类,而不是封闭闭包! |
当然,也可以通过这种方式调用封闭类中的方法
class Person {
String name
int age
String toString() { "$name is $age years old" }
String dump() {
def cl = {
String msg = this.toString() (1)
println msg
msg
}
cl()
}
}
def p = new Person(name:'Janice', age:74)
assert p.dump() == 'Janice is 74 years old'
1 | 闭包在this 上调用toString ,这实际上会调用封闭对象上的toString 方法,即Person 实例 |
闭包的拥有者
闭包的所有者与闭包中this
的定义非常相似,但有一个细微的差别:它将返回直接的封闭对象,无论是闭包还是类
class Enclosing {
void run() {
def whatIsOwnerMethod = { getOwner() } (1)
assert whatIsOwnerMethod() == this (2)
def whatIsOwner = { owner } (3)
assert whatIsOwner() == this (4)
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { owner } (5)
}
void run() {
def inner = new Inner()
assert inner.cl() == inner (6)
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { owner } (7)
cl()
}
assert nestedClosures() == nestedClosures (8)
}
}
1 | 一个闭包在Enclosing 类中定义,并返回getOwner |
2 | 调用闭包将返回定义闭包的Enclosing 实例 |
3 | 通常,您只想使用快捷方式owner 表示法 |
4 | 它返回**完全**相同的对象 |
5 | 如果闭包定义在内部类中 |
6 | 闭包中的owner **将**返回内部类,而不是顶层类 |
7 | 但在嵌套闭包的情况下,例如这里在nestedClosures 作用域内定义的cl |
8 | 那么owner 对应于封闭闭包,因此与this 是不同的对象! |
闭包的委托
闭包的委托可以通过使用delegate
属性或调用getDelegate
方法来访问。它是 Groovy 中构建领域特定语言的强大概念。虽然this和owner引用闭包的词法作用域,但委托是闭包将使用的用户定义对象。默认情况下,委托设置为owner
class Enclosing {
void run() {
def cl = { getDelegate() } (1)
def cl2 = { delegate } (2)
assert cl() == cl2() (3)
assert cl() == this (4)
def enclosed = {
{ -> delegate }.call() (5)
}
assert enclosed() == enclosed (6)
}
}
1 | 您可以调用getDelegate 方法获取闭包的委托 |
2 | 或使用delegate 属性 |
3 | 两者返回相同的对象 |
4 | 它是封闭类或闭包 |
5 | 特别是在嵌套闭包的情况下 |
6 | delegate 将对应于owner |
闭包的委托可以更改为**任何对象**。让我们通过创建两个不相互继承但都定义名为name
的属性的类来说明这一点
class Person {
String name
}
class Thing {
String name
}
def p = new Person(name: 'Norman')
def t = new Thing(name: 'Teapot')
然后我们定义一个闭包,它从委托中获取name
属性
def upperCasedName = { delegate.name.toUpperCase() }
然后通过更改闭包的委托,您可以看到目标对象将发生变化
upperCasedName.delegate = p
assert upperCasedName() == 'NORMAN'
upperCasedName.delegate = t
assert upperCasedName() == 'TEAPOT'
此时,行为与在闭包的词法作用域中定义了一个target
变量没有什么不同
def target = p
def upperCasedNameUsingVar = { target.name.toUpperCase() }
assert upperCasedNameUsingVar() == 'NORMAN'
然而,存在主要区别
-
在最后一个例子中,*target*是一个在闭包内部引用的局部变量
-
委托可以透明地使用,也就是说,在方法调用前无需加上
delegate.
前缀,如下一段所述。
委托策略
无论何时,在闭包中,如果访问属性时没有显式设置接收对象,则会涉及委托策略
class Person {
String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() } (1)
cl.delegate = p (2)
assert cl() == 'IGOR' (3)
1 | name 没有引用闭包词法作用域中的变量 |
2 | 我们可以将闭包的委托更改为Person 实例 |
3 | 并且方法调用将成功 |
这段代码之所以有效,是因为name
属性将在delegate
对象上透明地解析!这是一种在闭包内部解析属性或方法调用的非常强大的方式。无需设置显式的delegate.
接收者:调用将发生,因为闭包的默认委托策略使其如此。闭包实际上定义了您可以选择的多种解析策略
-
Closure.OWNER_FIRST
是**默认策略**。如果**所有者**上存在属性/方法,则将在所有者上调用。如果不存在,则使用**委托**。 -
Closure.DELEGATE_FIRST
反转逻辑:**委托**优先使用,然后是**所有者** -
Closure.OWNER_ONLY
将只在所有者上解析属性/方法查找:委托将被忽略。 -
Closure.DELEGATE_ONLY
将只在委托上解析属性/方法查找:所有者将被忽略。 -
Closure.TO_SELF
可供需要高级元编程技术并希望实现自定义解析策略的开发人员使用:解析将不会在所有者或委托上进行,而只在闭包类本身上进行。只有当您实现自己的Closure
子类时,使用此方法才有意义。
让我们用这段代码来说明默认的“所有者优先”策略
class Person {
String name
def pretty = { "My name is $name" } (1)
String toString() {
pretty()
}
}
class Thing {
String name (2)
}
def p = new Person(name: 'Sarah')
def t = new Thing(name: 'Teapot')
assert p.toString() == 'My name is Sarah' (3)
p.pretty.delegate = t (4)
assert p.toString() == 'My name is Sarah' (5)
1 | 为了说明,我们定义一个引用“name”的闭包成员 |
2 | Person 和Thing 类都定义了name 属性 |
3 | 使用默认策略,name 属性首先在所有者上解析 |
4 | 所以如果我们把delegate 改为t ,它是Thing 的一个实例 |
5 | 结果没有变化:name 首先在闭包的owner 上解析 |
然而,可以改变闭包的解析策略
p.pretty.resolveStrategy = Closure.DELEGATE_FIRST
assert p.toString() == 'My name is Teapot'
通过更改resolveStrategy
,我们正在修改 Groovy 解析“隐式 this”引用的方式:在这种情况下,name
将首先在委托中查找,如果找不到,则在所有者中查找。由于name
在委托(Thing
的一个实例)中定义,因此使用此值。
“委托优先”与“仅委托”或“所有者优先”与“仅所有者”之间的区别可以通过以下情况来说明:如果其中一个委托(或所有者)**没有**这样的方法或属性
class Person {
String name
int age
def fetchAge = { age }
}
class Thing {
String name
}
def p = new Person(name:'Jessica', age:42)
def t = new Thing(name:'Printer')
def cl = p.fetchAge
cl.delegate = p
assert cl() == 42 (1)
cl.delegate = t
assert cl() == 42 (1)
cl.resolveStrategy = Closure.DELEGATE_ONLY
cl.delegate = p
assert cl() == 42 (2)
cl.delegate = t
try {
cl() (3)
assert false
} catch (MissingPropertyException ex) {
// "age" is not defined on the delegate
}
1 | 对于“所有者优先”,委托是什么并不重要 |
2 | 对于“仅委托”,将p 作为委托成功 |
3 | 对于“仅委托”,将t 作为委托失败 |
在此示例中,我们定义了两个类,它们都具有name
属性,但只有Person
类声明了age
。Person
类还声明了一个引用age
的闭包。我们可以将默认的解析策略从“所有者优先”更改为“仅委托”。由于闭包的所有者是Person
类,因此我们可以检查,如果委托是Person
的实例,则调用闭包成功;但如果使用Thing
的实例作为委托调用闭包,则会抛出groovy.lang.MissingPropertyException
异常。尽管闭包定义在Person
类内部,但所有者未被使用。
有关如何使用此功能开发 DSL 的全面解释,请参阅手册的专门部分。 |
元编程存在时的委托策略
在描述“所有者优先”委托策略时,我们谈到如果属性/方法“存在”于所有者中,则使用所有者的属性/方法,否则使用委托的相应属性/方法。对于“委托优先”则相反。与其使用“存在”一词,不如使用“处理”一词更准确。这意味着对于“所有者优先”,如果属性/方法存在于所有者中,或者所有者具有propertyMissing/methodMissing钩子,则所有者将处理成员访问。
我们可以通过稍微修改之前的示例来看到实际效果
class Person {
String name
int age
def fetchAge = { age }
}
class Thing {
String name
def propertyMissing(String name) { -1 }
}
def p = new Person(name:'Jessica', age:42)
def t = new Thing(name:'Printer')
def cl = p.fetchAge
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl.delegate = p
assert cl() == 42
cl.delegate = t
assert cl() == -1
在这个例子中,尽管我们的Thing
类实例(cl
最后一次使用的委托)没有age
属性,但它通过其propertyMissing
钩子处理了缺失的属性,这意味着age
将是-1
。
1.5.4. GString 中的闭包
请看以下代码
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
代码的行为符合您的预期,但如果您添加
x = 2
assert gs == 'x = 2'
您会发现断言失败了!这有两个原因
-
GString 只惰性地评估值的
toString
表示 -
GString 中的语法
${x}
**不**代表闭包,而是**表达式**$x
,在 GString 创建时进行评估。
在我们的示例中,GString
是用引用x
的表达式创建的。当GString
创建时,x
的**值**是1,因此GString
创建时值为1。当断言触发时,GString
被评估,1使用toString
转换为String
。当我们把x
改为2时,我们确实改变了x
的值,但它是一个不同的对象,GString
仍然引用旧的对象。
只有当 GString 引用的值发生变化时,它的toString 表示才会改变。如果引用本身改变,什么都不会发生。 |
如果您需要在 GString 中使用真正的闭包,例如强制变量的惰性求值,您需要使用替代语法${→ x}
,如下面的修正示例所示
def x = 1
def gs = "x = ${-> x}"
assert gs == 'x = 1'
x = 2
assert gs == 'x = 2'
让我们用这段代码说明它与突变有何不同
class Person {
String name
String toString() { name } (1)
}
def sam = new Person(name:'Sam') (2)
def lucy = new Person(name:'Lucy') (3)
def p = sam (4)
def gs = "Name: ${p}" (5)
assert gs == 'Name: Sam' (6)
p = lucy (7)
assert gs == 'Name: Sam' (8)
sam.name = 'Lucy' (9)
assert gs == 'Name: Lucy' (10)
1 | Person 类有一个返回name 属性的toString 方法 |
2 | 我们创建一个名为 *Sam* 的第一个Person |
3 | 我们创建另一个名为 *Lucy* 的Person |
4 | 变量p 被设置为Sam |
5 | 并且创建了一个闭包,引用p 的值,即 *Sam* |
6 | 所以当我们评估这个字符串时,它返回 *Sam* |
7 | 如果我们把p 改为 *Lucy* |
8 | 字符串仍然评估为 *Sam*,因为它是创建GString 时p 的**值** |
9 | 所以如果我们改变 *Sam* 的名字为 *Lucy* |
10 | 这次GString 被正确地修改了 |
因此,如果您不想依赖可变对象或包装对象,您**必须**在GString
中使用闭包,并通过显式声明一个空参数列表来实现
class Person {
String name
String toString() { name }
}
def sam = new Person(name:'Sam')
def lucy = new Person(name:'Lucy')
def p = sam
// Create a GString with lazy evaluation of "p"
def gs = "Name: ${-> p}"
assert gs == 'Name: Sam'
p = lucy
assert gs == 'Name: Lucy'
1.5.5. 闭包强制转换
闭包可以转换为接口或单抽象方法类型。有关完整说明,请参阅手册的此部分。
1.5.6. 函数式编程
闭包,就像Java 8 中的 lambda 表达式一样,是 Groovy 函数式编程范式的核心。一些函数式编程操作可以直接在Closure
类上使用,如下节所示。
柯里化
在 Groovy 中,柯里化指的是部分应用的概念。它**不**对应于函数式编程中柯里化的真实概念,因为 Groovy 对闭包应用了不同的作用域规则。Groovy 中的柯里化允许您设置闭包的一个参数的值,它将返回一个接受少一个参数的新闭包。
左柯里化
左柯里化是指设置闭包最左侧参数的行为,如下例所示
def nCopies = { int n, String str -> str*n } (1)
def twice = nCopies.curry(2) (2)
assert twice('bla') == 'blabla' (3)
assert twice('bla') == nCopies(2, 'bla') (4)
1 | nCopies 闭包定义了两个参数 |
2 | curry 将第一个参数设置为2 ,创建一个接受单个String 的新闭包(函数) |
3 | 所以新函数可以只用一个String 来调用 |
4 | 它等同于用两个参数调用nCopies |
右柯里化
与左柯里化类似,可以设置闭包最右侧的参数
def nCopies = { int n, String str -> str*n } (1)
def blah = nCopies.rcurry('bla') (2)
assert blah(2) == 'blabla' (3)
assert blah(2) == nCopies(2, 'bla') (4)
1 | nCopies 闭包定义了两个参数 |
2 | rcurry 将最后一个参数设置为bla ,创建了一个接受单个int 的新闭包(函数) |
3 | 所以新函数可以只用一个int 来调用 |
4 | 它等同于用两个参数调用nCopies |
基于索引的柯里化
如果一个闭包接受多于 2 个参数,则可以使用ncurry
设置任意参数
def volume = { double l, double w, double h -> l*w*h } (1)
def fixedWidthVolume = volume.ncurry(1, 2d) (2)
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d) (3)
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d) (4)
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d) (5)
1 | volume 函数定义了3个参数 |
2 | ncurry 将第二个参数(索引 = 1)设置为2d ,创建一个新的 volume 函数,它接受 length 和 height |
3 | 该函数等同于调用volume 时省略宽度 |
4 | 也可以设置多个参数,从指定索引开始 |
5 | 结果函数接受的参数数量等于初始参数数量减去ncurry 设置的参数数量 |
记忆化
记忆化允许缓存闭包调用结果。如果函数(闭包)的计算很慢,但您知道该函数将经常使用相同的参数调用,那么记忆化就很有趣。一个典型的例子是斐波那契数列。一个天真的实现可能看起来像这样
def fib
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }
assert fib(15) == 610 // slow!
这是一个天真的实现,因为'fib'经常以相同的参数递归调用,导致指数级算法
-
计算
fib(15)
需要fib(14)
和fib(13)
的结果 -
计算
fib(14)
需要fib(13)
和fib(12)
的结果
由于调用是递归的,您已经可以看到我们将一遍又一遍地计算相同的值,尽管它们可以被缓存。这个天真的实现可以通过使用memoize
缓存调用结果来“修复”
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }.memoize()
assert fib(25) == 75025 // fast!
缓存**使用参数的实际值**。这意味着如果您将记忆化与非基本类型或非封装基本类型一起使用,应非常小心。 |
缓存的行为可以通过替代方法进行调整
-
memoizeAtMost
将生成一个新的闭包,它**最多**缓存 *n* 个值 -
memoizeAtLeast
将生成一个新的闭包,它**至少**缓存 *n* 个值 -
memoizeBetween
将生成一个新的闭包,它**至少**缓存 *n* 个值,并且**最多**缓存 *n* 个值
所有 memoize 变体中使用的缓存都是 LRU 缓存。
组合
闭包组合对应于函数组合的概念,即通过组合两个或多个函数(链式调用)来创建一个新函数,如下例所示
def plus2 = { it + 2 }
def times3 = { it * 3 }
def times3plus2 = plus2 << times3
assert times3plus2(3) == 11
assert times3plus2(4) == plus2(times3(4))
def plus2times3 = times3 << plus2
assert plus2times3(3) == 15
assert plus2times3(5) == times3(plus2(5))
// reverse composition
assert times3plus2(3) == (times3 >> plus2)(3)
蹦床(Trampoline)
递归算法通常受物理限制:最大堆栈高度。例如,如果您调用的方法递归调用自身过深,您最终会收到StackOverflowException
。
在这些情况下,一种有帮助的方法是使用Closure
及其蹦床功能。
闭包被包装在TrampolineClosure
中。在调用时,蹦床化的Closure
将调用原始Closure
并等待其结果。如果调用的结果是另一个TrampolineClosure
实例(可能是由于调用trampoline()
方法而创建的),则该Closure
将再次被调用。这种对返回的蹦床化闭包实例的重复调用将持续进行,直到返回一个非蹦床化Closure
的值。该值将成为蹦床的最终结果。通过这种方式,调用是串行进行的,而不是填充堆栈。
以下是使用trampoline()
实现阶乘函数的示例
def factorial
factorial = { int n, def accu = 1G ->
if (n < 2) return accu
factorial.trampoline(n - 1, n * accu)
}
factorial = factorial.trampoline()
assert factorial(1) == 1
assert factorial(3) == 1 * 2 * 3
assert factorial(1000) // == 402387260.. plus another 2560 digits
1.6. 语义
本章涵盖 Groovy 编程语言的语义。
1.6.1. 语句
变量定义
变量可以使用其类型(如String
)定义,也可以使用关键字def
(或var
)后跟变量名定义
String x
def y
var z
def
和var
充当类型占位符,即当你不想给出明确类型时,用于替代类型名称。这可能是因为你不关心编译时类型,或者依赖于类型推断(结合 Groovy 的静态特性)。变量定义必须有类型或占位符。如果省略,类型名称将被视为引用现有变量(可能已提前声明)。对于脚本,未声明的变量假定来自 Script 绑定。在其他情况下,你将得到一个缺失属性(动态 Groovy)或编译时错误(静态 Groovy)。如果你将def
和var
视为Object
的别名,你将立即理解。
变量定义可以提供一个初始值,在这种情况下,它就像声明和赋值(我们接下来会介绍)合二为一。
变量定义类型可以通过使用泛型来细化,例如List<String> names 。要了解有关泛型支持的更多信息,请阅读泛型部分。 |
变量赋值
您可以为变量赋值以供后续使用。尝试以下操作
x = 1
println x
x = new java.util.Date()
println x
x = -3.1499392
println x
x = false
println x
x = "Hi"
println x
多重赋值
Groovy 支持多重赋值,即可以一次性给多个变量赋值,例如
def (a, b, c) = [10, 20, 'foo']
assert a == 10 && b == 20 && c == 'foo'
如果您愿意,可以在声明中提供类型
def (int i, String j) = [10, 'foo']
assert i == 10 && j == 'foo'
它不仅用于声明变量,也适用于现有变量
def nums = [1, 3, 5]
def a, b, c
(a, b, c) = nums
assert a == 1 && b == 3 && c == 5
该语法适用于数组和列表,以及返回这些类型的任何方法
def (_, month, year) = "18th June 2009".split()
assert "In $month of $year" == 'In June of 2009'
溢出和下溢
如果左侧有太多变量,多余的变量将用 null 填充
def (a, b, c) = [1, 2]
assert a == 1 && b == 2 && c == null
如果右侧有太多变量,多余的变量将被忽略
def (a, b) = [1, 2, 3]
assert a == 1 && b == 2
使用多重赋值进行对象解构
在描述 Groovy 运算符的部分中,下标运算符的情况已经涵盖,解释了如何重写getAt()
/putAt()
方法。
通过这项技术,我们可以结合多重赋值和下标运算符方法来实现**对象解构**。
考虑以下不可变的Coordinates
类,它包含一对经度和纬度双精度值,并注意我们对getAt()
方法的实现
@Immutable
class Coordinates {
double latitude
double longitude
double getAt(int idx) {
if (idx == 0) latitude
else if (idx == 1) longitude
else throw new Exception("Wrong coordinate index, use 0 or 1")
}
}
现在让我们实例化这个类并解构它的经纬度
def coordinates = new Coordinates(latitude: 43.23, longitude: 3.67) (1)
def (la, lo) = coordinates (2)
assert la == 43.23 (3)
assert lo == 3.67
1 | 我们创建Coordinates 类的一个实例 |
2 | 然后,我们使用多重赋值来获取单独的经度和纬度值 |
3 | 我们最终可以断言它们的值。 |
控制结构
条件结构
Groovy 支持 Java 中常见的 if - else 语法
def x = false
def y = false
if ( !x ) {
x = true
}
assert x == true
if ( x ) {
x = false
} else {
y = true
}
assert x == y
Groovy 也支持正常的 Java“嵌套”if then else if 语法
if ( ... ) {
...
} else if (...) {
...
} else {
...
}
Groovy 中的 switch 语句与 Java 代码向后兼容;因此您可以穿透 case,为多个匹配共享相同的代码。
然而,一个不同之处是 Groovy 的 switch 语句可以处理任何类型的 switch 值,并且可以执行不同类型的匹配。
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 ~/fo*/: // toString() representation of x matches the pattern?
result = "foo regex"
break
case { it < 0 }: // or { x < 0 }
result = "negative"
break
default:
result = "default"
}
assert result == "number"
Switch 支持以下类型的比较
-
如果 switch 值是类的实例,则类 case 值匹配
-
如果 switch 值的
toString()
表示与正则表达式匹配,则正则表达式 case 值匹配 -
如果 switch 值包含在集合中,则集合 case 值匹配。这还包括范围(因为它们是 List)
-
如果调用闭包返回一个根据Groovy truth为真的结果,则闭包 case 值匹配
-
如果以上都不是,则如果 case 值等于 switch 值,则 case 值匹配
当使用闭包 case 值时,默认的it 参数实际上就是 switch 值(在我们的例子中是变量x )。 |
Groovy 还支持 switch 表达式,如下例所示
def partner = switch(person) {
case 'Romeo' -> 'Juliet'
case 'Adam' -> 'Eve'
case 'Antony' -> 'Cleopatra'
case 'Bonnie' -> 'Clyde'
}
循环结构
Groovy 支持标准的 Java / C for 循环
String message = ''
for (int i = 0; i < 5; i++) {
message += 'Hi '
}
assert message == 'Hi Hi Hi Hi Hi '
Java 中更复杂的经典 for 循环形式,带有逗号分隔的表达式,现在得到了支持。例如
def facts = []
def count = 5
for (int fact = 1, i = 1; i <= count; i++, fact *= i) {
facts << fact
}
assert facts == [1, 2, 6, 24, 120]
Groovy 自 Groovy 1.6 起就支持多重赋值语句
// multi-assignment with types
def (String x, int y) = ['foo', 42]
assert "$x $y" == 'foo 42'
这些现在可以出现在 for 循环中
// multi-assignment goes loopy
def baNums = []
for (def (String u, int v) = ['bar', 42]; v < 45; u++, v++) {
baNums << "$u $v"
}
assert baNums == ['bar 42', 'bas 43', 'bat 44']
Groovy 中的 for 循环更简单,并且适用于任何类型的数组、集合、Map 等。
// iterate over a range
def x = 0
for ( i in 0..9 ) {
x += i
}
assert x == 45
// iterate over a list
x = 0
for ( i in [0, 1, 2, 3, 4] ) {
x += i
}
assert x == 10
// iterate over an array
def array = (0..4).toArray()
x = 0
for ( i in array ) {
x += i
}
assert x == 10
// iterate over a map
def map = ['abc':1, 'def':2, 'xyz':3]
x = 0
for ( e in map ) {
x += e.value
}
assert x == 6
// iterate over values in a map
x = 0
for ( v in map.values() ) {
x += v
}
assert x == 6
// iterate over the characters in a string
def text = "abc"
def list = []
for (c in text) {
list.add(c)
}
assert list == ["a", "b", "c"]
Groovy 还支持 Java 的冒号变体:for (char c : text) {} |
Groovy 支持像 Java 一样的常用 while {…} 循环
def x = 0
def y = 5
while ( y-- > 0 ) {
x++
}
assert x == 5
Java 的经典 do/while 循环现在得到了支持。示例
// classic Java-style do..while loop
def count = 5
def fact = 1
do {
fact *= count--
} while(count > 1)
assert fact == 120
异常处理
异常处理与 Java 相同。
try / catch / finally
您可以指定完整的try-catch-finally
、try-catch
或try-finally
块集。
每个块的主体都需要大括号。 |
try {
'moo'.toLong() // this will generate an exception
assert false // asserting that this point should never be reached
} catch ( e ) {
assert e in NumberFormatException
}
我们可以在匹配的“try”子句之后将代码放入“finally”子句中,这样无论“try”子句中的代码是否抛出异常,finally 子句中的代码都将始终执行
def z
try {
def i = 7, j = 0
try {
def k = i / j
assert false //never reached due to Exception in previous line
} finally {
z = 'reached here' //always executed even if Exception thrown
}
} catch ( e ) {
assert e in ArithmeticException
assert z == 'reached here'
}
多重捕获
通过多重捕获块(自 Groovy 2.0 起),我们能够定义多个异常,并由同一个捕获块进行捕获和处理
try {
/* ... */
} catch ( IOException | NullPointerException e ) {
/* one block to handle 2 exceptions */
}
ARM 资源管理 Try-with-resources
Groovy 通常为 Java 7 的try
-with-resources 语句(用于自动资源管理 (ARM))提供更好的替代方案。现在,该语法已支持迁移到 Groovy 且仍希望使用旧式风格的 Java 程序员
class FromResource extends ByteArrayInputStream {
@Override
void close() throws IOException {
super.close()
println "FromResource closing"
}
FromResource(String input) {
super(input.toLowerCase().bytes)
}
}
class ToResource extends ByteArrayOutputStream {
@Override
void close() throws IOException {
super.close()
println "ToResource closing"
}
}
def wrestle(s) {
try (
FromResource from = new FromResource(s)
ToResource to = new ToResource()
) {
to << from
return to.toString()
}
}
def wrestle2(s) {
FromResource from = new FromResource(s)
try (from; ToResource to = new ToResource()) { // Enhanced try-with-resources in Java 9+
to << from
return to.toString()
}
}
assert wrestle("ARM was here!").contains('arm')
assert wrestle2("ARM was here!").contains('arm')
其输出如下
ToResource closing FromResource closing ToResource closing FromResource closing
能力断言
与 Groovy 共享assert
关键字的 Java 不同,后者在 Groovy 中的行为截然不同。首先,Groovy 中的断言总是执行的,独立于 JVM 的-ea
标志。这使其成为单元测试的首选。 “能力断言”的概念直接与 Groovy 的assert
行为方式相关。
一个能力断言由三部分组成
assert [left expression] == [right expression] : (optional message)
断言的结果与您在 Java 中得到的结果大相径庭。如果断言为真,则不发生任何事情。如果断言为假,则它会提供被断言表达式中每个子表达式的值的可视化表示。例如
assert 1+1 == 3
将产生
Caught: Assertion failed: assert 1+1 == 3 | | 2 false
当表达式更复杂时,能力断言变得非常有趣,如下一个例子所示
def x = 2
def y = 7
def z = 5
def calc = { a,b -> a*b+1 }
assert calc(x,y) == [x,z].sum()
这将打印每个子表达式的值
assert calc(x,y) == [x,z].sum()
| | | | | | |
15 2 7 | 2 5 7
false
如果您不想要像上面那样漂亮打印的错误消息,您可以通过更改断言的可选消息部分来回退到自定义错误消息,如下例所示
def x = 2
def y = 7
def z = 5
def calc = { a,b -> a*b+1 }
assert calc(x,y) == z*z : 'Incorrect computation result'
这将打印以下错误消息
Incorrect computation result. Expression: (calc.call(x, y) == (z * z)). Values: z = 5, z = 5
带标签的语句
任何语句都可以与标签关联。标签不影响代码的语义,可用于使代码更易于阅读,如下例所示
given:
def x = 1
def y = 2
when:
def z = x+y
then:
assert z == 3
尽管不改变带标签语句的语义,但可以在break
指令中使用标签作为跳转目标,如下例所示。但是,即使允许,这种编码风格通常也被认为是一种不好的做法
for (int i=0;i<10;i++) {
for (int j=0;j<i;j++) {
println "j=$j"
if (j == 5) {
break exit
}
}
exit: println "i=$i"
}
重要的是要理解,默认情况下,标签对代码的语义没有影响,但是它们属于抽象语法树 (AST),因此 AST 转换可以使用该信息对代码执行转换,从而导致不同的语义。Spock 框架就是这样做的,以便使测试更容易。
1.6.2. 表达式
表达式是 Groovy 程序的构建块,用于引用现有值并执行代码以创建新值。
Groovy 支持许多与 Java 相同的表达式种类,包括
表达式示例 |
描述 |
|
变量、字段、参数的名称,… |
|
特殊名称 |
|
字面量 |
|
类字面量 |
|
带括号的表达式 |
|
一元运算符表达式 |
|
二元运算符表达式 |
|
三元运算符表达式 |
|
Lambda 表达式 |
|
switch 表达式 |
Groovy 也有一些特殊的表达式
表达式示例 |
描述 |
|
缩写类字面量(无歧义时) |
|
闭包表达式 |
|
列表字面量表达式 |
|
映射字面量表达式 |
Groovy 还扩展了 Java 中用于成员访问的常规点表示法。Groovy 通过指定感兴趣数据的层次结构路径来提供对分层数据结构的特殊支持。这些**Groovy 路径**表达式被称为 GPath 表达式。
GPath 表达式
GPath
是一种集成到 Groovy 中的路径表达式语言,它允许识别嵌套结构化数据的部分。从这个意义上讲,它与 XPath 对 XML 的目标和范围相似。GPath 通常用于处理 XML 的上下文中,但它确实适用于任何对象图。XPath 使用类似文件系统的路径表示法,树层次结构的部分由斜杠/
分隔,而 GPath**使用点对象表示法**来执行对象导航。
例如,您可以指定对象或感兴趣元素的路径
-
a.b.c
→ 对于 XML,获取a
中b
中所有c
元素 -
a.b.c
→ 对于 POJO,获取a
的所有b
属性的c
属性(类似于 JavaBeans 中的a.getB().getC()
)
在这两种情况下,GPath 表达式都可以被视为对对象图的查询。对于 POJO,对象图通常由正在编写的程序通过对象实例化和组合来构建;对于 XML 处理,对象图是**解析**XML 文本的结果,通常使用 XmlParser 或 XmlSlurper 等类。有关在 Groovy 中使用 XML 的更深入细节,请参阅处理 XML。
查询由 XmlParser 或 XmlSlurper 生成的对象图时,GPath 表达式可以使用
|
对象导航
让我们看一个 GPath 表达式在简单**对象图**上的示例,即使用 Java 反射获取的对象图。假设您在某个类的非静态方法中,该类有另一个名为aMethodFoo
的方法
void aMethodFoo() { println "This is aMethodFoo." } (0)
以下 GPath 表达式将获取该方法的名称
assert ['aMethodFoo'] == this.class.methods.name.grep(~/.*Foo/)
**更准确地说**,上述 GPath 表达式生成一个字符串列表,每个字符串都是this
上现有方法的名称,并且该名称以Foo
结尾。
现在,给定该类中也定义了以下方法
void aMethodBar() { println "This is aMethodBar." } (1)
void anotherFooMethod() { println "This is anotherFooMethod." } (2)
void aSecondMethodBar() { println "This is aSecondMethodBar." } (3)
那么以下 GPath 表达式将获取**(1)**和**(3)**的名称,而不是**(2)**或**(0)**
assert ['aMethodBar', 'aSecondMethodBar'] as Set == this.class.methods.name.grep(~/.*Bar/) as Set
表达式分解
我们可以分解表达式this.class.methods.name.grep(~/.*Bar/)
来了解 GPath 是如何评估的
this.class
-
属性访问器,等同于 Java 中的
this.getClass()
,生成一个Class
对象。 this.class.methods
-
属性访问器,等同于
this.getClass().getMethods()
,生成一个Method
对象数组。 this.class.methods.name
-
对数组的每个元素应用属性访问器并生成结果列表。
this.class.methods.name.grep(…)
-
对
this.class.methods.name
生成的列表的每个元素调用方法grep
并生成结果列表。
像this.class.methods 这样的子表达式会产生一个数组,因为这是在 Java 中调用this.getClass().getMethods() 会产生的结果。GPath 表达式没有约定说s 意味着列表或类似的东西。 |
GPath 表达式的一个强大特性是,对集合的属性访问会转换为**对集合每个元素的属性访问**,并将结果收集到一个集合中。因此,表达式this.class.methods.name
在 Java 中可以表示为如下所示
List<String> methodNames = new ArrayList<String>();
for (Method method : this.getClass().getMethods()) {
methodNames.add(method.getName());
}
return methodNames;
数组访问表示法也可以在存在集合的 GPath 表达式中使用
assert 'aSecondMethodBar' == this.class.methods.name.grep(~/.*Bar/).sort()[1]
GPath 表达式中的数组访问是基于零的 |
用于 XML 导航的 GPath
这是一个使用 XML 文档和各种形式 GPath 表达式的示例
def xmlText = """
| <root>
| <level>
| <sublevel id='1'>
| <keyVal>
| <key>mykey</key>
| <value>value 123</value>
| </keyVal>
| </sublevel>
| <sublevel id='2'>
| <keyVal>
| <key>anotherKey</key>
| <value>42</value>
| </keyVal>
| <keyVal>
| <key>mykey</key>
| <value>fizzbuzz</value>
| </keyVal>
| </sublevel>
| </level>
| </root>
"""
def root = new XmlSlurper().parseText(xmlText.stripMargin())
assert root.level.size() == 1 (1)
assert root.level.sublevel.size() == 2 (2)
assert root.level.sublevel.findAll { it.@id == 1 }.size() == 1 (3)
assert root.level.sublevel[1].keyVal[0].key.text() == 'anotherKey' (4)
1 | 在root 下有一个level 节点 |
2 | 在root/level 下有两个sublevel 节点 |
3 | 有一个元素sublevel ,其属性id 的值为1 |
4 | root/level 下第二个sublevel 元素的第一个keyVal 元素的key 元素的文本值为 'anotherKey' |
有关 XML GPath 表达式的更多详细信息,请参见XML 用户指南。
1.6.3. 类型提升与强制转换
闭包到类型强制转换
将闭包赋值给 SAM 类型
SAM 类型是定义单个抽象方法的类型。这包括
interface Predicate<T> {
boolean accept(T obj)
}
abstract class Greeter {
abstract String getName()
void greet() {
println "Hello, $name"
}
}
任何闭包都可以使用as
运算符转换为 SAM 类型
Predicate filter = { it.contains 'G' } as Predicate
assert filter.accept('Groovy') == true
Greeter greeter = { 'Groovy' } as Greeter
greeter.greet()
然而,自 Groovy 2.2.0 起,as Type
表达式是可选的。您可以省略它,直接写
Predicate filter = { it.contains 'G' }
assert filter.accept('Groovy') == true
Greeter greeter = { 'Groovy' }
greeter.greet()
这意味着您也可以使用方法指针,如下例所示
boolean doFilter(String s) { s.contains('G') }
Predicate filter = this.&doFilter
assert filter.accept('Groovy') == true
Greeter greeter = GroovySystem.&getVersion
greeter.greet()
使用闭包调用接受 SAM 类型的方法
闭包到 SAM 类型强制转换的第二个也是可能更重要的用例是调用接受 SAM 类型的方法。想象一下以下方法
public <T> List<T> filter(List<T> source, Predicate<T> predicate) {
source.findAll { predicate.accept(it) }
}
然后您可以使用闭包调用它,而无需创建接口的显式实现
assert filter(['Java','Groovy'], { it.contains 'G'} as Predicate) == ['Groovy']
但自 Groovy 2.2.0 起,您还可以省略显式强制转换,并将方法调用视为使用了闭包
assert filter(['Java','Groovy']) { it.contains 'G'} == ['Groovy']
如您所见,这有一个优点,即允许您将闭包语法用于方法调用,即把闭包放在括号外面,从而提高代码的可读性。
闭包到任意类型的强制转换
除了 SAM 类型之外,闭包还可以被强制转换为任何类型,尤其是接口。让我们定义以下接口
interface FooBar {
int foo()
void bar()
}
您可以使用as
关键字将闭包强制转换为接口
def impl = { println 'ok'; 123 } as FooBar
这将生成一个所有方法都使用闭包实现的类
assert impl.foo() == 123
impl.bar()
但也可以将闭包强制转换为任何类。例如,我们可以将我们定义的interface
替换为class
,而无需更改断言
class FooBar {
int foo() { 1 }
void bar() { println 'bar' }
}
def impl = { println 'ok'; 123 } as FooBar
assert impl.foo() == 123
impl.bar()
Map 到类型强制转换
通常,使用单个闭包来实现具有多个方法的接口或类并不是最佳选择。作为替代方案,Groovy 允许您将映射强制转换为接口或类。在这种情况下,映射的键被解释为方法名,而值是方法实现。以下示例说明了将映射强制转换为Iterator
def map
map = [
i: 10,
hasNext: { map.i > 0 },
next: { map.i-- },
]
def iter = map as Iterator
当然,这是一个相当牵强的例子,但它说明了这个概念。您只需要实现那些实际被调用的方法,但如果调用的方法在映射中不存在,则会抛出MissingMethodException
或UnsupportedOperationException
,具体取决于传递给调用的参数,如下例所示
interface X {
void f()
void g(int n)
void h(String s, int n)
}
x = [ f: {println "f called"} ] as X
x.f() // method exists
x.g() // MissingMethodException here
x.g(5) // UnsupportedOperationException here
异常类型取决于调用本身
-
如果调用的参数与接口/类中的参数不匹配,则为
MissingMethodException
-
如果调用的参数与接口/类的重载方法之一匹配,则为
UnsupportedOperationException
String 到枚举强制转换
Groovy 允许透明的String
(或GString
)到枚举值的强制转换。假设您定义了以下枚举
enum State {
up,
down
}
那么您可以将字符串赋值给枚举,而无需使用显式的as
强制转换
State st = 'up'
assert st == State.up
也可以使用GString
作为值
def val = "up"
State st = "${val}"
assert st == State.up
然而,这会抛出运行时错误(IllegalArgumentException
)
State st = 'not an enum value'
请注意,在 switch 语句中也可以使用隐式强制转换
State switchState(State st) {
switch (st) {
case 'up':
return State.down // explicit constant
case 'down':
return 'up' // implicit coercion for return types
}
}
特别是,请注意case
如何使用字符串常量。但如果您调用一个使用枚举并带有String
参数的方法,您仍然必须使用显式as
强制转换
assert switchState('up' as State) == State.down
assert switchState(State.down) == State.up
自定义类型强制转换
类可以通过实现asType
方法来定义自定义强制转换策略。自定义强制转换使用as
运算符调用,并且从不隐式。例如,假设您定义了两个类Polar
和Cartesian
,如下例所示
class Polar {
double r
double phi
}
class Cartesian {
double x
double y
}
并且您希望从极坐标转换为笛卡尔坐标。一种方法是在Polar
类中定义asType
方法
def asType(Class target) {
if (Cartesian==target) {
return new Cartesian(x: r*cos(phi), y: r*sin(phi))
}
}
这允许您使用as
强制转换运算符
def sigma = 1E-16
def polar = new Polar(r:1.0,phi:PI/2)
def cartesian = polar as Cartesian
assert abs(cartesian.x-sigma) < sigma
总而言之,Polar
类看起来像这样
class Polar {
double r
double phi
def asType(Class target) {
if (Cartesian==target) {
return new Cartesian(x: r*cos(phi), y: r*sin(phi))
}
}
}
但也可以在Polar
类之外定义asType
,如果您想为“封闭”类或您不拥有源代码的类定义自定义强制转换策略(例如使用元类),这会很实用
Polar.metaClass.asType = { Class target ->
if (Cartesian==target) {
return new Cartesian(x: r*cos(phi), y: r*sin(phi))
}
}
类字面量与变量和 as 运算符
只有当您有对类的静态引用时才可以使用as
关键字,如下面的代码所示
interface Greeter {
void greet()
}
def greeter = { println 'Hello, Groovy!' } as Greeter // Greeter is known statically
greeter.greet()
但是,如果您通过反射获取类,例如通过调用Class.forName
呢?
Class clazz = Class.forName('Greeter')
尝试使用对类的引用与as
关键字将失败
greeter = { println 'Hello, Groovy!' } as clazz
// throws:
// unable to resolve class clazz
// @ line 9, column 40.
// greeter = { println 'Hello, Groovy!' } as clazz
它失败了,因为as
关键字只适用于类字面量。相反,您需要调用asType
方法
greeter = { println 'Hello, Groovy!' }.asType(clazz)
greeter.greet()
1.6.4. 可选性
可选括号
如果至少有一个参数且没有歧义,则方法调用可以省略括号
println 'Hello World'
def maximum = Math.max 5, 10
无参数或模糊的方法调用需要括号
println()
println(Math.max(5, 10))
可选分号
在 Groovy 中,如果一行只包含一个语句,则行末的分号可以省略。
这意味着
assert true;
更符合习惯的写法是
assert true
一行中的多个语句需要分号分隔
boolean a = true; assert a
可选的 return 关键字
在 Groovy 中,方法或闭包主体中评估的最后一个表达式将被返回。这意味着 return
关键字是可选的。
int add(int a, int b) {
return a+b
}
assert add(1, 2) == 3
可以缩短为
int add(int a, int b) {
a+b
}
assert add(1, 2) == 3
可选的 public 关键字
默认情况下,Groovy 类和方法是 public
。因此,这个类
public class Server {
public String toString() { "a server" }
}
与此类别相同
class Server {
String toString() { "a server" }
}
1.6.5. Groovy 的真理
Groovy 通过应用以下规则来判断表达式是真还是假。
迭代器和枚举
具有更多元素的迭代器和枚举被强制转换为真。
assert [0].iterator()
assert ![].iterator()
Vector v = [0] as Vector
Enumeration enumeration = v.elements()
assert enumeration
enumeration.nextElement()
assert !enumeration
字符串
非空字符串、GString 和 CharSequence 被强制转换为真。
assert 'a'
assert !''
def nonEmpty = 'a'
assert "$nonEmpty"
def empty = ''
assert !"$empty"
使用 asBoolean() 方法定制真理
为了定制 groovy 将您的对象评估为 true
还是 false
,请实现 asBoolean()
方法
class Color {
String name
boolean asBoolean(){
name == 'green' ? true : false
}
}
Groovy 将调用此方法将您的对象强制转换为布尔值,例如
assert new Color(name: 'green')
assert !new Color(name: 'red')
1.6.6. 类型
可选类型
可选类型是指即使您不显式指定变量类型,程序也能工作的思想。作为一种动态语言,Groovy 自然地实现了这一特性,例如当您声明一个变量时
String aString = 'foo' (1)
assert aString.toUpperCase() (2)
1 | foo 使用显式类型 String 声明 |
2 | 我们可以在 String 上调用 toUpperCase 方法 |
Groovy 将允许您这样编写
def aString = 'foo' (1)
assert aString.toUpperCase() (2)
1 | foo 使用 def 声明 |
2 | 我们仍然可以调用 toUpperCase 方法,因为 aString 的类型在运行时解析 |
因此,您在这里是否使用显式类型并不重要。当您将此功能与静态类型检查结合使用时,它尤其有趣,因为类型检查器会执行类型推断。
同样,Groovy 不强制声明方法中的参数类型
String concat(String a, String b) {
a+b
}
assert concat('foo','bar') == 'foobar'
可以使用 def
作为返回类型和参数类型进行重写,以便利用鸭子类型,如此示例所示
def concat(def a, def b) { (1)
a+b
}
assert concat('foo','bar') == 'foobar' (2)
assert concat(1,2) == 3 (3)
1 | 返回类型和参数类型都使用 def |
2 | 它使得使用 String 调用方法成为可能 |
3 | 也可以使用 int ,因为 plus 方法已定义 |
这里建议使用 def 关键字来描述方法的意图,该方法应该适用于任何类型,但从技术上讲,我们可以使用 Object 代替,结果将是相同的:在 Groovy 中,def 严格等同于使用 Object 。 |
最后,类型可以完全从返回类型和描述符中删除。但是,如果您想从返回类型中删除它,那么您需要为方法添加一个显式修饰符,以便编译器可以区分方法声明和方法调用,如此示例所示
private concat(a,b) { (1)
a+b
}
assert concat('foo','bar') == 'foobar' (2)
assert concat(1,2) == 3 (3)
1 | 如果我们要省略返回类型,必须设置一个显式修饰符。 |
2 | 仍然可以使用 String 调用该方法 |
3 | 也可以使用 int |
对于公共 API 中的方法参数或方法返回类型,省略类型通常被认为是一种不好的做法。虽然在局部变量中使用 def 并不是真正的问题,因为变量的可见性仅限于方法本身,但在方法参数上设置时,def 将在方法签名中转换为 Object ,这使得用户难以知道参数的预期类型。这意味着您应该将其限制在您明确依赖鸭子类型的情况。 |
静态类型检查
默认情况下,Groovy 在编译时执行最小类型检查。由于它主要是一种动态语言,因此静态编译器通常会执行的大多数检查在编译时是不可能的。通过运行时元编程添加的方法可能会改变类或对象的运行时行为。以下示例说明了原因
class Person { (1)
String firstName
String lastName
}
def p = new Person(firstName: 'Raymond', lastName: 'Devos') (2)
assert p.formattedName == 'Raymond Devos' (3)
1 | Person 类只定义了两个属性:firstName 和 lastName |
2 | 我们可以创建一个 Person 实例 |
3 | 并调用一个名为 formattedName 的方法 |
在动态语言中,像上面这样的代码通常不会抛出任何错误。这是怎么回事?在 Java 中,这通常会在编译时失败。然而,在 Groovy 中,它不会在编译时失败,如果编码正确,也不会在运行时失败。事实上,为了让它在运行时工作,一种可能性是依赖运行时元编程。因此,在声明 Person
类之后添加这一行就足够了
Person.metaClass.getFormattedName = { "$delegate.firstName $delegate.lastName" }
这意味着在 Groovy 中,一般情况下,您不能对对象的类型做出任何超出其声明类型的假设,即使您知道它,也无法在编译时确定将调用哪个方法或检索哪个属性。它有很多用途,从编写 DSL 到测试,这些将在本手册的其他部分讨论。
然而,如果您的程序不依赖动态特性并且您来自静态世界(特别是 Java 思维),那么在编译时没有捕获此类“错误”可能会令人惊讶。正如我们在前面的示例中看到的,编译器无法确定这是一个错误。要让它知道这是一个错误,您必须明确指示编译器您正在切换到类型检查模式。这可以通过使用 @groovy.transform.TypeChecked
注解类或方法来完成。
当类型检查被激活时,编译器执行更多工作
-
类型推断被激活,这意味着即使您在局部变量上使用
def
,类型检查器也能够从赋值中推断变量的类型 -
方法调用在编译时解析,这意味着如果一个方法没有在一个类上声明,编译器将抛出错误
-
总的来说,您习惯在静态语言中找到的所有编译时错误都会出现:方法未找到、属性未找到、方法调用类型不兼容、数字精度错误等等。
在本节中,我们将描述类型检查器在各种情况下的行为,并解释在代码中使用 @TypeChecked
的限制。
@TypeChecked
注解
groovy.transform.TypeChecked
注解启用类型检查。它可以放在一个类上
@groovy.transform.TypeChecked
class Calculator {
int sum(int x, int y) { x+y }
}
或在方法上
class Calculator {
@groovy.transform.TypeChecked
int sum(int x, int y) { x+y }
}
在第一种情况下,被注解类的所有方法、属性、字段、内部类等都将进行类型检查,而在第二种情况下,只有方法及其可能包含的闭包或匿名内部类会进行类型检查。
类型检查的范围可以限制。例如,如果一个类被类型检查,您可以指示类型检查器通过使用 @TypeChecked(TypeCheckingMode.SKIP)
注解方法来跳过它
import groovy.transform.TypeChecked
import groovy.transform.TypeCheckingMode
@TypeChecked (1)
class GreetingService {
String greeting() { (2)
doGreet()
}
@TypeChecked(TypeCheckingMode.SKIP) (3)
private String doGreet() {
def b = new SentenceBuilder()
b.Hello.my.name.is.John (4)
b
}
}
def s = new GreetingService()
assert s.greeting() == 'Hello my name is John'
1 | GreetingService 类被标记为类型检查 |
2 | 因此 greeting 方法会自动进行类型检查 |
3 | 但 doGreet 标记为 SKIP |
4 | 类型检查器不在这里抱怨缺少属性 |
在前面的例子中,SentenceBuilder
依赖于动态代码。没有真正的 Hello
方法或属性,所以类型检查器通常会抱怨并导致编译失败。由于使用构建器的方法被标记为 TypeCheckingMode.SKIP
,所以该方法的类型检查被 跳过,因此代码将编译,即使类的其余部分被类型检查。
以下部分描述了 Groovy 中类型检查的语义。
类型检查赋值
类型为 A
的对象 o
可以赋值给类型为 T
的变量,当且仅当
-
T
等于A
Date now = new Date()
-
或
T
是String
,boolean
,Boolean
或Class
之一String s = new Date() // implicit call to toString Boolean boxed = 'some string' // Groovy truth boolean prim = 'some string' // Groovy truth Class clazz = 'java.lang.String' // class coercion
-
或
o
为 null 且T
不是基本类型String s = null // passes int i = null // fails
-
或
T
是一个数组,A
也是一个数组,并且A
的组件类型可以赋值给T
的组件类型int[] i = new int[4] // passes int[] i = new String[4] // fails
-
或
T
是一个数组,A
是一个集合或流,并且A
的组件类型可以赋值给T
的组件类型int[] i = [1,2,3] // passes int[] i = [1,2, new Date()] // fails Set set = [1,2,3] Number[] na = set // passes def stream = Arrays.stream(1,2,3) int[] i = stream // passes
-
或
T
是A
的超类AbstractList list = new ArrayList() // passes LinkedList list = new ArrayList() // fails
-
或
T
是由A
实现的接口List list = new ArrayList() // passes RandomAccess list = new LinkedList() // fails
-
或
T
或A
是基本类型,且它们的包装类型可以赋值int i = 0 Integer bi = 1 int x = Integer.valueOf(123) double d = Float.valueOf(5f)
-
或
T
扩展groovy.lang.Closure
且A
是一个 SAM 类型(单一抽象方法类型)Runnable r = { println 'Hello' } interface SAMType { int doSomething() } SAMType sam = { 123 } assert sam.doSomething() == 123 abstract class AbstractSAM { int calc() { 2* value() } abstract int value() } AbstractSAM c = { 123 } assert c.calc() == 246
-
或
T
和A
派生自java.lang.Number
并符合下表
T | A | 示例 |
---|---|---|
Double |
除 BigDecimal 或 BigInteger 外的任何类型 |
|
Float |
除 BigDecimal、BigInteger 或 Double 外的任何类型 |
|
Long |
除 BigDecimal、BigInteger、Double 或 Float 外的任何类型 |
|
Integer |
除 BigDecimal、BigInteger、Double、Float 或 Long 外的任何类型 |
|
Short |
除 BigDecimal、BigInteger、Double、Float、Long 或 Integer 外的任何类型 |
|
字节 |
字节 |
|
列表和映射构造函数
除了上述赋值规则外,如果一个赋值被认为是无效的,在类型检查模式下,如果一个 列表 字面量或 映射 字面量 A
可以赋值给类型为 T
的变量,那么
-
该赋值是一个变量声明,并且
A
是一个列表字面量,并且T
有一个构造函数,其参数与列表字面量中的元素的类型匹配 -
该赋值是一个变量声明,并且
A
是一个 map 字面量,并且T
有一个无参构造函数,并且每个 map 键都有一个属性
例如,Instead of writing
@groovy.transform.TupleConstructor
class Person {
String firstName
String lastName
}
Person classic = new Person('Ada','Lovelace')
您可以使用“列表构造函数”
Person list = ['Ada','Lovelace']
或“map 构造函数”
Person map = [firstName:'Ada', lastName:'Lovelace']
如果您使用 map 构造函数,则会对 map 的键进行额外检查,以检查是否定义了同名属性。例如,以下操作将在编译时失败
@groovy.transform.TupleConstructor
class Person {
String firstName
String lastName
}
Person map = [firstName:'Ada', lastName:'Lovelace', age: 24] (1)
1 | 类型检查器将在编译时抛出错误 No such property: age for class: Person |
方法解析
在类型检查模式下,方法在编译时解析。解析按名称和参数进行。返回类型与方法选择无关。参数类型根据以下规则与参数类型匹配
类型为 A
的参数 o
可用于类型为 T
的参数,当且仅当
-
T
等于A
int sum(int x, int y) { x+y } assert sum(3,4) == 7
-
或
T
是String
且A
是GString
String format(String str) { "Result: $str" } assert format("${3+4}") == "Result: 7"
-
或
o
为 null 且T
不是基本类型String format(int value) { "Result: $value" } assert format(7) == "Result: 7" format(null) // fails
-
或
T
是一个数组,A
也是一个数组,并且A
的组件类型可以赋值给T
的组件类型String format(String[] values) { "Result: ${values.join(' ')}" } assert format(['a','b'] as String[]) == "Result: a b" format([1,2] as int[]) // fails
-
或
T
是A
的超类String format(AbstractList list) { list.join(',') } format(new ArrayList()) // passes String format(LinkedList list) { list.join(',') } format(new ArrayList()) // fails
-
或
T
是由A
实现的接口String format(List list) { list.join(',') } format(new ArrayList()) // passes String format(RandomAccess list) { 'foo' } format(new LinkedList()) // fails
-
或
T
或A
是基本类型,且它们的包装类型可以赋值int sum(int x, Integer y) { x+y } assert sum(3, new Integer(4)) == 7 assert sum(new Integer(3), 4) == 7 assert sum(new Integer(3), new Integer(4)) == 7 assert sum(new Integer(3), 4) == 7
-
或
T
扩展groovy.lang.Closure
且A
是一个 SAM 类型(单一抽象方法类型)interface SAMType { int doSomething() } int twice(SAMType sam) { 2*sam.doSomething() } assert twice { 123 } == 246 abstract class AbstractSAM { int calc() { 2* value() } abstract int value() } int eightTimes(AbstractSAM sam) { 4*sam.calc() } assert eightTimes { 123 } == 984
-
或
T
和A
派生自java.lang.Number
并符合与 数字赋值 相同的规则
如果在编译时未找到具有适当名称和参数的方法,则会抛出错误。“普通”Groovy 的区别在以下示例中说明
class MyService {
void doSomething() {
printLine 'Do something' (1)
}
}
1 | printLine 是一个错误,但由于我们处于动态模式,因此错误未在编译时捕获 |
上面的示例展示了一个 Groovy 能够编译的类。但是,如果您尝试创建 MyService
的实例并调用 doSomething
方法,那么它将在 运行时 失败,因为 printLine
不存在。当然,我们已经展示了 Groovy 如何使其成为一个完全有效的调用,例如通过捕获 MethodMissingException
或实现自定义元类,但如果您知道您不在这种情况中,那么 @TypeChecked
就派上用场了
@groovy.transform.TypeChecked
class MyService {
void doSomething() {
printLine 'Do something' (1)
}
}
1 | printLine 这次是一个编译时错误 |
只需添加 @TypeChecked
即可触发编译时方法解析。类型检查器将尝试在 MyService
类上查找接受 String
的 printLine
方法,但找不到。它将以以下消息编译失败
找不到匹配的方法 MyService#printLine(java.lang.String)
理解类型检查器背后的逻辑非常重要:它是一种编译时检查,因此根据定义,类型检查器不了解您所做的任何形式的 运行时 元编程。这意味着在没有 @TypeChecked 的情况下完全有效的代码,如果您激活类型检查,将 不再 编译。如果您考虑到鸭子类型,这一点尤其正确 |
class Duck {
void quack() { (1)
println 'Quack!'
}
}
class QuackingBird {
void quack() { (2)
println 'Quack!'
}
}
@groovy.transform.TypeChecked
void accept(quacker) {
quacker.quack() (3)
}
accept(new Duck()) (4)
1 | 我们定义一个 Duck 类,它定义了一个 quack 方法 |
2 | 我们定义另一个 QuackingBird 类,它也定义了一个 quack 方法 |
3 | quacker 是松散类型的,所以由于方法是 @TypeChecked ,我们将得到一个编译时错误 |
4 | 即使在非类型检查的 Groovy 中,这也会通过 |
有一些可能的解决方案,例如引入接口,但基本上,通过激活类型检查,您可以获得类型安全,但会失去语言的一些特性。希望 Groovy 会引入一些特性,例如流类型,以缩小类型检查和非类型检查 Groovy 之间的差距。
类型推断
当代码用 @TypeChecked
注解时,编译器执行类型推断。它不仅依赖于静态类型,还使用各种技术来推断变量、返回类型、字面量等的类型,以便即使您激活类型检查器,代码也能保持尽可能干净。
最简单的例子是推断变量的类型
def message = 'Welcome to Groovy!' (1)
println message.toUpperCase() (2)
println message.upper() // compile time error (3)
1 | 变量使用 def 关键字声明 |
2 | 类型检查器允许调用 toUpperCase |
3 | 调用 upper 将在编译时失败 |
调用 toUpperCase
之所以有效,是因为 message
的类型被 推断 为 String
。
值得注意的是,尽管编译器对局部变量执行类型推断,但它 不 对字段执行任何类型的推断,总是回退到字段的 声明类型。为了说明这一点,让我们看这个例子
class SomeClass {
def someUntypedField (1)
String someTypedField (2)
void someMethod() {
someUntypedField = '123' (3)
someUntypedField = someUntypedField.toUpperCase() // compile-time error (4)
}
void someSafeMethod() {
someTypedField = '123' (5)
someTypedField = someTypedField.toUpperCase() (6)
}
void someMethodUsingLocalVariable() {
def localVariable = '123' (7)
someUntypedField = localVariable.toUpperCase() (8)
}
}
1 | someUntypedField 使用 def 作为声明类型 |
2 | someTypedField 使用 String 作为声明类型 |
3 | 我们可以将 任何内容 赋值给 someUntypedField |
4 | 然而,调用 toUpperCase 在编译时失败,因为该字段未正确类型化 |
5 | 我们可以将 String 赋值给 String 类型的字段 |
6 | 这次允许 toUpperCase |
7 | 如果我们将 String 赋值给局部变量 |
8 | 然后在局部变量上允许调用 toUpperCase |
为什么会有这样的差异?原因在于 线程安全。在编译时,我们无法对字段的类型做出 任何 保证。任何线程都可以在任何时候访问任何字段,并且在方法中为字段赋值为某种类型变量的时刻与下一行使用该字段的时刻之间,另一个线程可能已经改变了字段的内容。局部变量则不然:我们知道它们是否“逸出”,因此我们可以确保变量的类型随时间保持不变(或不)。请注意,即使字段是 final 的,JVM 也无法保证其类型,因此如果字段是 final 的,类型检查器也不会有不同的行为。
这是我们建议使用 类型化 字段的原因之一。虽然由于类型推断,在局部变量中使用 def 完全没问题,但对于字段来说并非如此,字段也属于类的公共 API,因此类型很重要。 |
Groovy 为各种类型字面量提供了语法。Groovy 中有三种原生集合字面量
-
列表,使用
[]
字面量 -
映射,使用
[:]
字面量 -
范围,使用
from..to
(包含)、from..
(右排除)、 from<..to
(左排除)和from<..
(完全排除)
字面量的推断类型取决于字面量的元素,如下表所示
字面量 | 推断类型 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
如您所见,除了 IntRange
这一显著例外,推断类型利用泛型类型来描述集合的内容。如果集合包含不同类型的元素,类型检查器仍然会对其组件执行类型推断,但会使用 最小上界 的概念。
在 Groovy 中,两个类型 A
和 B
的 最小上界 定义为一种类型,其
-
超类对应于
A
和B
的共同超类 -
接口对应于
A
和B
都实现的接口 -
如果
A
或B
是基本类型,且A
不等于B
,则A
和B
的最小上界是它们包装类型的最小上界
如果 A
和 B
只有一个(1)共同接口,并且它们的共同超类是 Object
,那么两者的 LUB 就是该共同接口。
最小上界表示 A
和 B
都可以赋值到的最小类型。因此,例如,如果 A
和 B
都是 String
,那么两者的 LUB(最小上界)也是 String
。
class Top {}
class Bottom1 extends Top {}
class Bottom2 extends Top {}
assert leastUpperBound(String, String) == String (1)
assert leastUpperBound(ArrayList, LinkedList) == AbstractList (2)
assert leastUpperBound(ArrayList, List) == List (3)
assert leastUpperBound(List, List) == List (4)
assert leastUpperBound(Bottom1, Bottom2) == Top (5)
assert leastUpperBound(List, Serializable) == Object (6)
1 | String 和 String 的 LUB 是 String |
2 | ArrayList 和 LinkedList 的 LUB 是它们的共同超类型 AbstractList |
3 | ArrayList 和 List 的 LUB 是它们唯一的共同接口 List |
4 | 两个相同接口的 LUB 就是接口本身 |
5 | Bottom1 和 Bottom2 的 LUB 是它们的超类 Top |
6 | 两个没有任何共同之处的类型的 LUB 是 Object |
在这些示例中,LUB 始终可以表示为普通的、JVM 支持的类型。但是 Groovy 内部将 LUB 表示为一种更复杂的类型,您无法用它来定义变量。为了说明这一点,让我们继续这个示例
interface Foo {}
class Top {}
class Bottom extends Top implements Serializable, Foo {}
class SerializableFooImpl implements Serializable, Foo {}
Bottom
和 SerializableFooImpl
的最小上界是什么?它们没有共同的超类(除了 Object
),但它们确实共享 2 个接口(Serializable
和 Foo
),所以它们的最小上界是一种表示两个接口(Serializable
和 Foo
)并集的类型。这种类型无法在源代码中定义,但 Groovy 知道它。
在集合类型推断(以及泛型类型推断)的上下文中,这变得很方便,因为组件的类型被推断为最小上界。我们可以在以下示例中说明这为什么很重要
interface Greeter { void greet() } (1)
interface Salute { void salute() } (2)
class A implements Greeter, Salute { (3)
void greet() { println "Hello, I'm A!" }
void salute() { println "Bye from A!" }
}
class B implements Greeter, Salute { (4)
void greet() { println "Hello, I'm B!" }
void salute() { println "Bye from B!" }
void exit() { println 'No way!' } (5)
}
def list = [new A(), new B()] (6)
list.each {
it.greet() (7)
it.salute() (8)
it.exit() (9)
}
1 | Greeter 接口定义了一个方法,greet |
2 | Salute 接口定义了一个方法,salute |
3 | 类 A 实现了 Greeter 和 Salute ,但没有显式接口扩展两者 |
4 | B 也一样 |
5 | 但 B 定义了一个额外的 exit 方法 |
6 | list 的类型被推断为“A 和 B 的 LUB 列表” |
7 | 因此,可以通过 Greeter 接口调用在 A 和 B 上定义的 greet |
8 | 并且可以通过 Salute 接口调用在 A 和 B 上定义的 salute |
9 | 但调用 exit 是一个编译时错误,因为它不属于 A 和 B 的 LUB(仅在 B 中定义) |
错误消息将如下所示
[Static type checking] - Cannot find matching method Greeter or Salute#exit()
这表明 exit
方法既未定义在 Greeter
上,也未定义在 Salute
上,而这两个接口是在 A
和 B
的最小上界中定义的。
在正常的、非类型检查的 Groovy 中,您可以编写如下内容
class Greeter {
String greeting() { 'Hello' }
}
void doSomething(def o) {
if (o instanceof Greeter) { (1)
println o.greeting() (2)
}
}
doSomething(new Greeter())
1 | 使用 instanceof 检查来保护方法调用 |
2 | 进行调用 |
方法调用之所以有效,是因为动态调度(方法在运行时选择)。Java 中等效的代码需要在调用 greeting
方法之前将 o
转换为 Greeter
,因为方法是在编译时选择的
if (o instanceof Greeter) {
System.out.println(((Greeter)o).greeting());
}
然而,在 Groovy 中,即使您在 doSomething
方法上添加 @TypeChecked
(从而激活类型检查),强制转换也 不 是必需的。编译器嵌入了 instanceof 推断,使强制转换成为可选。
流类型是 Groovy 在类型检查模式下的一个重要概念,也是类型推断的扩展。其思想是编译器能够推断代码流中变量的类型,而不仅仅是在初始化时
@groovy.transform.TypeChecked
void flowTyping() {
def o = 'foo' (1)
o = o.toUpperCase() (2)
o = 9d (3)
o = Math.sqrt(o) (4)
}
1 | 首先,o 使用 def 声明并赋值为 String |
2 | 编译器推断 o 是 String ,因此允许调用 toUpperCase |
3 | o 被重新赋值为 double |
4 | 调用 Math.sqrt 通过编译,因为编译器知道此时 o 是 double |
因此,类型检查器 知道 变量的具体类型会随时间变化。特别是,如果您将最后一个赋值替换为
o = 9d
o = o.toUpperCase()
类型检查器现在将在编译时失败,因为它知道当调用 toUpperCase
时,o
是一个 double
,所以这是一个类型错误。
重要的是要理解,触发类型推断的并非是用 def
声明变量。流类型适用于 任何 类型变量。使用显式类型声明变量只限制了您可以赋给该变量的值
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
List list = ['a','b','c'] (1)
list = list*.toUpperCase() (2)
list = 'foo' (3)
}
1 | list 声明为未检查的 List ,并赋值为 String 列表字面量 |
2 | 这一行通过编译,因为流类型:类型检查器知道 list 此时是 List<String> |
3 | 但是不能将 String 赋值给 List ,所以这是一个类型检查错误 |
您还可以注意到,即使变量声明 没有 泛型信息,类型检查器也知道组件类型。因此,这样的代码将编译失败
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
List list = ['a','b','c'] (1)
list.add(1) (2)
}
1 | list 被推断为 List<String> |
2 | 因此,向 List<String> 添加 int 是一个编译时错误 |
解决这个问题需要在声明中添加显式泛型类型
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
List<? extends Serializable> list = [] (1)
list.addAll(['a','b','c']) (2)
list.add(1) (3)
}
1 | list 声明为 List<? extends Serializable> 并用空列表初始化 |
2 | 添加到列表的元素符合列表的声明类型 |
3 | 因此允许将 int 添加到 List<? extends Serializable> |
引入流类型是为了减少经典 Groovy 和静态 Groovy 之间语义的差异。特别是,考虑 Java 中这段代码的行为
public Integer compute(String str) {
return str.length();
}
public String compute(Object o) {
return "Nope";
}
// ...
Object string = "Some string"; (1)
Object result = compute(string); (2)
System.out.println(result); (3)
1 | o 声明为 Object 并赋值为 String |
2 | 我们使用 o 调用 compute 方法 |
3 | 并打印结果 |
在 Java 中,此代码将输出 Nope
,因为方法选择是在编译时完成的,并且基于 声明 类型。因此,即使 o
在运行时是 String
,仍然会调用 Object
版本,因为 o
已声明为 Object
。简而言之,在 Java 中,声明类型最重要,无论是变量类型、参数类型还是返回类型。
在 Groovy 中,我们可以写
int compute(String string) { string.length() }
String compute(Object o) { "Nope" }
Object o = 'string'
def result = compute(o)
println result
但这次,它将返回 6
,因为所选的方法 在运行时 是基于 实际 参数类型。因此在运行时,o
是一个 String
,所以使用了 String
变体。请注意,这种行为与类型检查无关,它是 Groovy 的一般工作方式:动态调度。
在类型检查的 Groovy 中,我们希望确保类型检查器在 编译时 选择与运行时会选择的相同方法。由于语言的语义,这通常是不可能的,但我们可以通过流式类型改进它。通过流式类型,当调用 compute
方法时,o
被 推断 为 String
,因此选择了接受 String
并返回 int
的版本。这意味着我们可以将方法的返回类型推断为 int
,而不是 String
。这对于后续调用和类型安全非常重要。
因此,在类型检查的 Groovy 中,流类型是一个非常重要的概念,这也意味着如果应用了 @TypeChecked
,方法是根据参数的 推断类型 而不是声明类型选择的。这并不能确保 100% 的类型安全,因为类型检查器 可能 选择错误的方法,但它确保了最接近动态 Groovy 的语义。
class Top {
void methodFromTop() {}
}
class Bottom extends Top {
void methodFromBottom() {}
}
def o
if (someCondition) {
o = new Top() (1)
} else {
o = new Bottom() (2)
}
o.methodFromTop() (3)
o.methodFromBottom() // compilation error (4)
1 | 如果 someCondition 为 true,则 o 被赋值为 Top |
2 | 如果 someCondition 为 false,则 o 被赋值为 Bottom |
3 | 调用 methodFromTop 是安全的 |
4 | 但调用 methodFromBottom 不安全,所以这是一个编译时错误 |
当类型检查器访问 if/else
控制结构时,它会检查 if/else
分支中所有被赋值的变量,并计算所有赋值的 最小上界。此类型是 if/else
块后推断变量的类型,因此在此示例中,o
在 if
分支中被赋值为 Top
,在 else
分支中被赋值为 Bottom
。它们的 LUB 是 Top
,因此在条件分支之后,编译器推断 o
为 Top
。因此,允许调用 methodFromTop
,但不允许调用 methodFromBottom
。
闭包和闭包共享变量也存在相同的推理。闭包共享变量是在闭包外部定义但在闭包内部使用的变量,例如
def text = 'Hello, world!' (1)
def closure = {
println text (2)
}
1 | 声明了一个名为 text 的变量 |
2 | text 在闭包内部使用。这是一个 闭包共享变量。 |
Groovy 允许开发者使用这些变量,而无需它们是 final 的。这意味着闭包共享变量可以在闭包内部重新赋值
String result
doSomething { String it ->
result = "Result: $it"
}
result = result?.toUpperCase()
问题在于闭包是一个独立的、可以(或不)在 任何 时候执行的代码块。特别是,doSomething
可能是异步的。这意味着闭包的主体不属于主控制流。因此,类型检查器还会为每个闭包共享变量计算所有赋值的 LUB,并将该 LUB
用作闭包范围之外的推断类型,如此示例所示
class Top {
void methodFromTop() {}
}
class Bottom extends Top {
void methodFromBottom() {}
}
def o = new Top() (1)
Thread.start {
o = new Bottom() (2)
}
o.methodFromTop() (3)
o.methodFromBottom() // compilation error (4)
1 | 一个闭包共享变量首先被赋值为 Top |
2 | 在闭包内部,它被赋值为 Bottom |
3 | methodFromTop 允许 |
4 | methodFromBottom 是一个编译错误 |
这里很明显,当调用 methodFromBottom
时,在编译时或运行时都无法保证 o
的类型 实际 是 Bottom
。它有可能是,但我们无法确定,因为它是异步的。因此,类型检查器只允许在 最小上界 上进行调用,这里是 Top
。
闭包和类型推断
类型检查器对闭包执行特殊推断,从而一方面进行额外检查,另一方面提高流畅性。
类型检查器能够做的第一件事是推断闭包的 返回类型。这在以下示例中得到了简单说明
@groovy.transform.TypeChecked
int testClosureReturnTypeInference(String arg) {
def cl = { "Arg: $arg" } (1)
def val = cl() (2)
val.length() (3)
}
1 | 定义一个闭包,它返回一个字符串(更准确地说是 GString ) |
2 | 我们调用闭包并将结果赋给一个变量 |
3 | 类型检查器推断闭包会返回一个字符串,因此允许调用 length() |
如您所见,与显式声明返回类型的方法不同,无需声明闭包的返回类型:其类型是从闭包主体推断的。
除了返回类型之外,闭包还可以从上下文中推断其参数类型。编译器推断参数类型有两种方式
-
通过 隐式 SAM 类型强制转换
-
通过 API 元数据
为了说明这一点,我们从一个将因类型检查器无法推断参数类型而导致编译失败的示例开始
class Person {
String name
int age
}
void inviteIf(Person p, Closure<Boolean> predicate) { (1)
if (predicate.call(p)) {
// send invite
// ...
}
}
@groovy.transform.TypeChecked
void failCompilation() {
Person p = new Person(name: 'Gerard', age: 55)
inviteIf(p) { (2)
it.age >= 18 // No such property: age (3)
}
}
1 | inviteIf 方法接受一个 Person 和一个 Closure |
2 | 我们使用 Person 和 Closure 调用它 |
3 | 然而 it 在静态上不被认为是 Person ,并且编译失败 |
在此示例中,闭包主体包含 it.age
。对于动态的、未类型检查的代码,这将起作用,因为 it
的类型在运行时将是 Person
。不幸的是,在编译时,仅通过读取 inviteIf
的签名,无法知道 it
的类型。
简而言之,类型检查器没有足够的 inviteIf
方法的上下文信息来静态确定 it
的类型。这意味着需要将方法调用重写为这样
inviteIf(p) { Person it -> (1)
it.age >= 18
}
1 | it 的类型需要显式声明 |
通过显式声明 it
变量的类型,您可以解决问题并使此代码进行静态检查。
对于 API 或框架设计者来说,有两种方法可以使这对于用户来说更优雅,这样他们就不必为闭包参数声明显式类型。第一种也是最简单的方法是将闭包替换为 SAM 类型
interface Predicate<On> { boolean apply(On e) } (1)
void inviteIf(Person p, Predicate<Person> predicate) { (2)
if (predicate.apply(p)) {
// send invite
// ...
}
}
@groovy.transform.TypeChecked
void passesCompilation() {
Person p = new Person(name: 'Gerard', age: 55)
inviteIf(p) { (3)
it.age >= 18 (4)
}
}
1 | 声明一个带 apply 方法的 SAM 接口 |
2 | inviteIf 现在使用 Predicate<Person> 而不是 Closure<Boolean> |
3 | 不再需要声明 it 变量的类型 |
4 | it.age 编译正常,it 的类型是从 Predicate#apply 方法签名推断的 |
通过使用这种技术,我们利用了 Groovy 的 闭包到 SAM 类型自动强制转换 功能。您是否应该使用 SAM 类型 或 闭包 实际上取决于您需要做什么。在很多情况下,使用 SAM 接口就足够了,特别是如果您考虑 Java 8 中发现的函数式接口。然而,闭包提供了函数式接口无法访问的功能。特别是,闭包可以有委托、所有者,并且可以在调用之前作为对象(例如,克隆、序列化、柯里化等)进行操作。它们还可以支持多个签名(多态性)。因此,如果您需要这种操作,最好切换到下面描述的更高级的类型推断注解。 |
当涉及到闭包参数类型推断时,需要解决的原始问题是,静态确定闭包参数的类型,而 无需 显式声明它们,这是因为 Groovy 类型系统继承了 Java 类型系统,它不足以描述参数的类型。
@ClosureParams
注解Groovy 提供了一个注解 @ClosureParams
,旨在完善类型信息。此注解主要针对希望通过提供类型推断元数据来扩展类型检查器功能的框架和 API 开发人员。如果您的库使用闭包并且您也希望获得最大程度的工具支持,这一点很重要。
让我们通过引入 @ClosureParams
注解来修复原始示例,从而说明这一点
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FirstParam
void inviteIf(Person p, @ClosureParams(FirstParam) Closure<Boolean> predicate) { (1)
if (predicate.call(p)) {
// send invite
// ...
}
}
inviteIf(p) { (2)
it.age >= 18
}
1 | 闭包参数用 @ClosureParams 注解 |
2 | 无需为 it 使用显式类型,它会自动推断 |
@ClosureParams
注解最少接受一个参数,该参数名为 类型提示。类型提示是一个类,负责在编译时为闭包完成类型信息。在此示例中,使用的类型提示是 groovy.transform.stc.FirstParam
,它指示类型检查器闭包将接受一个参数,其类型是方法的第一个参数的类型。在这种情况下,方法的第一个参数是 Person
,因此它指示类型检查器闭包的第一个参数实际上是 Person
。
第二个可选参数名为 options。其语义取决于 类型提示 类。Groovy 附带了各种捆绑的类型提示,如下表所示
类型提示 | 多态的? | 描述和示例 |
---|---|---|
|
不 |
方法的第一个(分别为第二个、第三个)参数类型
|
|
不 |
方法的第一个(分别为第二个、第三个)参数的第一个泛型类型
|
|
不 |
一种类型提示,其闭包参数的类型来自 options 字符串。
此类型提示支持 单个 签名,每个参数都使用完全限定类型名或基本类型作为 options 数组的值指定。 |
|
是 |
一个专门用于闭包的类型提示,闭包要么处理单个参数的
此类型提示 要求 第一个参数是 |
|
是 |
从某些类型的抽象方法推断闭包参数类型。为 每个 抽象方法推断一个签名。
如果像上面的例子中那样存在多个签名,那么只有当每个方法的元数不同时,类型检查器才能够推断出参数的类型。在上面的例子中, |
|
是 |
从 接受
一个多态闭包,接受
一个多态闭包,接受
|
即使您使用 FirstParam 、SecondParam 或 ThirdParam 作为类型提示,这并不严格意味着将传递给闭包的参数 将是 方法调用的第一个(分别为第二个、第三个)参数。这仅意味着闭包参数的 类型 将与方法调用的第一个(分别为第二个、第三个)参数的类型 相同。 |
简而言之,在接受 Closure
的方法上缺少 @ClosureParams
注解 不会 导致编译失败。如果存在(并且它可以在 Java 源代码和 Groovy 源代码中存在),那么类型检查器将拥有 更多 信息,并且可以执行额外的类型推断。这使得此功能对于框架开发人员特别有趣。
第三个可选参数名为 conflictResolutionStrategy。它可以引用一个类(扩展自 ClosureSignatureConflictResolver
),该类可以在初始推断计算完成后,如果找到多个参数类型,则执行额外的参数类型解析。Groovy 附带了一个默认的类型解析器,它什么也不做,另一个解析器如果找到多个签名则选择第一个签名。解析器仅在找到多个签名时调用,并且在设计上是一个后处理器。任何需要注入类型信息的语句都必须通过类型提示确定的参数签名之一。然后解析器在返回的候选签名中进行选择。
@DelegatesTo
@DelegatesTo
注解被类型检查器用来推断委托的类型。它允许 API 设计者指示编译器委托的类型和委托策略。@DelegatesTo
注解在特定部分中讨论。
静态编译
动态与静态
在类型检查部分,我们已经看到 Groovy 通过 @TypeChecked
注解提供了可选的类型检查。类型检查器在编译时运行,并对动态代码执行静态分析。无论是否启用类型检查,程序的行为都将完全相同。这意味着 @TypeChecked
注解对于程序的语义是中立的。尽管可能需要在源代码中添加类型信息以使程序被认为是类型安全的,但最终,程序的语义是相同的。
虽然这听起来不错,但实际上有一个问题:编译时对动态代码进行的类型检查,根据定义,只有在没有发生运行时特定行为时才正确。例如,以下程序通过了类型检查
class Computer {
int compute(String str) {
str.length()
}
String compute(int x) {
String.valueOf(x)
}
}
@groovy.transform.TypeChecked
void test() {
def computer = new Computer()
computer.with {
assert compute(compute('foobar')) =='6'
}
}
有两个 compute
方法。一个接受 String
并返回 int
,另一个接受 int
并返回 String
。如果编译此代码,则认为其类型安全:内部的 compute('foobar')
调用将返回一个 int
,而在此 int
上调用 compute
将反过来返回一个 String
。
现在,在调用 test()
之前,考虑添加以下行
Computer.metaClass.compute = { String str -> new Date() }
通过运行时元编程,我们实际上正在修改 compute(String)
方法的行为,使其不再返回所提供参数的长度,而是返回一个 Date
。如果执行程序,它将在运行时失败。由于此行可以从任何地方、任何线程添加,因此类型检查器绝对无法静态地确保不会发生此类情况。简而言之,类型检查器容易受到猴子补丁的影响。这只是一个例子,但这说明了对动态程序进行静态分析本质上是错误的概念。
Groovy 语言提供了 @TypeChecked
的替代注解,它实际上可以确保推断为被调用的方法 将 在运行时有效被调用。这个注解将 Groovy 编译器变成一个 静态编译器,其中所有方法调用都在编译时解析 并且 生成的字节码确保了这一点:该注解是 @groovy.transform.CompileStatic
。
@CompileStatic
注解
@CompileStatic
注解可以添加到任何可以使用 @TypeChecked
注解的地方,即类或方法。不需要同时添加 @TypeChecked
和 @CompileStatic
,因为 @CompileStatic
会执行 @TypeChecked
所做的一切,此外还会触发静态编译。
让我们看一个失败的例子,但这次我们用 @CompileStatic
替换 @TypeChecked
注解
class Computer {
int compute(String str) {
str.length()
}
String compute(int x) {
String.valueOf(x)
}
}
@groovy.transform.CompileStatic
void test() {
def computer = new Computer()
computer.with {
assert compute(compute('foobar')) =='6'
}
}
Computer.metaClass.compute = { String str -> new Date() }
test()
这是 唯一 的区别。如果我们执行这个程序,这次没有运行时错误。test
方法对猴子补丁免疫了,因为在其主体中调用的 compute
方法在编译时链接,所以即使 Computer
的元类发生变化,程序仍然按照 类型检查器的预期 运行。
主要优点
在代码中使用 @CompileStatic
有几个好处
-
类型安全
-
对猴子补丁免疫
-
性能提升
性能提升取决于您正在执行的程序类型。如果它是 I/O 密集型,则静态编译代码和动态代码之间的差异几乎不明显。在 CPU 密集型代码上,由于生成的字节码与 Java 为等效程序生成的字节码非常接近(如果不是相同),因此性能大大提高。
使用 Groovy 的 invokedynamic 版本,对于使用 JDK 7 及更高版本的人来说,动态代码的性能应该非常接近静态编译代码的性能。有时,它甚至可能更快!只有一种方法可以确定您应该选择哪个版本:测量。原因在于,根据您的程序 和 您使用的 JVM,性能可能会有显著差异。特别是,Groovy 的 invokedynamic 版本对使用的 JVM 版本非常敏感。 |
1.6.7. 类型检查扩展
编写类型检查扩展
迈向更智能的类型检查器
尽管 Groovy 是一种动态语言,但它可以通过 @TypeChecked
注解在编译时与静态类型检查器一起使用。在此模式下,编译器变得更加冗长,并抛出错误,例如拼写错误、不存在的方法等。但这也有一些限制,其中大部分来自于 Groovy 本质上仍然是一种动态语言的事实。例如,您将无法对使用标记构建器的代码进行类型检查
def builder = new MarkupBuilder(out)
builder.html {
head {
// ...
}
body {
p 'Hello, world!'
}
}
在前面的示例中,html
、head
、body
或 p
方法都不存在。但是,如果您执行代码,它会起作用,因为 Groovy 使用动态调度并在运行时转换这些方法调用。在此构建器中,对可以使用多少标签以及属性没有限制,这意味着类型检查器无法在编译时知道所有可能的方法(标签),除非您专门为 HTML 创建一个构建器。
Groovy 是实现内部 DSL 的理想平台。灵活的语法,结合运行时和编译时元编程功能,使 Groovy 成为一个有趣的选择,因为它允许程序员专注于 DSL 而不是工具或实现。由于 Groovy DSL 是 Groovy 代码,因此无需编写专用插件即可轻松获得 IDE 支持。
在许多情况下,DSL 引擎是用 Groovy(或 Java)编写的,然后用户代码作为脚本执行,这意味着您在用户逻辑之上有一种包装器。该包装器可以例如由 GroovyShell
或 GroovyScriptEngine
组成,它们在运行脚本之前透明地执行一些任务(添加导入、应用 AST 转换、扩展基本脚本等)。通常,用户编写的脚本在没有测试的情况下投入生产,因为 DSL 逻辑达到了 任何 用户都可以使用 DSL 语法编写代码的地步。最终,用户可能只是忽略他们编写的实际上是 代码。这给 DSL 实现者带来了一些挑战,例如保护用户代码的执行,或者在这种情况下,尽早报告错误。
例如,想象一个旨在远程驾驶火星探测器的 DSL。向探测器发送一条消息大约需要 15 分钟。如果探测器执行脚本并因错误(例如拼写错误)而失败,您会遇到两个问题
-
首先,反馈仅在 30 分钟后到达(探测器接收脚本所需的时间以及接收错误所需的时间)
-
其次,脚本的一部分已经被执行,您可能需要显著更改已修复的脚本(这意味着您需要知道探测器的当前状态……)
类型检查扩展是一种机制,它允许 DSL 引擎的开发者通过应用与静态类型检查在常规 Groovy 类上允许的相同类型的检查,使这些脚本更安全。
这里的原则是尽早失败,也就是说,尽快失败脚本的编译,并尽可能向用户提供反馈(包括友好的错误消息)。
简而言之,类型检查扩展背后的想法是让编译器了解 DSL 使用的所有运行时元编程技巧,以便脚本可以受益于与冗长静态编译代码相同的编译时检查级别。我们将看到,您甚至可以更进一步,执行正常类型检查器不会执行的检查,为您的用户提供强大的编译时检查。
扩展属性
@TypeChecked
注解支持一个名为 extensions
的属性。此参数接受一个字符串数组,对应于 类型检查扩展脚本 列表。这些脚本在 编译时 在类路径上找到。例如,您可以编写
@TypeChecked(extensions='/path/to/myextension.groovy')
void foo() { ...}
在这种情况下,foo 方法将根据正常类型检查器的规则进行类型检查,并由 myextension.groovy 脚本中的规则补充。请注意,虽然类型检查器内部支持多种机制来实现类型检查扩展(包括纯 Java 代码),但建议的方式是使用这些类型检查扩展脚本。
类型检查的 DSL
类型检查扩展背后的理念是使用 DSL 来扩展类型检查器的功能。此 DSL 允许您使用“事件驱动”API 挂钩到编译过程,更具体地说是类型检查阶段。例如,当类型检查器进入方法体时,它会抛出一个 beforeVisitMethod 事件,扩展可以对此做出反应
beforeVisitMethod { methodNode ->
println "Entering ${methodNode.name}"
}
想象一下您手头有这个漫游车 DSL。用户会编写
robot.move 100
如果您有一个类定义如下
class Robot {
Robot move(int qt) { this }
}
脚本可以在执行前使用以下脚本进行类型检查
def config = new CompilerConfiguration()
config.addCompilationCustomizers(
new ASTTransformationCustomizer(TypeChecked) (1)
)
def shell = new GroovyShell(config) (2)
def robot = new Robot()
shell.setVariable('robot', robot)
shell.evaluate(script) (3)
1 | 编译器配置将 @TypeChecked 注解添加到所有类 |
2 | 在 GroovyShell 中使用配置 |
3 | 这样使用 shell 编译的脚本会在没有用户显式添加的情况下用 @TypeChecked 编译 |
使用上面的编译器配置,我们可以透明地将 @TypeChecked 应用到脚本中。在这种情况下,它将在编译时失败
[Static type checking] - The variable [robot] is undeclared.
现在,我们将稍微更新配置,以包含 ``extensions'' 参数
config.addCompilationCustomizers(
new ASTTransformationCustomizer(
TypeChecked,
extensions:['robotextension.groovy'])
)
然后将以下内容添加到您的类路径中
unresolvedVariable { var ->
if ('robot'==var.name) {
storeType(var, classNodeFor(Robot))
handled = true
}
}
在这里,我们告诉编译器,如果找到 未解析的变量,并且变量的名称是 robot,那么我们可以确保该变量的类型是 Robot
。
类型检查扩展 API
类型检查 API 是一个底层 API,处理抽象语法树。即使 DSL 使其比直接处理纯 Java 或 Groovy 中的 AST 代码容易得多,您也必须非常了解 AST 才能开发扩展。
类型检查器发送以下事件,扩展脚本可以对此做出反应
事件名称 |
setup |
调用时机 |
在类型检查器完成初始化后调用 |
参数 |
无 |
用法 |
可用于执行扩展的设置 |
事件名称 |
finish |
调用时机 |
类型检查器完成类型检查后调用 |
参数 |
无 |
用法 |
可用于在类型检查器完成其工作后执行额外的检查。 |
事件名称 |
unresolvedVariable |
调用时机 |
当类型检查器发现未解析的变量时调用 |
参数 |
VariableExpression vexp |
用法 |
允许开发者使用用户注入的变量帮助类型检查器。 |
事件名称 |
unresolvedProperty |
调用时机 |
当类型检查器无法在接收器上找到属性时调用 |
参数 |
PropertyExpression pexp |
用法 |
允许开发者处理“动态”属性 |
事件名称 |
unresolvedAttribute |
调用时机 |
当类型检查器无法在接收器上找到属性时调用 |
参数 |
AttributeExpression aexp |
用法 |
允许开发者处理缺失的属性 |
事件名称 |
beforeMethodCall |
调用时机 |
在类型检查器开始类型检查方法调用之前调用 |
参数 |
MethodCall call |
用法 |
允许您在类型检查器执行自己的检查之前拦截方法调用。如果您想用自定义检查替换默认类型检查(在有限范围内),这将很有用。在这种情况下,您必须将 handled 标志设置为 true,以便类型检查器跳过自己的检查。 |
事件名称 |
afterMethodCall |
调用时机 |
类型检查器完成方法调用类型检查后调用 |
参数 |
MethodCall call |
用法 |
允许您在类型检查器完成自己的检查后执行额外的检查。如果需要执行标准类型检查测试,同时还要确保额外的类型安全,例如检查参数之间的兼容性,这将特别有用。请注意,即使您调用了 |
事件名称 |
onMethodSelection |
调用时机 |
当类型检查器找到适合方法调用的方法时调用 |
参数 |
Expression expr, MethodNode node |
用法 |
类型检查器通过推断方法调用的参数类型来工作,然后选择一个目标方法。如果它找到了一个对应的方法,那么它会触发此事件。例如,如果您想对特定的方法调用做出反应,例如进入一个接受闭包作为参数的方法的范围(如构建器),这会很有趣。请注意,此事件可能针对各种类型的表达式抛出,而不仅仅是方法调用(例如二进制表达式)。 |
事件名称 |
methodNotFound |
调用时机 |
当类型检查器无法找到适合方法调用的方法时调用 |
参数 |
ClassNode receiver, String name, ArgumentListExpression argList, ClassNode[] argTypes,MethodCall call |
用法 |
与 |
事件名称 |
beforeVisitMethod |
调用时机 |
类型检查器在类型检查方法体之前调用 |
参数 |
MethodNode node |
用法 |
类型检查器将在开始类型检查方法体之前调用此方法。例如,如果您想自行执行类型检查而不是让类型检查器执行,则必须将 handled 标志设置为 true。此事件还可用于帮助定义扩展的范围(例如,仅当您在方法 foo 内部时才应用它)。 |
事件名称 |
afterVisitMethod |
调用时机 |
类型检查器在类型检查方法体后调用 |
参数 |
MethodNode node |
用法 |
让您有机会在类型检查器访问方法体后执行额外的检查。这在您收集信息(例如)并希望在收集所有信息后执行额外检查时很有用。 |
事件名称 |
beforeVisitClass |
调用时机 |
类型检查器在类型检查类之前调用 |
参数 |
ClassNode node |
用法 |
如果一个类被类型检查,那么在访问该类之前,将发送此事件。对于在用 |
事件名称 |
afterVisitClass |
调用时机 |
类型检查器完成对类型检查类的访问后调用 |
参数 |
ClassNode node |
用法 |
在类型检查器完成其工作后,针对每个正在进行类型检查的类调用。这包括用 |
事件名称 |
incompatibleAssignment |
调用时机 |
当类型检查器认为赋值不正确时调用,这意味着赋值的右侧与左侧不兼容 |
参数 |
ClassNode lhsType, ClassNode rhsType, Expression assignment |
用法 |
允许开发人员处理不正确的赋值。例如,当一个类重写 |
事件名称 |
incompatibleReturnType |
调用时机 |
当类型检查器认为返回值与封闭闭包或方法的返回类型不兼容时调用 |
参数 |
ReturnStatement statement, ClassNode valueType |
用法 |
允许开发人员处理不正确的返回值。例如,当返回值将进行隐式转换或封闭闭包的目标类型难以正确推断时,这很有用。在这种情况下,您可以通过告诉类型检查器赋值有效(通过设置 |
事件名称 |
ambiguousMethods |
调用时机 |
当类型检查器无法在多个候选方法之间进行选择时调用 |
参数 |
List<MethodNode> methods, Expression origin |
用法 |
允许开发人员处理不正确的赋值。例如,当一个类重写 |
当然,一个扩展脚本可以由多个块组成,并且您可以有多个块响应同一个事件。这使得 DSL 看起来更好,更容易编写。然而,响应事件远远不够。如果您知道可以响应事件,您还需要处理错误,这意味着需要几个 辅助 方法来使事情更容易。
使用扩展
DSL 依赖于一个支持类,即 org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport。这个类本身继承自 org.codehaus.groovy.transform.stc.TypeCheckingExtension。这两个类定义了许多 *辅助* 方法,这些方法将使使用 AST 变得更容易,尤其是在类型检查方面。一个有趣的事情是,你**可以访问类型检查器**。这意味着你可以以编程方式调用类型检查器的方法,包括那些允许你**抛出编译错误**的方法。
扩展脚本委托给 org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport 类,这意味着你可以直接访问以下变量:
-
context: 类型检查器上下文,类型为 org.codehaus.groovy.transform.stc.TypeCheckingContext
-
typeCheckingVisitor: 类型检查器本身,一个 org.codehaus.groovy.transform.stc.StaticTypeCheckingVisitor 实例
-
generatedMethods: “生成的方法”列表,实际上是你可以使用
newMethod
调用在类型检查扩展中创建的“虚拟”方法列表
类型检查上下文包含许多对类型检查器有用的信息。例如,当前包含方法调用、二进制表达式、闭包等的堆栈……如果你必须知道错误发生时你*在哪里*以及你想如何处理它,这些信息尤其重要。
除了 GroovyTypeCheckingExtensionSupport
和 StaticTypeCheckingVisitor
提供的功能之外,类型检查 DSL 脚本还导入了 org.codehaus.groovy.ast.ClassHelper 和 org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport 的静态成员,从而通过 OBJECT_TYPE
、STRING_TYPE
、THROWABLE_TYPE
等访问常见类型,并进行 missesGenericsTypes(ClassNode)
、isClassClassNodeWrappingConcreteType(ClassNode)
等检查。
当使用类型检查扩展时,处理类节点需要特别注意。编译使用抽象语法树 (AST),并且当你类型检查一个类时,树可能不完整。这也意味着当你引用类型时,你不能使用类字面量,例如 String
或 HashSet
,而是要使用表示这些类型的类节点。这需要一定程度的抽象和理解 Groovy 如何处理类节点。为了简化操作,Groovy 提供了几个辅助方法来处理类节点。例如,如果你想表示“String 的类型”,你可以这样写:
assert classNodeFor(String) instanceof ClassNode
你还会注意到 classNodeFor 有一个接受 String
作为参数而不是 Class
的变体。通常,你**不应该**使用那个,因为它会创建一个名称为 String
的类节点,但其上没有定义任何方法、任何属性……第一个版本返回一个*已解析*的类节点,而第二个版本返回一个*未解析*的类节点。因此,后者应保留用于非常特殊的情况。
你可能会遇到的第二个问题是引用尚未编译的类型。这可能比你想象的更频繁地发生。例如,当你同时编译一组文件时。在这种情况下,如果你想说“那个变量是 Foo 类型”,但 Foo
尚未编译,你仍然可以使用 lookupClassNodeFor
引用 Foo
类节点。
assert lookupClassNodeFor('Foo') instanceof ClassNode
假设你知道变量 foo
的类型是 Foo
,并且你想告诉类型检查器。那么你可以使用 storeType
方法,它接受两个参数:第一个是你想要存储类型的节点,第二个是节点的类型。如果你查看 storeType
的实现,你会发现它委托给类型检查器对应的等效方法,该方法本身做了大量工作来存储节点元数据。你还会发现存储类型不限于变量:你可以设置任何表达式的类型。
同样,获取 AST 节点的类型只需在该节点上调用 getType
。这通常是你想要的,但你必须理解一些事情:
-
getType
返回表达式的**推断类型**。这意味着对于声明为Object
类型的变量,它不会返回Object
的类节点,而是返回该变量**在代码当前点**的推断类型(流式类型) -
如果你想访问变量(或字段/参数)的原始类型,那么你必须在 AST 节点上调用适当的方法
要抛出类型检查错误,你只需调用 addStaticTypeError
方法,它接受两个参数:
-
一个 *message*,这是一个将显示给最终用户的字符串
-
导致错误的 *AST 节点*。最好提供最合适的 AST 节点,因为它将用于检索行号和列号。
通常需要知道 AST 节点的类型。为了可读性,DSL 提供了一个特殊的 isXXXExpression 方法,该方法将委托给 x instance of XXXExpression
。例如,你可以直接写:
if (node instanceof BinaryExpression) {
...
}
你只需这样写:
if (isBinaryExpression(node)) {
...
}
当你对动态代码进行类型检查时,你经常会遇到这样的情况:你知道一个方法调用是有效的,但其背后并没有“真实”的方法。以 Grails 动态查找器为例。你可能有一个名为 *findByName(…)* 的方法调用。由于 bean 中没有定义 *findByName* 方法,类型检查器会报错。然而,你知道这个方法在运行时不会失败,你甚至可以告知这个方法的返回类型。对于这种情况,DSL 支持两种特殊的构造,它们由 *phantom methods* 组成。这意味着你将返回一个实际上不存在但已在类型检查上下文中定义的方法节点。存在三个方法:
-
newMethod(String name, Class returnType)
-
newMethod(String name, ClassNode returnType)
-
newMethod(String name, Callable<ClassNode> return Type)
这三个变体都做同样的事情:它们创建一个新的方法节点,其名称是提供的名称,并定义此方法的返回类型。此外,类型检查器会将这些方法添加到 generatedMethods
列表中(参见下面的 isGenerated
)。我们只设置名称和返回类型的原因是,在 90% 的情况下,这正是你所需要的。例如,在上面的 findByName
示例中,你唯一需要知道的是 findByName
在运行时不会失败,并且它返回一个域类。返回类型的 Callable
版本很有趣,因为它将返回类型的计算推迟到类型检查器实际需要它时。这很有趣,因为在某些情况下,当类型检查器要求时,你可能不知道实际的返回类型,因此你可以使用一个闭包,每当类型检查器在此方法节点上调用 getReturnType
时,该闭包都会被调用。如果将其与延迟检查结合使用,你可以实现非常复杂的类型检查,包括处理前向引用。
newMethod(name) {
// each time getReturnType on this method node will be called, this closure will be called!
println 'Type checker called me!'
lookupClassNodeFor(Foo) // return type
}
如果您需要除了名称和返回类型之外的更多信息,您可以随时自行创建一个新的 MethodNode
。
作用域在 DSL 类型检查中非常重要,也是我们无法使用基于*切入点*的方法进行 DSL 类型检查的原因之一。基本上,你必须能够非常精确地定义你的扩展何时适用,何时不适用。此外,你必须能够处理常规类型检查器无法处理的情况,例如前向引用。
point a(1,1)
line a,b // b is referenced afterwards!
point b(5,2)
例如,假设你要处理一个构建器
builder.foo {
bar
baz(bar)
}
那么,你的扩展应该只在进入 foo
方法后才激活,并在该范围之外不激活。但你可能会遇到复杂的情况,比如同一个文件中存在多个构建器或嵌入式构建器(构建器中嵌套构建器)。虽然你不应该试图从一开始就解决所有这些问题(你必须接受类型检查的限制),但类型检查器确实提供了一个很好的机制来处理这个问题:一个作用域堆栈,使用 newScope
和 scopeExit
方法。
-
newScope
创建一个新的作用域并将其推到堆栈顶部 -
scopeExits
从堆栈中弹出一个作用域
一个作用域包括:
-
一个父作用域
-
一个自定义数据映射
如果你想查看实现,它只是一个 LinkedHashMap
(org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport.TypeCheckingScope),但它功能强大。例如,你可以使用这样一个作用域来存储一个闭包列表,以便在退出作用域时执行。这就是你处理前向引用的方式:
def scope = newScope()
scope.secondPassChecks = []
//...
scope.secondPassChecks << { println 'executed later' }
// ...
scopeExit {
secondPassChecks*.run() // execute deferred checks
}
也就是说,如果在某个点你无法确定表达式的类型,或者你无法在该点检查赋值是否有效,你仍然可以在以后进行检查……这是一个非常强大的功能。现在,newScope
和 scopeExit
提供了一些有趣的语法糖:
newScope {
secondPassChecks = []
}
在 DSL 中,您可以随时使用 getCurrentScope()
或更简单地使用 currentScope
访问当前范围。
//...
currentScope.secondPassChecks << { println 'executed later' }
// ...
那么,一般模式将是:
-
确定一个*切入点*,在此处将新作用域推入堆栈并在该作用域内初始化自定义变量。
-
利用各种事件,你可以使用存储在自定义作用域中的信息来执行检查、延迟检查等。
-
确定一个*切入点*,在此处退出作用域,调用
scopeExit
并最终执行额外的检查
有关辅助方法的完整列表,请参阅 org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport 和 org.codehaus.groovy.transform.stc.TypeCheckingExtension 类。但是,请特别注意以下方法:
-
isDynamic
:接受一个 VariableExpression 作为参数,如果变量是 DynamicExpression,则返回 true,这意味着在脚本中,它没有使用类型或def
定义。 -
isGenerated
:接受一个 MethodNode 作为参数,并判断该方法是否是类型检查器扩展使用newMethod
方法生成的 -
isAnnotatedBy
:接受一个 AST 节点和一个 Class(或 ClassNode),并判断该节点是否被此 Class 注解。例如:isAnnotatedBy(node, NotNull)
-
getTargetMethod
:接受一个方法调用作为参数,并返回类型检查器为其确定的MethodNode
-
delegatesTo
:模拟@DelegatesTo
注解的行为。它允许你指定参数将委托给特定类型(你还可以指定委托策略)
高级类型检查扩展
预编译类型检查扩展
以上所有示例都使用类型检查脚本。它们以源代码形式存在于类路径中,这意味着:
-
与类型检查扩展对应的 Groovy 源文件在编译类路径上可用。
-
此文件由 Groovy 编译器为每个正在编译的源单元编译(通常,一个源单元对应一个文件)
这是一种非常方便的开发类型检查扩展的方式,但它意味着编译阶段会变慢,因为每次编译每个文件时,扩展本身也会被编译。因此,依赖预编译的扩展可能更实用。你有两种选择:
-
用 Groovy 编写扩展,编译它,然后使用对扩展类的引用而不是源
-
用 Java 编写扩展,编译它,然后使用对扩展类的引用
用 Groovy 编写类型检查扩展是最简单的途径。基本上,其思想是类型检查扩展脚本成为类型检查扩展类主方法的主体,如下所示:
import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
class PrecompiledExtension extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL { (1)
@Override
Object run() { (2)
unresolvedVariable { var ->
if ('robot'==var.name) {
storeType(var, classNodeFor(Robot)) (3)
handled = true
}
}
}
}
1 | 继承 TypeCheckingDSL 类是最简单的 |
2 | 然后扩展代码需要放在 run 方法内部 |
3 | 你可以使用与源代码形式编写的扩展完全相同的事件。 |
设置扩展非常类似于使用源代码形式的扩展。
config.addCompilationCustomizers(
new ASTTransformationCustomizer(
TypeChecked,
extensions:['typing.PrecompiledExtension'])
)
不同之处在于,您只需指定预编译扩展的完全限定类名,而不是使用类路径中的路径。
如果您真的想用 Java 编写扩展,那么您将无法从类型检查扩展 DSL 中受益。上面的扩展可以这样用 Java 重写:
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.transform.stc.AbstractTypeCheckingExtension;
import org.codehaus.groovy.transform.stc.StaticTypeCheckingVisitor;
public class PrecompiledJavaExtension extends AbstractTypeCheckingExtension { (1)
public PrecompiledJavaExtension(final StaticTypeCheckingVisitor typeCheckingVisitor) {
super(typeCheckingVisitor);
}
@Override
public boolean handleUnresolvedVariableExpression(final VariableExpression vexp) { (2)
if ("robot".equals(vexp.getName())) {
storeType(vexp, ClassHelper.make(Robot.class));
setHandled(true);
return true;
}
return false;
}
}
1 | 扩展 AbstractTypeCheckingExtension 类 |
2 | 然后根据需要重写 handleXXX 方法 |
在类型检查扩展中使用 @Grab
在类型检查扩展中使用 @Grab
注解是完全可能的。这意味着你可以包含只在编译时可用的库。在这种情况下,你必须明白,这将显著增加编译时间(至少,第一次抓取依赖项时)。
共享或打包类型检查扩展
类型检查扩展只是一个需要位于类路径上的脚本。因此,您可以直接共享它,或者将其打包到添加到类路径的 jar 文件中。
全局类型检查扩展
虽然您可以将编译器配置为透明地将类型检查扩展添加到您的脚本中,但目前没有办法仅仅通过将其放在类路径上就透明地应用扩展。
类型检查扩展与 @CompileStatic
类型检查扩展与 @TypeChecked
一起使用,但也可以与 @CompileStatic
一起使用。但是,您必须注意:
-
与
@CompileStatic
一起使用的类型检查扩展通常不足以让编译器知道如何从“不安全”代码生成静态可编译代码 -
可以与
@CompileStatic
一起使用类型检查扩展来增强类型检查,也就是说,引入**更多**编译错误,而无需实际处理动态代码
我们来解释第一点,即即使您使用扩展,编译器也无法知道如何静态编译您的代码:从技术上讲,即使您告诉类型检查器动态变量的类型,例如,它也无法知道如何编译它。是 getBinding('foo')
吗,是 getProperty('foo')
吗,还是 delegate.getFoo()
等等?即使您使用类型检查扩展(它只会再次提供类型提示),也绝对没有直接的方法来告诉静态编译器如何编译这样的代码。
针对这个特殊示例,一种可能的解决方案是指示编译器使用混合模式编译。更高级的解决方案是使用类型检查期间的 AST 转换,但这要复杂得多。
类型检查扩展允许您在类型检查器失败时提供帮助,但也允许您在类型检查器未失败时使其失败。在这种情况下,支持 @CompileStatic
的扩展也是有意义的。想象一个能够类型检查 SQL 查询的扩展。在这种情况下,该扩展在动态和静态上下文中都将有效,因为没有该扩展,代码仍然可以通过。
混合模式编译
在上一节中,我们强调了您可以通过 @CompileStatic
激活类型检查扩展的事实。在这种情况下,类型检查器将不再抱怨一些未解析的变量或未知的方法调用,但它仍然不知道如何静态编译它们。
混合模式编译提供了第三种方式,即指示编译器,当发现未解析的变量或方法调用时,它应该回退到动态模式。这可以通过类型检查扩展和特殊的 makeDynamic
调用实现。
为了说明这一点,让我们回到 Robot
示例:
robot.move 100
让我们尝试使用 @CompileStatic
而不是 @TypeChecked
激活我们的类型检查扩展
def config = new CompilerConfiguration()
config.addCompilationCustomizers(
new ASTTransformationCustomizer(
CompileStatic, (1)
extensions:['robotextension.groovy']) (2)
)
def shell = new GroovyShell(config)
def robot = new Robot()
shell.setVariable('robot', robot)
shell.evaluate(script)
1 | 透明地应用 @CompileStatic |
2 | 激活类型检查扩展 |
脚本将正常运行,因为静态编译器已得知 robot
变量的类型,因此它能够直接调用 move
。但在此之前,编译器是如何知道如何获取 robot
变量的呢?实际上,在类型检查扩展中,默认情况下,对未解析的变量设置 handled=true
将自动触发动态解析,因此在这种情况下,您无需做任何特殊操作即可让编译器使用混合模式。但是,让我们稍微更新一下我们的示例,从机器人脚本开始:
move 100
这里你可以注意到不再有对 robot
的引用。我们的扩展将无法提供帮助,因为我们将无法指示编译器 move
是在 Robot
实例上完成的。这个代码示例可以通过 groovy.util.DelegatingScript 的帮助完全以动态方式执行:
def config = new CompilerConfiguration()
config.scriptBaseClass = 'groovy.util.DelegatingScript' (1)
def shell = new GroovyShell(config)
def runner = shell.parse(script) (2)
runner.setDelegate(new Robot()) (3)
runner.run() (4)
1 | 我们将编译器配置为使用 DelegatingScript 作为基类 |
2 | 脚本源需要解析,并将返回一个 DelegatingScript 实例 |
3 | 然后我们可以调用 setDelegate 来使用 Robot 作为脚本的委托 |
4 | 然后执行脚本。move 将直接在委托上执行 |
如果我们想让它通过 @CompileStatic
,我们必须使用类型检查扩展,所以让我们更新我们的配置:
config.addCompilationCustomizers(
new ASTTransformationCustomizer(
CompileStatic, (1)
extensions:['robotextension2.groovy']) (2)
)
1 | 透明地应用 @CompileStatic |
2 | 使用旨在识别对 move 调用的替代类型检查扩展。 |
在上一节中,我们已经学习了如何处理无法识别的方法调用,因此我们能够编写此扩展:
methodNotFound { receiver, name, argList, argTypes, call ->
if (isMethodCallExpression(call) (1)
&& call.implicitThis (2)
&& 'move'==name (3)
&& argTypes.length==1 (4)
&& argTypes[0] == classNodeFor(int) (5)
) {
handled = true (6)
newMethod('move', classNodeFor(Robot)) (7)
}
}
1 | 如果调用是方法调用(而不是静态方法调用) |
2 | 此调用是在“隐式 this”上进行的(没有显式的 this. ) |
3 | 被调用的方法是 move |
4 | 且该调用带有一个参数 |
5 | 且该参数是 int 类型 |
6 | 然后告诉类型检查器该调用是有效的 |
7 | 并且调用的返回类型是 Robot |
如果你尝试执行此代码,你可能会惊讶地发现它在运行时实际失败了。
java.lang.NoSuchMethodError: java.lang.Object.move()Ltyping/Robot;
原因很简单:虽然类型检查扩展足以满足 @TypeChecked
(不涉及静态编译),但对于 @CompileStatic
来说却不够,后者需要额外信息。在这种情况下,你告诉编译器方法存在,但你没有向它解释该方法实际上**是**什么方法,以及消息的接收者(委托)是什么。
解决这个问题非常容易,只需将 newMethod
调用替换为其他东西即可:
methodNotFound { receiver, name, argList, argTypes, call ->
if (isMethodCallExpression(call)
&& call.implicitThis
&& 'move'==name
&& argTypes.length==1
&& argTypes[0] == classNodeFor(int)
) {
makeDynamic(call, classNodeFor(Robot)) (1)
}
}
1 | 告诉编译器该调用应动态进行 |
makeDynamic
调用执行 3 项操作:
-
它返回一个虚拟方法,就像
newMethod
一样 -
自动为您将
handled
标志设置为true
-
但也将
call
标记为动态执行。
因此,当编译器需要为 move
调用生成字节码时,由于它现在被标记为动态调用,它将回退到动态编译器并让它处理该调用。而且,由于扩展告诉我们动态调用的返回类型是 Robot
,后续调用将静态执行!
有些人会想,为什么静态编译器默认不这样做,而不需要扩展。这是一个设计决策:
-
如果代码是静态编译的,我们通常希望类型安全和最佳性能
-
因此,如果未识别的变量/方法调用被动态化,你将失去类型安全,同时也将失去编译时对拼写错误的所有支持!
简而言之,如果您想要混合模式编译,它**必须**通过类型检查扩展明确指定,以便编译器和 DSL 设计者完全了解他们正在做什么。
makeDynamic
可用于 3 种 AST 节点:
-
一个方法节点(
MethodNode
) -
一个变量(
VariableExpression
) -
一个属性表达式(
PropertyExpression
)
如果这还不够,那就意味着无法直接进行静态编译,并且你必须依赖 AST 转换。
在扩展中转换 AST
从 AST 转换设计角度来看,类型检查扩展非常有吸引力:扩展可以访问推断类型等上下文,这通常很好用。并且扩展可以直接访问抽象语法树。既然你可以访问 AST,理论上没有什么能阻止你修改 AST。但是,我们不建议你这样做,除非你是高级 AST 转换设计者并且非常了解编译器内部结构。
-
首先,您将明确违反类型检查的契约,即只注解 AST。类型检查**不应**修改 AST 树,因为您将无法再保证没有 * @TypeChecked * 注解的代码在没有注解的情况下行为相同。
-
如果您的扩展旨在与 @CompileStatic 配合使用,那么您**可以**修改 AST,因为这正是 @CompileStatic 最终会做的事情。静态编译不能保证与动态 Groovy 相同的语义,因此用 @CompileStatic 编译的代码与用 @TypeChecked 编译的代码之间确实存在差异。您可以选择任何策略来更新 AST,但可能使用在类型检查之前运行的 AST 转换更容易。
-
如果您不能依赖在类型检查器之前启动的转换,那么您必须**非常**小心。
类型检查阶段是字节码生成之前编译器中运行的最后一个阶段。所有其他 AST 转换都在此之前运行,并且编译器在“修复”类型检查阶段之前生成的错误 AST 方面做得非常好。一旦您在类型检查期间执行转换,例如直接在类型检查扩展中,那么您就必须自己完成生成一个 100% 编译器兼容的抽象语法树的所有工作,这很容易变得复杂。这就是为什么如果您刚开始使用类型检查扩展和 AST 转换,我们不建议您走这条路。 |
示例
真实类型检查扩展的例子很容易找到。您可以下载 Groovy 的源代码,并查看 TypeCheckingExtensionsTest 类,该类链接到 各种扩展脚本。
一个复杂的类型检查扩展示例可以在 Markup Template Engine 源代码中找到:这个模板引擎依赖于类型检查扩展和 AST 转换将模板转换为完全静态编译的代码。源代码可以在这里找到。
2. 工具
2.1. 从命令行运行 Groovy
2.1.1. groovy,Groovy 命令
groovy
调用 Groovy 命令行处理器。它允许您运行内联 Groovy 表达式,以及 Groovy 文件中的脚本、测试或应用程序。它在 Java 世界中扮演的角色与 java
类似,但它处理内联脚本,并且通常通过脚本调用,而不是调用类文件,并且会根据需要自动调用 Groovy 编译器。
运行 Groovy 脚本、测试或应用程序最简单的方法是在 shell 提示符下运行以下命令:
> groovy MyScript.groovy
.groovy
部分是可选的。groovy
命令支持许多命令行开关:
短版本 | 长版本 | 描述 | 示例 |
---|---|---|---|
-a |
--autosplit <splitPattern> |
使用 splitPattern(默认“\s”)拆分行,使用隐式“split”变量 |
|
-b |
--basescript <class> |
脚本的基类名(必须派生自 Script) |
|
-c |
--encoding <charset> |
指定文件的编码 |
|
-cp <path> |
-classpath <path> |
指定编译类路径。必须是第一个参数。 |
groovy -cp lib/dep.jar MyScript |
--configscript <path> |
高级编译器配置脚本 |
groovy --configscript config/config.groovy src/Person.groovy |
|
-D |
--define <name=value> |
定义一个系统属性 |
|
-d |
--debug |
调试模式将打印完整的堆栈跟踪 |
|
--disableopt <optlist> |
禁用一个或所有优化元素。 |
||
-e <script> |
指定一个内联命令行脚本 |
groovy -e "println new Date()" |
|
-h |
--help |
显示命令行 groovy 命令的使用信息 |
groovy --help |
-i <extension> |
原地修改文件;如果给出扩展名(例如“.bak”),则创建备份。 |
||
-l <port> |
监听端口并处理入站行(默认:1960) |
||
-n |
使用隐式“line”变量逐行处理文件 |
||
-p |
逐行处理文件并打印结果(另请参阅 -n) |
||
-v |
--version |
显示 Groovy 和 JVM 版本 |
groovy -v |
-pa |
--parameters |
在 JDK 8 及更高版本上为方法参数名称的反射生成元数据。默认为 false。 |
groovy --parameters Person.groovy |
-pr |
--enable-preview |
启用预览 Java 功能(仅限 JDK12+)。 |
groovy --enable-preview Person.groovy |
2.2. 编译 Groovy
2.2.1. groovyc,Groovy 编译器
groovyc
是 Groovy 命令行编译器工具。它允许您将 Groovy 源文件编译成字节码。它在 Java 世界中扮演着与 javac
相同的角色。编译 Groovy 脚本或类最简单的方法是运行以下命令:
groovyc MyClass.groovy
这将生成一个 MyClass.class
文件(以及根据源文件内容的其他 .class 文件)。groovyc
支持许多命令行开关:
短版本 | 长版本 | 描述 | 示例 |
---|---|---|---|
-cp |
-classpath, --classpath |
指定编译类路径。必须是第一个参数。 |
groovyc -cp lib/dep.jar MyClass.groovy |
--sourcepath |
查找源文件的目录。不再使用。指定此参数将不起作用。 |
||
--temp |
编译器的临时目录 |
||
--encoding |
源文件的编码 |
groovyc --encoding utf-8 script.groovy |
|
--help |
显示命令行 groovyc 工具的帮助信息 |
groovyc --help |
|
-d |
指定生成类文件的位置。 |
groovyc -d target Person.groovy |
|
-v |
--version |
显示编译器版本 |
groovyc -v |
-e |
--exception |
编译错误时显示堆栈跟踪 |
groovyc -e script.groovy |
-j |
--jointCompilation* |
启用联合编译 |
groovyc -j A.groovy B.java |
-b |
--basescript |
脚本的基类名(必须派生自 Script) |
|
--configscript |
高级编译器配置脚本 |
groovyc --configscript config/config.groovy src/Person.groovy |
|
-Jproperty=value |
如果启用了联合编译,则传递给 |
groovyc -j -Jtarget=1.6 -Jsource=1.6 A.groovy B.java |
|
-Fflag |
如果启用了联合编译,则传递给 |
groovyc -j -Fnowarn A.groovy B.java |
|
-pa |
--parameters |
为方法参数名称上的反射生成元数据。需要 Java 8+。 |
groovyc --parameters Person.groovy |
-pr |
--enable-preview |
启用预览 Java 功能(仅限 JDK12+)。 |
groovy --enable-preview Person.groovy |
@argfile |
从指定文件读取选项和源文件。 |
groovyc @conf/args |
注意:* 有关联合编译的完整说明,请参阅联合编译部分。
2.2.2. Ant 任务
请参阅 groovyc Ant 任务 文档。它允许从 Apache Ant 调用 Groovy 编译器。
2.2.3. Gant
Gant 是一个用于使用 Groovy 而不是 XML 指定逻辑来脚本化 Ant 任务的工具。因此,它具有与 Groovyc Ant 任务完全相同的功能。
2.2.5. Maven 集成
有几种方法可以在 Maven 项目中编译 Groovy 代码。GMavenPlus 最灵活、功能最丰富,但与大多数 Groovy 编译器工具一样,它在联合 Java-Groovy 项目中可能遇到困难(原因与 GMaven 和 Gradle 可能出现问题的原因相同)。Maven 的 Groovy-Eclipse 编译器插件 规避了联合编译问题。阅读此处以深入讨论这两种方法的优缺点。
第三种方法是使用 Maven 的 Ant 插件来编译 Groovy 项目。请注意,在下面的示例中,Ant 插件绑定到构建的编译和测试编译阶段。它将在这些阶段被调用,并执行包含的任务,这些任务将通过源和测试目录运行 Groovy 编译器。生成的 Java 类将与从 Java 源编译的任何标准 Java 类共存并被视为相同,并且对 JRE 或 JUnit 运行时没有区别。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycomp.MyGroovy</groupId>
<artifactId>MyGroovy</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>Maven Example building a Groovy project</name>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.5.0</version>
<type>pom</type> <!-- required JUST since Groovy 2.5.0 -->
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<configuration>
<tasks>
<mkdir dir="${basedir}/src/main/groovy"/>
<taskdef name="groovyc"
classname="org.codehaus.groovy.ant.Groovyc">
<classpath refid="maven.compile.classpath"/>
</taskdef>
<mkdir dir="${project.build.outputDirectory}"/>
<groovyc destdir="${project.build.outputDirectory}"
srcdir="${basedir}/src/main/groovy/" listfiles="true">
<classpath refid="maven.compile.classpath"/>
</groovyc>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<configuration>
<tasks>
<mkdir dir="${basedir}/src/test/groovy"/>
<taskdef name="groovyc"
classname="org.codehaus.groovy.ant.Groovyc">
<classpath refid="maven.test.classpath"/>
</taskdef>
<mkdir dir="${project.build.testOutputDirectory}"/>
<groovyc destdir="${project.build.testOutputDirectory}"
srcdir="${basedir}/src/test/groovy/" listfiles="true">
<classpath refid="maven.test.classpath"/>
</groovyc>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
这假设您有一个 Maven 项目设置,其中 groovy
子文件夹与 Java 源和测试子文件夹同级。您可以使用 java
/jar
原型进行设置,然后将 Java 文件夹重命名为 Groovy,或者保留 Java 文件夹并仅创建 Groovy 同级文件夹。此外,还存在一个 Groovy 插件,但尚未经过测试或在生产中使用。在如上例中定义了构建部分后,您可以正常调用典型的 Maven 构建阶段。例如,mvn test
将执行测试阶段,编译 Groovy 源和 Groovy 测试源,最后执行单元测试。如果您运行 mvn jar
,它将执行 jar 阶段,在所有单元测试通过后将所有编译的生产类打包到 jar 中。有关 Maven 构建阶段的更多详细信息,请查阅 Maven2 文档。
GMaven 和 GMavenPlus
GMaven
GMaven 是原始的 Groovy Maven 插件,支持编译和脚本化 Groovy。
重要
您应该注意,GMaven **不再受支持**,并且在联合编译方面可能存在困难。GMavenPlus 是一个不错的替代品,但如果您在联合编译方面遇到问题,您可以考虑Groovy Eclipse Maven 插件。
GMavenPlus
GMavenPlus 是 GMaven 的重写版,并且正在积极开发中。它支持 GMaven 的大多数功能(一些显著的例外是 mojo Javadoc 标签 和对旧 Groovy 版本的支持)。它的联合编译使用存根(这意味着它与 GMaven 和 Gradle 具有相同的潜在问题)。与前身相比,它的主要优点是支持最新的 Groovy 版本、InvokeDynamic、Groovy on Android、GroovyDoc 和配置脚本。
Groovy Eclipse Maven 插件
Groovy-Eclipse 为 Maven 提供了一个编译器插件。使用该编译器插件,可以使用 Groovy-Eclipse 编译器编译您的 Maven 项目。其中一个在其他地方无法获得的功能是无存根联合编译。
2.2.6. 联合编译
联合编译意味着 Groovy 编译器将解析 Groovy 源文件,为所有这些文件创建存根,调用 Java 编译器编译存根以及 Java 源文件,然后以正常的 Groovy 编译器方式继续编译。这允许 Java 和 Groovy 文件无限制地混合使用。
联合编译可以通过命令行编译器使用 -j
标志启用,或者对于 Ant 任务,使用嵌套标签和所有属性以及所需的进一步嵌套标签。
需要注意的是,如果您不启用联合编译并尝试使用 Groovy 编译器编译 Java 源文件,Java 源文件将被编译为 Groovy 源文件。在某些情况下,这可能有效,因为大多数 Java 语法与 Groovy 兼容,但在某些地方语义可能不同。
2.2.7. Android 支持
用 Groovy 编写 Android 应用程序是可能的。但是,这需要一个特殊版本的编译器,这意味着您不能使用常规的 groovyc 工具 来针对 Android 字节码。特别是,Groovy 为 Android 提供了特定的 JAR 文件,其分类器为 grooid
。为了简化操作,一个 Gradle 插件 在 Android Gradle 工具链中添加了对 Groovy 语言的支持。
插件可以这样应用:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.2'
classpath 'org.codehaus.groovy:groovy-android-gradle-plugin:1.0.0'
}
}
apply plugin: 'groovyx.android'
然后你需要添加对 Groovy 编译器 grooid
版本的依赖:
dependencies {
compile 'org.codehaus.groovy:groovy:2.4.7:grooid'
}
请注意,如果 Groovy jar 不提供 grooid
分类器替代方案,则意味着该 jar 直接与 Android 兼容。在这种情况下,您可以直接添加依赖项,如下所示:
dependencies {
compile 'org.codehaus.groovy:groovy:2.4.7:grooid' // requires the grooid classifier
compile ('org.codehaus.groovy:groovy-json:2.4.7') { // no grooid version available
transitive = false // so do not depend on non-grooid version
}
}
请注意,groovy-json
的 transitive=false
参数将允许 Gradle 下载 JSON 支持 jar,而无需添加对 Groovy 正常 jar 的依赖。
请务必访问插件主页以查找最新的文档和版本。
2.3. Groovysh,Groovy shell
2.3.1. Groovy : Groovy Shell
Groovy Shell,又名 groovysh
,是一个命令行应用程序,可以轻松访问评估 Groovy 表达式、定义类和运行简单实验。
功能
-
无需
go
命令执行缓冲区。 -
得益于 JLine2,实现丰富的跨平台编辑行编辑、历史和自动补全。
-
ANSI 颜色(提示、异常跟踪等)。
-
简单而强大的命令系统,提供在线帮助、用户别名支持等。
-
用户配置文件支持
命令行选项和参数
Shell 支持多种选项来控制详细程度、ANSI 颜色和其他功能。
./bin/groovysh --help
Usage: groovysh [options] [...]
The Groovy Shell, aka groovysh, is a command-line application which allows easy
access to evaluate Groovy expressions, define classes and run simple
experiments.
-C, --color[=<FLAG>] Enable or disable use of ANSI colors
-cp, -classpath, --classpath
Specify where to find the class files - must be first
argument
-d, --debug Enable debug output
-D, --define=<name=value>
Define a system property
-e, --evaluate=<CODE> Evaluate the code first when starting interactive session
-h, --help Display this help message
-pa, --parameters Generate metadata for reflection on method parameter names
(jdk8+ only)
-pr, --enable-preview Enable preview Java features (jdk12+ only)
-q, --quiet Suppress superfluous output
-T, --terminal=<TYPE> Specify the terminal TYPE to use
-v, --verbose Enable verbose output
-V, --version Display the version
评估表达式
简单表达式
println "Hello"
评估结果
当找到完整的表达式时,它会被编译和评估。评估结果存储在 _ 变量中。
多行表达式
多行/复杂表达式(如闭包或类定义)可以跨多行定义。当 shell 检测到它有一个完整的表达式时,它将编译并评估它。
class Foo {
def bar() {
println "baz"
}
}
foo = new Foo()
foo.bar()
变量
Shell 变量**全部**是无类型的(即没有 def
或其他类型信息)。
这**将**设置一个 shell 变量
foo = "bar"
但是,这会评估一个局部变量,并且**不会**保存到 shell 的环境中。
def foo = "bar"
此行为可以通过激活解释器模式来更改。
函数
函数可以在 shell 中定义,并保存以备后用。
定义函数很简单:
groovy:000> def hello(name) {
groovy:001> println("Hello $name")
groovy:002> }
然后使用它就像人们预期的那样:
hello("Jason")
在内部,shell 创建一个闭包来封装函数,然后将闭包绑定到一个变量。因此,变量和函数共享相同的命名空间。
命令
shell 有许多不同的命令,它们提供了对 shell 环境的丰富访问。
命令都有一个 *名称* 和一个 *快捷方式*(类似于 \h
)。命令也可能有一些预定义的系统 *别名*。用户也可以创建自己的别名。
已识别命令
help
显示命令(和别名)列表或特定命令的帮助文本。
命令列表
groovy:000> :help For information about Groovy, visit: https://groovy-lang.cn Available commands: :help (:h ) Display this help message ? (:? ) Alias to: :help :exit (:x ) Exit the shell :quit (:q ) Alias to: :exit import (:i ) Import a class into the namespace :display (:d ) Display the current buffer :clear (:c ) Clear the buffer and reset the prompt counter :show (:S ) Show variables, classes or imports :inspect (:n ) Inspect a variable or the last result with the GUI object browser :purge (:p ) Purge variables, classes, imports or preferences :edit (:e ) Edit the current buffer :load (:l ) Load a file or URL into the buffer . (:. ) Alias to: :load :save (:s ) Save the current buffer to a file :record (:r ) Record the current session to a file :history (:H ) Display, manage and recall edit-line history :alias (:a ) Create an alias :set (:= ) Set (or list) preferences :grab (:g ) Add a dependency to the shell environment :register (:rc) Register a new command with the shell :doc (:D ) Open a browser window displaying the doc for the argument For help on a specific command type: :help <command>
命令帮助
在交互式 shell 中,您可以请求任何命令的帮助,以获取有关其语法或功能的更多详细信息。以下是您请求 help
命令的帮助时发生的情况示例:
groovy:000> :help :help usage: :help [<command>] Display the list of commands or the help text for <command>.
grab
从互联网来源或缓存中抓取一个依赖项(Maven、Ivy 等),并将其添加到 Groovy Shell 环境中。
groovy:000> :grab 'com.google.guava:guava:19.0' groovy:000> import com.google.common.collect.BiMap ===> com.google.common.collect.BiMap
此命令可以随时给定,以添加新的依赖项。
display
显示当前缓冲区的内容。
这只显示不完整表达式的缓冲区。一旦表达式完成,缓冲区就会重置。提示也会更新以显示当前缓冲区的大小。
示例
groovy:000> class Foo { groovy:001> def bar groovy:002> def baz() { groovy:003> :display 001> class Foo { 002> def bar 003> def baz() {
clear
清除当前缓冲区,将提示计数器重置为 000。可用于从编译错误中恢复。
inspect
打开 GUI 对象浏览器以检查变量或上次评估的结果。
load
将一个或多个文件(或 URL)加载到缓冲区中。
save
将缓冲区内容保存到文件。
alias
创建一个别名。
doc
打开浏览器,显示所提供类的文档。
例如,我们可以获取 java.util.List
的 Javadoc 和 GDK 增强文档(显示在 JDK17 上运行):
groovy:000> :doc java.util.List https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/List.html https://docs.groovy-lang.cn/4.0.28/html/groovy-jdk/java/util/List.html
这将打印找到的文档 URL 并打开两个窗口(或选项卡,取决于您的浏览器):
-
一个用于 JDK 文档
-
一个用于 GDK 文档
默认情况下,对于 Java 类,假定为 java.base
模块。您可以为其他情况指定可选模块(显示在 JDK17 上运行):
groovy:000> :doc java.scripting javax.script.ScriptContext https://docs.oracle.com/en/java/javase/17/docs/api/java.scripting/javax/script/ScriptContext.html
为了向后兼容,如果搜索 Java 类时未指定模块,且在 java.base
模块中未找到类,则会额外尝试在 JDK8(模块前)Javadoc 中查找该类的文档。
groovy:000> :doc javax.script.ScriptContext https://docs.oracle.com/javase/8/docs/api/javax/script/ScriptContext.html
要获取 groovy.ant.AntBuilder
和 groovy.xml.XmlSlurper
的 Groovydoc:
groovy:000> :doc groovy.ant.AntBuilder https://docs.groovy-lang.cn/4.0.28/html/gapi/groovy/ant/AntBuilder.html groovy:000> :doc groovy.xml.XmlSlurper https://docs.groovy-lang.cn/4.0.28/html/gapi/groovy/xml/XmlSlurper.html
获取 groovy.lang.Closure
和 groovy.sql.GroovyResultSet
的 Groovydoc 和 GDK 增强文档:
groovy:000> :doc groovy.lang.Closure https://docs.groovy-lang.cn/4.0.28/html/gapi/groovy/lang/Closure.html https://docs.groovy-lang.cn/4.0.28/html/groovy-jdk/groovy/lang/Closure.html groovy:000> :doc groovy.sql.GroovyResultSet https://docs.groovy-lang.cn/4.0.28/html/gapi/groovy/sql/GroovyResultSet.html https://docs.groovy-lang.cn/4.0.28/html/groovy-jdk/groovy/sql/GroovyResultSet.html
还可获取对原始数组和数组的数组的 GDK 增强文档
groovy:000> :doc int[] https://docs.groovy-lang.cn/4.0.28/html/groovy-jdk/primitives-and-primitive-arrays/int%5B%5D.html groovy:000> :doc double[][] https://docs.groovy-lang.cn/4.0.28/html/groovy-jdk/primitives-and-primitive-arrays/double%5B%5D%5B%5D.html
在不希望打开浏览器的情况下,例如在 CI 服务器上,可以通过将 groovysh.disableDocCommand 系统属性设置为 true 来禁用此命令。 |
set
设置或列出偏好设置。
偏好设置
groovysh
行为的某些方面可以通过设置偏好来定制。偏好通过 set
命令或 :=
快捷方式设置。
已识别的偏好设置
interpreterMode
允许使用带类型变量(即 def
或其他类型信息)
groovy:000> def x = 3 ===> 3 groovy:000> x ===> 3
这对于从教程等复制粘贴代码到正在运行的会话中特别有用。
verbosity
设置 shell 的详细级别。预期为以下之一:
-
DEBUG
-
VERBOSE
-
INFO
-
QUIET
默认值为 INFO
。
如果此偏好设置为无效值,则将使用先前的设置,如果不存在,则删除偏好并使用默认值。
editor
配置 edit
命令使用的编辑器。
默认为系统环境变量 EDITOR
的值。
要在 macOS 上使用默认文本编辑器 TextEdit,请配置:set editor /Applications/TextEdit.app/Contents/MacOS/TextEdit
设置偏好
groovy:000> :set verbosity DEBUG
清除偏好(即重置为默认值)
groovy:000> :purge preferences
用户配置文件脚本和状态
配置文件脚本
$HOME/.groovy/groovysh.profile
该脚本(如果存在)在 shell 启动时加载。
$HOME/.groovy/groovysh.rc
此脚本(如果存在)在 shell 进入交互模式时加载。
状态
$HOME/.groovy/groovysh.history
编辑行历史记录存储在此文件中。
自定义命令
register
命令允许你在 shell 中注册自定义命令。例如,编写以下内容将注册 Stats
命令:
groovy:000> :register Stats
其中 Stats
类是扩展 org.apache.groovy.groovysh.CommandSupport
类的类。例如:
import org.apache.groovy.groovysh.CommandSupport
import org.apache.groovy.groovysh.Groovysh
class Stats extends CommandSupport {
protected Stats(final Groovysh shell) {
super(shell, 'stats', 'T')
}
public Object execute(List args) {
println "Free memory: ${Runtime.runtime.freeMemory()}"
}
}
然后可以使用以下方式调用该命令:
groovy:000> :stats stats Free memory: 139474880 groovy:000>
请注意,命令类必须在类路径中找到:您无法在 shell 内部定义新命令。
故障排除
请报告您遇到的任何问题。请务必将 JIRA 问题标记为 Groovysh
组件。
平台问题
在 Windows 上,JLine2(用于花哨的 shell 输入/历史/补全功能)使用一个**微小的** DLL 文件来欺骗**邪恶的** Windows 伪 shell(CMD.EXE
或 COMMAND.COM
)向 Java 提供无缓冲输入。在某些罕见情况下,这可能无法加载或初始化。
一种解决方案是禁用花哨功能并使用不支持的终端实例。您可以在命令行中使用 --terminal
标志并将其设置为以下之一:
-
无
-
false
-
关
-
jline.UnsupportedTerminal
groovysh --terminal=none
有些人在使用 Cygwin 运行 groovysh 时遇到问题。如果您有麻烦,以下可能有所帮助:
stty -icanon min 1 -echo groovysh --terminal=unix stty icanon echo
2.3.2. GMavenPlus Maven 插件
GMavenPlus 是一个 Maven 插件,其目标支持启动绑定到 Maven 项目的 Groovy Shell 或 Groovy Console。
2.3.3. Gradle Groovysh 插件
Gradle Groovysh Plugin 是一个 Gradle 插件,提供 Gradle 任务来启动绑定到 Gradle 项目的 Groovy Shell。
2.4. groovyConsole,Groovy Swing 控制台
2.4.1. Groovy : Groovy 控制台
Groovy Swing Console 允许用户输入和运行 Groovy 脚本。本页面记录了此用户界面的功能。
2.4.2. 基础知识
-
Groovy Console 通过
groovyConsole
或groovyConsole.bat
启动,两者都位于$GROOVY_HOME/bin
中。 -
控制台有一个输入区和一个输出区。
-
您在输入区键入 Groovy 脚本。
-
当您从“Actions”菜单中选择“Run”时,控制台将编译并运行脚本。
-
任何通常会打印到
System.out
的内容都会打印在输出区域。 -
如果脚本返回非空结果,则打印该结果。
2.4.3. 功能
命令行选项和参数
Groovy Console 支持多种选项来控制类路径和其他功能。
./bin/groovyConsole --help
Usage: groovyConsole [options] [filename]
The Groovy Swing Console allows a user to enter and run Groovy scripts.
--configscript=PARAM A script for tweaking the compiler configuration options
-cp, -classpath, --classpath
Specify where to find the class files - must be first
argument
-D, --define=<name=value> Define a system property
-h, --help Display this help message
-pa, --parameters Generate metadata for reflection on method parameter
names (jdk8+ only)
-pr, --enable-preview Enable preview Java features (jdk12+ only)
-V, --version Display the version
运行脚本
您可以使用以下快捷方式来运行脚本或代码片段:
-
Ctrl+Enter
和Ctrl+R
都是Run Script
的快捷键。 -
如果您只高亮输入区域的一部分文本,那么 Groovy 将只运行该文本。
-
脚本的结果是执行的最后一个表达式的值。
-
您可以通过从“Actions”菜单中选择“Capture System.out”来打开和关闭 System.out 捕获。
编辑文件
您可以打开任何文本文件,编辑它,作为 Groovy 脚本运行它,然后在完成后再次保存它。
-
选择
File > Open
(快捷键ctrl+O
)打开文件。 -
选择
File > Save
(快捷键ctrl+S
)保存文件。 -
选择
File > New File
(快捷键ctrl+Q
)以空白输入区重新开始。
历史记录和结果
-
您可以通过从“Actions”菜单中选择“Inspect Last”来弹出 GUI 检查器,查看最后一个(非空)结果。检查器是查看列表和映射的便捷方式。
-
控制台会记住最近十次脚本运行。您可以通过从“Edit”菜单中选择“Next”和“Previous”来在历史记录中前后滚动。
Ctrl-N
和ctrl-P
是方便的快捷键。 -
最后一个(非空)结果绑定到一个名为
_
(下划线)的变量。 -
历史记录中每次运行的最后一个结果(空和非空)都绑定到一个名为
__
(两个下划线)的列表变量。最后一次运行的结果是__[-1]
,倒数第二次运行的结果是__[-2]
,依此类推。
中断脚本
Groovy 控制台是开发脚本的非常方便的工具。通常,您会发现自己多次运行脚本,直到它按照您想要的方式工作。但是,如果您的代码运行时间过长,或者更糟,创建了一个无限循环怎么办?中断脚本执行可以通过单击脚本执行时弹出的小对话框中的“中断”按钮或通过工具栏中的“中断”图标来实现。
然而,这可能不足以中断脚本:单击按钮会中断执行线程,但如果您的代码不处理中断标志,脚本可能会继续运行,而您无法有效停止它。为避免这种情况,您必须确保“脚本 > 允许中断”菜单项已标记。这将自动将 AST 转换应用于您的脚本,该转换将负责检查中断标志(@ThreadInterrupt
)。这样,即使您不明确处理中断,您也能保证脚本可以被中断,但代价是额外的执行时间。
更多功能
-
您可以通过从“Actions”菜单中选择“Smaller Font”或“Larger Font”来更改字体大小。
-
该控制台可以作为 Applet 运行,这得益于
groovy.ui.ConsoleApplet
-
当您回车时,代码会自动缩进。
-
您可以将 Groovy 脚本拖放到文本区域以打开文件。
-
您可以从“Script”菜单中添加新的 JAR 或目录到类路径,从而修改控制台中脚本运行的类路径。
-
编译错误或抛出异常时,输出区域会显示错误超链接。
2.4.4. 嵌入控制台
要将 Swing 控制台嵌入您的应用程序,只需创建 Console 对象,加载一些变量,然后启动它。控制台可以嵌入 Java 或 Groovy 代码。Java 代码如下:
import groovy.ui.Console;
...
Console console = new Console();
console.setVariable("var1", getValueOfVar1());
console.setVariable("var2", getValueOfVar2());
console.run();
...
控制台启动后,您可以在 Groovy 代码中使用变量值。
2.4.5. 可视化脚本输出结果
您可以自定义脚本输出结果的可视化方式。让我们看看如何自定义。例如,查看地图结果会显示如下内容:
您在这里看到的是 Map 的常见文本表示。但是,如果我们启用了某些结果的自定义可视化会怎样?Swing 控制台就允许您这样做。首先,您必须确保可视化选项已勾选:“视图 → 可视化脚本结果”——顺便说一句,Groovy 控制台的所有设置都通过 Preference API 存储和记住。内置了一些结果可视化:如果脚本返回 java.awt.Image
、javax.swing.Icon
或没有父级的 java.awt.Component
,则显示对象而不是其 toString()
表示。否则,所有其他内容仍然只是文本表示。现在,在 ~/.groovy/OutputTransforms.groovy
中创建以下 Groovy 脚本:
import javax.swing.*
transforms << { result ->
if (result instanceof Map) {
def table = new JTable(
result.collect{ k, v ->
[k, v?.inspect()] as Object[]
} as Object[][],
['Key', 'Value'] as Object[])
table.preferredViewportSize = table.preferredSize
return new JScrollPane(table)
}
}
Groovy Swing 控制台将在启动时执行该脚本,并将一个转换列表注入脚本的绑定中,以便您可以添加自己的脚本结果表示。在我们的例子中,我们将 Map 转换为一个漂亮美观的 Swing JTable。我们现在能够以友好且吸引人的方式可视化地图,如下图所示:
2.4.6. 高级调试:AST 浏览器
Groovy 控制台可以可视化表示当前编辑脚本的 AST(抽象语法树),如下图所示。这在您想了解 AST 转换如何工作时很有用,并且在您开发自己的 AST 转换时特别方便。在下面的示例中,我们用 @Immutable
注解标记了我们的类,Groovy 编译器为我们生成了大量样板代码。我们可以在“Source”选项卡中看到生成的 equals 方法的代码。
我们甚至可以检查编译器生成的 JVM 字节码。在下图中,我们正在查看 Groovy 表达式 LocalDate.parse('2020/02/10', 'yyyy/MM/dd')
的字节码。
2.5. groovydoc,Groovy & Java 文档生成器
GroovyDoc 是一个负责从代码生成文档的工具。它的作用类似于 Java 世界中的 Javadoc 工具,但能够处理 groovy
和 java
文件。发行版提供了两种生成文档的方式:从命令行或从Apache Ant。其他构建工具如 Maven 或 Gradle 也提供 Groovydoc 的包装器。
2.5.1. groovydoc 命令行工具
可以调用 groovydoc
命令行来生成 groovydocs
groovydoc [options] [packagenames] [sourcefiles]
其中选项必须从下表中选择
短版本 | 长版本 | 描述 |
---|---|---|
-author |
包含 @author 段落(目前未使用) |
|
-charset <charset> |
用于跨平台查看生成文档的字符集 |
|
-classpath, -cp |
--classpath |
指定类文件的位置 - 必须是第一个参数 |
-d |
--destdir <dir> |
输出文件的目标目录 |
--debug |
启用调试输出 |
|
-doctitle <html> |
包含概述页面的标题 |
|
-exclude <pkglist> |
指定要排除的包列表(所有操作系统均以冒号分隔) |
|
-fileEncoding <charset> |
生成文档文件的字符集 |
|
-footer <html> |
为每个页面包含页脚文本 |
|
-header <html> |
为每个页面包含页眉文本 |
|
-help |
--help |
显示帮助信息 |
-nomainforscripts |
不包含脚本的隐式“public static void main”方法 |
|
-noscripts |
不处理 Groovy 脚本 |
|
-notimestamp |
不在生成的 HTML 中包含隐藏注释中的时间戳 |
|
-noversionstamp |
不在生成的 HTML 的隐藏注释中包含 Groovy 版本 |
|
-overview <file> |
从 HTML 文件读取概述文档 |
|
-package |
显示包/受保护/公共类和成员 |
|
-private |
显示所有类和成员 |
|
-protected |
显示受保护/公共类和成员(默认) |
|
-public |
只显示公共类和成员 |
|
-quiet |
抑制多余的输出 |
|
-sourcepath <pathlist> |
指定查找源文件的位置(目录以平台路径分隔符分隔) |
|
-stylesheetfile <path> |
用于更改生成文档样式的 文件 |
|
-verbose |
启用详细输出 |
|
--version |
显示版本 |
|
-windowtitle <text> |
文档的浏览器窗口标题 |
|
-javaversion <version> |
Java 源文件的版本 |
Java 版本
groovydoc
支持的 Java 版本由 JavaParser 库的 LanguageLevel 类定义。
2.5.2. groovydoc Ant 任务
groovydoc
Ant 任务允许从 Ant 构建中生成 groovydocs。
必需的 taskdef
假设您需要的所有 groovy jar 文件都在 *my.classpath* 中(这将是 groovy-VERSION.jar
、groovy-ant-VERSION.jar
、groovy-groovydoc-VERSION.jar
以及您可能正在使用的任何模块和传递依赖项),您需要在调用 groovydoc 任务之前在 build.xml 中的某个位置声明此任务。
<taskdef name = "groovydoc"
classname = "org.codehaus.groovy.ant.Groovydoc"
classpathref = "my.classpath"/>
<groovydoc> 属性
属性 | 描述 | 必填项 |
---|---|---|
目标目录 |
存储类文件的位置。 |
是 |
源路径 |
要使用的源路径。 |
不 |
包名 |
逗号分隔的包文件列表(带终止通配符)。 |
不 |
用途 |
创建类和包使用页面。 |
不 |
窗口标题 |
文档的浏览器窗口标题(文本)。 |
不 |
文档标题 |
包含包索引(第一个)页面的标题(html-code)。 |
不 |
页眉 |
为每个页面包含页眉文本(html-code)。 |
不 |
页脚 |
为每个页面包含页脚文本(html-code)。 |
不 |
概述 |
从 HTML 文件读取概述文档。 |
不 |
private |
如果设置为 ``true'',则显示所有类和成员(即包括私有成员)。 |
不 |
java版本 |
Java 源文件的版本。 |
不 |
<groovydoc> 嵌套元素
示例 #1 - <groovydoc> Ant 任务
<taskdef name = "groovydoc"
classname = "org.codehaus.groovy.ant.Groovydoc"
classpathref = "path_to_groovy_all"/>
<groovydoc destdir = "${docsDirectory}/gapi"
sourcepath = "${mainSourceDirectory}"
packagenames = "**.*"
use = "true"
windowtitle = "${title}"
doctitle = "${title}"
header = "${title}"
footer = "${docFooter}"
overview = "src/main/overview.html"
private = "false">
<link packages="java.,org.xml.,javax.,org.xml." href="http://docs.oracle.com/javase/8/docs/api/"/>
<link packages="org.apache.tools.ant." href="https://docs.groovy-lang.cn/docs/ant/api/"/>
<link packages="org.junit.,junit.framework." href="https://junit.cn/junit4/javadoc/latest/"/>
<link packages="groovy.,org.codehaus.groovy." href="https://docs.groovy-lang.cn/latest/html/api/"/>
<link packages="org.codehaus.gmaven." href="http://groovy.github.io/gmaven/apidocs/"/>
</groovydoc>
示例 #2 - 从 Groovy 执行 <groovydoc>
def ant = new AntBuilder()
ant.taskdef(name: "groovydoc", classname: "org.codehaus.groovy.ant.Groovydoc")
ant.groovydoc(
destdir : "${docsDirectory}/gapi",
sourcepath : "${mainSourceDirectory}",
packagenames : "**.*",
use : "true",
windowtitle : "${title}",
doctitle : "${title}",
header : "${title}",
footer : "${docFooter}",
overview : "src/main/overview.html",
private : "false") {
link(packages:"java.,org.xml.,javax.,org.xml.",href:"http://docs.oracle.com/javase/8/docs/api/")
link(packages:"groovy.,org.codehaus.groovy.", href:"https://docs.groovy-lang.cn/latest/html/api/")
link(packages:"org.apache.tools.ant.", href:"https://docs.groovy-lang.cn/docs/ant/api/")
link(packages:"org.junit.,junit.framework.", href:"https://junit.cn/junit4/javadoc/latest/")
link(packages:"org.codehaus.gmaven.", href:"http://groovy.github.io/gmaven/apidocs/")
}
自定义模板
groovydoc
Ant 任务支持自定义模板,但这需要两个步骤:
-
自定义 groovydoc 类
-
新的 groovydoc 任务定义
自定义 Groovydoc 类
第一步要求您扩展 Groovydoc
类,如下例所示
package org.codehaus.groovy.tools.groovydoc;
import org.codehaus.groovy.ant.Groovydoc;
/**
* Overrides GroovyDoc's default class template - for testing purpose only.
*/
public class CustomGroovyDoc extends Groovydoc {
@Override
protected String[] getClassTemplates() {
return new String[]{"org/codehaus/groovy/tools/groovydoc/testfiles/classDocName.html"};
}
}
您可以覆盖以下方法
-
getClassTemplates
用于类级别模板 -
getPackageTemplates
用于包级别模板 -
getDocTemplates
用于顶级模板
您可以在 org.codehaus.groovy.tools.groovydoc.gstringTemplates.GroovyDocTemplateInfo
类中找到默认模板列表。
使用自定义 groovydoc 任务
一旦您编写了类,使用它只需要重新定义 groovydoc
任务
<taskdef name = "groovydoc"
classname = "org.codehaus.groovy.ant.CustomGroovyDoc"
classpathref = "path_to_groovy_all"/>
请注意,模板定制按原样提供。API 可能会更改,因此您必须将其视为一项脆弱功能。
2.5.3. GMavenPlus Maven 插件
GMavenPlus 是一个 Maven 插件,其目标支持 GroovyDoc 生成。
2.6. IDE 集成
许多 IDE 和文本编辑器支持 Groovy 编程语言。
编辑器 | 语法高亮 | 代码补全 | 重构 |
---|---|---|---|
是 |
是 |
是 |
|
是 |
是 |
是 |
|
是 |
是 |
是 |
|
是 |
不 |
不 |
|
是 |
不 |
不 |
|
是 |
不 |
不 |
|
是 |
不 |
不 |
|
是 |
不 |
不 |
|
是 |
不 |
不 |
|
是 |
不 |
3. 用户指南
3.1. 入门
3.1.1. 下载
从下载页面,您可以下载 Groovy 的发行版(二进制和源代码)、Windows 安装程序(社区作品)和文档。
快照
对于那些希望测试最新 Groovy 版本并走在前沿的人,您可以使用我们的快照构建。一旦我们的持续集成服务器上的构建成功,就会将快照部署到此存储库。这些快照不是官方版本,旨在供开发社区在官方版本发布之前进行集成测试。我们欢迎任何反馈。
先决条件
Groovy 4.0 需要 Java 8+,并支持高达 Java 16。
各种 Groovy CI 服务器在多个 Java 版本上运行测试套件(超过 10000 个测试)。这些服务器也有助于确认不同 Groovy 版本的受支持 Java 版本。
3.1.2. Maven 仓库
如果您希望将 Groovy 嵌入到您的应用程序中,您可能更喜欢将您的构建指向您喜欢的 maven 仓库或 Groovy Artifactory 实例。请参阅下载页面以获取每个 Groovy 版本的可用模块。
3.1.3. SDKMAN!(软件开发工具包管理器)
这个工具使得在任何 Bash 平台(Mac OSX、Linux、Cygwin、Solaris 或 FreeBSD)上安装 Groovy 都非常容易。
只需打开一个新终端并输入
$ curl -s "https://get.sdkman.io" | bash
按照屏幕上的说明完成安装。
打开一个新终端或输入命令
$ source "$HOME/.sdkman/bin/sdkman-init.sh"
然后安装最新的稳定版 Groovy
$ sdk install groovy
安装完成后并将其设置为默认版本后,使用以下命令进行测试
$ groovy -version
就这么简单!
3.1.4. 获取 Groovy 的其他方式
在 Windows 上安装
如果您使用 Windows,也可以使用 Windows 安装程序。
其他发行版
您可以从 ASF 存档仓库或 Groovy Artifactory 实例(也包括 ASF 之前版本)下载其他 Groovy 发行版。
源代码
如果您喜欢走在前沿,您也可以从 GitHub 获取源代码。
3.1.5. 安装二进制文件
这些说明描述了如何安装 Groovy 的二进制发行版
-
下载 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 交互式控制台,输入
groovyConsole
要运行特定的 Groovy 脚本,请键入
groovy SomeScript
3.2. 与 Java 的区别
Groovy 致力于尽可能地让 Java 开发人员感到自然。我们在设计 Groovy 时,特别是对于那些有 Java 背景的 Groovy 初学者,努力遵循最小惊讶原则。
这里我们列出了 Java 和 Groovy 之间所有主要的区别。
3.2.1. 默认导入
所有这些包和类都默认导入,即您无需使用显式 import
语句即可使用它们
-
java.io.*
-
java.lang.*
-
java.math.BigDecimal
-
java.math.BigInteger
-
java.net.*
-
java.util.*
-
groovy.lang.*
-
groovy.util.*
3.2.2. 多方法
在 Groovy 中,将要调用的方法是在运行时选择的。这称为运行时调度或多方法。这意味着方法将根据运行时参数的类型进行选择。在 Java 中,情况则相反:方法是在编译时根据声明的类型选择的。
以下代码以 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
调用,因此会调用 String
版本。
3.2.3. 数组初始化器
在 Java 中,数组初始化器采用以下两种形式之一
int[] array = {1, 2, 3}; // Java array initializer shorthand syntax
int[] array2 = new int[] {4, 5, 6}; // Java array initializer long syntax
在 Groovy 中,{ … }
块保留用于闭包。这意味着您不能使用 Java 的数组初始化器简写语法创建数组字面量。您可以借用 Groovy 的字面量列表表示法,如下所示
int[] array = [1, 2, 3]
对于 Groovy 3+,您可以选择使用 Java 的数组初始化器长语法
def array2 = new int[] {1, 2, 3} // Groovy 3.0+ supports the Java-style array initialization long syntax
3.2.4. 包作用域可见性
在 Groovy 中,省略字段上的修饰符不会像 Java 中那样导致包私有字段
class Person {
String name
}
相反,它用于创建**属性**,即**私有字段**、关联的**getter**和关联的**setter**。
可以通过使用 @PackageScope
注解来创建包私有字段
class Person {
@PackageScope String name
}
3.2.5. ARM 块
Java 7 引入了 ARM (Automatic Resource Management) 块(也称为 try-with-resources)块,如下所示
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();
}
Groovy 3+ 支持此类块。然而,Groovy 提供了各种依赖闭包的方法,它们具有相同的效果,但更具惯用性。例如
new File('/path/to/file').eachLine('UTF-8') {
println it
}
或者,如果您想要一个更接近 Java 的版本
new File('/path/to/file').withReader('UTF-8') { reader ->
reader.eachLine {
println it
}
}
3.2.6. 内部类
匿名内部类和嵌套类的实现与 Java 密切相关,但存在一些差异,例如,从此类中访问的局部变量不必是 final。我们在生成内部类字节码时,会利用用于 groovy.lang.Closure 的一些实现细节。 |
匿名内部类
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)
创建非静态内部类实例
在 Java 中,您可以这样做
public class Y {
public class X {}
public X foo() {
return new X();
}
public static X createX(Y y) {
return y.new X();
}
}
在 3.0.0 之前,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)`,这是一个危险。由于这可能也是常见的方式,我们还没有找到一种好的方法来防止这个问题。 |
Groovy 3.0.0 支持 Java 风格的语法来创建非静态内部类实例。 |
3.2.7. Lambda 表达式和方法引用运算符
Java 8+ 支持 lambda 表达式和方法引用运算符 (::
)
Runnable run = () -> System.out.println("Run"); // Java
list.forEach(System.out::println);
Groovy 3 及更高版本也在 Parrot 解析器中支持这些。在早期版本的 Groovy 中,您应该改用闭包
Runnable run = { println 'run' }
list.each { println it } // or list.each(this.&println)
3.2.8. GString
由于双引号字符串字面量被解释为 GString
值,如果包含美元字符的 String
字面量的类使用 Groovy 和 Java 编译器编译,Groovy 可能会出现编译错误或产生细微不同的代码。
虽然通常情况下,如果 API 声明了参数的类型,Groovy 会在 GString
和 String
之间自动转换,但请注意接受 Object
参数然后检查实际类型的 Java API。
3.2.9. 字符串和字符字面量
在 Groovy 中,单引号字面量用于 String
,双引号字面量产生 String
或 GString
,取决于字面量中是否有插值。
assert 'c'.class == String
assert "c".class == String
assert "c${1}".class in GString
Groovy 仅在赋值给 char
类型的变量时才将单字符 String
自动转换为 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 样式强制转换将失败并抛出异常。
// 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'
3.2.10. ==
的行为
在 Java 中,==
表示基本类型的相等性或对象的同一性。在 Groovy 中,==
在所有地方都表示相等性。对于非基本类型,它转换为 a.compareTo(b) == 0
(当评估 Comparable
对象的相等性时)和 a.equals(b)
(否则)。
要检查同一性(引用相等性),请使用 is
方法:a.is(b)
。从 Groovy 3 开始,您还可以使用 ===
运算符(或取反版本):a === b
(或 c !== d
)。
3.2.11. 基本类型和包装器
在纯面向对象的语言中,所有事物都将是对象。Java 认为基本类型(如 int、boolean 和 double)使用非常频繁,值得特殊对待。基本类型可以高效地存储和操作,但不能在所有可以使用对象的上下文中进行使用。幸运的是,Java 在将基本类型作为参数传递或用作返回类型时会自动装箱和拆箱
public class Main { // Java
float f1 = 1.0f;
Float f2 = 2.0f;
float add(Float a1, float a2) { return a1 + a2; }
Float calc() { return add(f1, f2); } (1)
public static void main(String[] args) {
Float calcResult = new Main().calc();
System.out.println(calcResult); // => 3.0
}
}
1 | add 方法期望包装器后跟基本类型参数,但我们提供的是基本类型后跟包装器类型。类似地,add 的返回类型是基本类型,但我们需要包装器类型。 |
Groovy 也是如此
class Main {
float f1 = 1.0f
Float f2 = 2.0f
float add(Float a1, float a2) { a1 + a2 }
Float calc() { add(f1, f2) }
}
assert new Main().calc() == 3.0
Groovy 也支持基本类型和对象类型,但是,它在推动面向对象纯洁性方面更进一步;它努力将**所有事物**都视为对象。任何基本类型变量或字段都可以像对象一样处理,并且会根据需要自动包装。虽然底层可能使用基本类型,但其使用应尽可能与普通对象使用无异,并且会根据需要进行装箱/拆箱。
这是一个使用 Java 尝试(对于 Java 而言不正确)解引用基本 float
的小例子
public class Main { // Java
public float z1 = 0.0f;
public static void main(String[] args){
new Main().z1.equals(1.0f); // DOESN'T COMPILE, error: float cannot be dereferenced
}
}
使用 Groovy 的相同示例可以成功编译并运行
class Main {
float z1 = 0.0f
}
assert !(new Main().z1.equals(1.0f))
由于 Groovy 额外使用了拆装箱,它不遵循 Java 中加宽优先于拆装箱的行为。这是一个使用 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 将调用的方法,因为加宽优先于拆箱。 |
2 | 这是 Groovy 实际调用的方法,因为所有原始引用都使用它们的包装类。 |
使用 @CompileStatic
进行数字原始优化
由于 Groovy 在更多地方转换为包装类,您可能想知道它是否会为数字表达式生成效率较低的字节码。Groovy 有一套高度优化的类用于进行数学计算。使用 @CompileStatic
时,仅涉及基本类型的表达式使用与 Java 相同的字节码。
正/负零的边缘情况
Java float/double 操作(包括基本类型和包装类)遵循 IEEE 754 标准,但存在一个有趣的边缘情况,涉及正零和负零。该标准支持区分这两种情况,虽然在许多情况下程序员可能不关心这种差异,但在某些数学或数据科学场景中,区分它们很重要。
对于基本类型,Java 在比较这些值时会映射到一个特殊的字节码指令,该指令具有“正零和负零被认为是相等的”属性。
jshell> float f1 = 0.0f
f1 ==> 0.0
jshell> float f2 = -0.0f
f2 ==> -0.0
jshell> f1 == f2
$3 ==> true
对于包装类,例如 java.base/java.lang.Float#equals(java.lang.Object),对于相同情况,结果是 false
。
jshell> Float f1 = 0.0f
f1 ==> 0.0
jshell> Float f2 = -0.0f
f2 ==> -0.0
jshell> f1.equals(f2)
$3 ==> false
Groovy 一方面试图密切遵循 Java 的行为,但另一方面又在更多地方自动在原始类型和包装等效类型之间切换。为了避免混淆,我们建议遵循以下准则
-
如果您希望区分正零和负零,请直接使用
equals
方法,或者在使用==
之前将任何原始类型转换为其包装器等效类型。 -
如果您希望忽略正零和负零之间的差异,请直接使用
equalsIgnoreZeroSign
方法,或者在使用==
之前将任何非基本类型转换为其基本类型等效类型。
这些准则在以下示例中进行说明
float f1 = 0.0f
float f2 = -0.0f
Float f3 = 0.0f
Float f4 = -0.0f
assert f1 == f2
assert (Float) f1 != (Float) f2
assert f3 != f4 (1)
assert (float) f3 == (float) f4
assert !f1.equals(f2)
assert !f3.equals(f4)
assert f1.equalsIgnoreZeroSign(f2)
assert f3.equalsIgnoreZeroSign(f4)
1 | 请记住,对于非基本类型,== 映射到 .equals() |
3.2.12. 转换
Java 执行自动加宽和收窄转换。
转换为 |
||||||||
从 |
boolean |
byte |
short |
char |
int |
long |
float |
double |
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' 表示 Java 在有显式强制类型转换时可以进行的转换
-
'T' 表示 Java 可以进行的转换,但数据会被截断
-
'N' 表示 Java 无法进行的转换
Groovy 在这方面进行了极大的扩展。
转换为 |
||||||||||||||||||
从 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- |
B |
N |
N |
N |
N |
N |
N |
N |
N |
N |
N |
N |
N |
N |
N |
N |
N |
|
B |
- |
N |
N |
N |
N |
N |
N |
N |
N |
N |
N |
N |
N |
N |
N |
N |
N |
|
T |
T |
- |
B |
Y |
Y |
Y |
D |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
|
T |
T |
B |
- |
Y |
Y |
Y |
D |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
|
T |
T |
D |
D |
- |
B |
Y |
D |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
|
T |
T |
D |
T |
B |
- |
Y |
D |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
|
T |
T |
Y |
D |
Y |
D |
- |
D |
Y |
D |
Y |
D |
D |
Y |
D |
Y |
D |
D |
|
T |
T |
D |
D |
D |
D |
D |
- |
D |
D |
D |
D |
D |
D |
D |
D |
D |
D |
|
T |
T |
D |
D |
D |
D |
Y |
D |
- |
B |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
|
T |
T |
D |
D |
D |
D |
Y |
D |
B |
- |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
|
T |
T |
D |
D |
D |
D |
Y |
D |
D |
D |
- |
B |
Y |
T |
T |
T |
T |
Y |
|
T |
T |
D |
D |
D |
T |
Y |
D |
D |
T |
B |
- |
Y |
T |
T |
T |
T |
Y |
|
T |
T |
D |
D |
D |
D |
D |
D |
D |
D |
D |
D |
- |
D |
D |
D |
D |
T |
|
T |
T |
D |
D |
D |
D |
T |
D |
D |
D |
D |
D |
D |
- |
B |
Y |
Y |
Y |
|
T |
T |
D |
T |
D |
T |
T |
D |
D |
T |
D |
T |
D |
B |
- |
Y |
Y |
Y |
|
T |
T |
D |
D |
D |
D |
T |
D |
D |
D |
D |
D |
D |
D |
D |
- |
B |
Y |
|
T |
T |
D |
T |
D |
T |
T |
D |
D |
T |
D |
T |
D |
D |
T |
B |
- |
Y |
|
T |
T |
D |
D |
D |
D |
D |
D |
D |
D |
D |
D |
D |
T |
D |
T |
D |
- |
-
'Y' 表示 Groovy 可以进行的转换
-
'D' 表示 Groovy 在动态编译或显式转换时可以进行的转换
-
'T' 表示 Groovy 可以进行的转换,但数据会被截断
-
'B' 表示装箱/拆箱操作
-
'N' 表示 Groovy 无法进行的转换。
当转换为 boolean
/Boolean
时,截断使用 Groovy Truth。将数字转换为字符会将 Number.intValue()
转换为 char
。当从 Float
或 Double
转换时,Groovy 使用 Number.doubleValue()
构造 BigInteger
和 BigDecimal
,否则它使用 toString()
构造。其他转换的行为由 java.lang.Number
定义。
3.2.13. 额外关键字
Groovy 拥有与 Java 相同的许多关键字,Groovy 3 及更高版本也拥有与 Java 相同的 var
保留类型。此外,Groovy 还有以下关键字
-
as
-
def
-
in
-
trait
-
it
// 在闭包内
Groovy 不像 Java 那样严格,它允许某些关键字出现在 Java 中非法的位置,例如以下是有效的:`var var = [def: 1, as: 2, in: 3, trait: 4]`。尽管如此,不鼓励您在可能引起混淆的地方使用上述关键字,即使编译器可能接受。特别是,避免将它们用于变量、方法和类名,因此我们之前的 `var var` 示例将被视为糟糕的风格。
有关关键字的额外文档可用。
3.3. Groovy 开发工具包
3.3.1. 处理 I/O
Groovy 提供了许多用于处理 I/O 的辅助方法。虽然您可以在 Groovy 中使用标准 Java 代码来处理这些,但 Groovy 提供了更方便的方式来处理文件、流、读取器等。
特别是,您应该查看已添加到以下类的方法
-
java.io.File
类 : https://docs.groovy-lang.cn/latest/html/groovy-jdk/java/io/File.html -
java.io.InputStream
类: https://docs.groovy-lang.cn/latest/html/groovy-jdk/java/io/InputStream.html -
java.io.OutputStream
类: https://docs.groovy-lang.cn/latest/html/groovy-jdk/java/io/OutputStream.html -
java.io.Reader
类: https://docs.groovy-lang.cn/latest/html/groovy-jdk/java/io/Reader.html -
java.io.Writer
类: https://docs.groovy-lang.cn/latest/html/groovy-jdk/java/io/Writer.html -
java.nio.file.Path
类: https://docs.groovy-lang.cn/latest/html/groovy-jdk/java/nio/file/Path.html
以下部分重点介绍使用上述辅助方法的示例惯用构造,但并非旨在完整描述所有可用方法。为此,请阅读GDK API。
读取文件
作为一个例子,让我们看看如何在 Groovy 中打印文本文件的所有行
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 添加的所有 I/O 资源方法都适用。
例如,在某些情况下,您更喜欢使用 Reader
,但仍受益于 Groovy 的自动资源管理。在下一个示例中,即使发生异常,读取器**也将**关闭
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')
}
}
}
如果您需要将文本文件的行收集到列表中,您可以这样做
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
处理 I/O 不限于处理文件。实际上,许多操作都依赖于输入/输出流,因此 Groovy 为它们添加了许多支持方法,如您在文档中看到的。
例如,您可以非常轻松地从 File
获取 InputStream
def is = new File(baseDir,'haiku.txt').newInputStream()
// do something ...
is.close()
但是您可以看到它要求您处理关闭输入流。在 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 | 否则打印文件名并继续 |
数据和对象
在 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 中处理文件、读取器或流是多么容易。然而,在系统管理或 DevOps 等领域,通常需要与外部进程进行通信。
Groovy 提供了一种简单的方法来执行命令行进程。只需将命令行写成字符串并调用 execute()
方法。例如,在 *nix 机器上(或安装了相应 *nix 命令的 Windows 机器上),您可以执行此操作
def process = "ls -l".execute() (1)
println "Found text ${process.text}" (2)
1 | 在外部进程中执行 ls 命令 |
2 | 消费命令输出并检索文本 |
execute()
方法返回一个 java.lang.Process
实例,该实例随后将允许处理输入/输出/错误流,并检查进程的退出值等。
例如,这里是与上面相同的命令,但我们现在将一次处理一行结果流
def process = "ls -l".execute() (1)
process.in.eachLine { line -> (2)
println line (3)
}
1 | 在外部进程中执行 ls 命令 |
2 | 对于进程输入流的每一行 |
3 | 打印行 |
值得注意的是,in
对应于命令标准输出的输入流。out
将引用一个您可以向进程发送数据(其标准输入)的流。
请记住,许多命令都是 shell 内置命令,需要特殊处理。因此,如果您想在 Windows 机器上列出目录中的文件并写入
def process = "dir".execute()
println "${process.text}"
您将收到一个 IOException
,提示*无法运行程序 "dir":CreateProcess error=2,系统找不到指定的文件。*
这是因为 dir
是 Windows shell (cmd.exe
) 的内置命令,不能作为简单的可执行文件运行。相反,您需要编写
def process = "cmd /c dir".execute()
println "${process.text}"
此外,由于此功能目前在底层使用了 java.lang.Process
,因此必须考虑该类的不足之处。特别是,此类的 javadoc 提到
由于某些本机平台仅为标准输入和输出流提供有限的缓冲区大小,未能及时写入输入流或读取子进程的输出流可能导致子进程阻塞,甚至死锁
因此,Groovy 提供了一些额外的辅助方法,使进程的流处理变得更容易。
以下是如何吞噬进程的所有输出(包括错误流输出)
def p = "rm -f foo.tmp".execute([], tmpDir)
p.consumeProcessOutput()
p.waitFor()
还有 consumeProcessOutput
的变体,使用 StringBuffer
、InputStream
、OutputStream
等… 有关完整列表,请阅读java.lang.Process 的 GDK API
此外,还有一个 pipeTo
命令(映射到 |
以允许重载),它允许一个进程的输出流输入到另一个进程的输入流中。
以下是一些使用示例
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
}
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"
3.3.2. 使用集合
Groovy 为各种集合类型提供原生支持,包括列表、映射或范围。这些大部分基于 Java 集合类型,并用Groovy 开发工具包中找到的额外方法进行装饰。
列表
列表字面量
您可以按如下方式创建列表。请注意,[]
是空列表表达式。
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
每个列表表达式都会创建一个 java.util.List 的实现。
当然,列表可以用作构建另一个列表的源
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
列表是对象的有序集合
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 IndexOutOfBoundsException
}
列表作为布尔表达式
列表可以评估为 boolean
值
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
方法来迭代列表元素,这些方法在列表的每个项上执行代码
[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"
}
除了迭代,通过将每个元素转换为其他形式来创建新列表也很有用。此操作通常称为映射,在 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']
然而,重要的是,列表上的 +
运算符是**非变异的**。与 <<
相比,它会创建一个新列表,这通常不是您想要的,并且可能导致性能问题。
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]
也可以通过将其索引传递给 remove
方法来删除元素,在这种情况下,列表会发生变异
def list = ['a','b','c','d','e','f','b','b','a']
assert list.remove(2) == 'c' // remove the third element, and return it
assert list == ['a','b','d','e','f','b','b','a']
如果您只想删除列表中第一个具有相同值的元素,而不是删除所有元素,则可以调用 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']
如您所见,有两个 remove
方法可用。一个接受整数并按索引删除元素,另一个将删除与传递值匹配的第一个元素。那么,当我们有一个整数列表时应该怎么办呢?在这种情况下,您可能希望使用 removeAt
按索引删除元素,并使用 removeElement
删除与值匹配的第一个元素。
def list = [1,2,3,4,5,6,2,2,1]
assert list.remove(2) == 3 // this removes the element at index 2, and returns it
assert list == [1,2,4,5,6,2,2,1]
assert list.removeElement(2) // remove first 2 and return true
assert list == [1,4,5,6,2,2,1]
assert ! list.removeElement(8) // return false because 8 is not in the list
assert list == [1,4,5,6,2,2,1]
assert list.removeAt(1) == 4 // remove element at index 1, and return it
assert list == [1,5,6,2,2,1]
当然,removeAt
和 removeElement
适用于任何类型的列表。
此外,通过调用 clear
方法可以删除列表中的所有元素
def list= ['a',2,'c',4]
list.clear()
assert list == []
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]
映射
映射字面量
在 Groovy 中,可以使用映射字面量语法创建映射(也称为关联数组):[:]
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
映射键默认为字符串:[a:1]
等效于 ['a':1]
。如果您定义了一个名为 a
的变量,并且您希望 a
的**值**成为映射中的键,这可能会造成混淆。在这种情况下,您**必须**通过添加括号来转义键,如下例所示
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!
除了映射字面量,还可以通过克隆来获取映射的新副本
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
生成的映射是原始映射的**浅**副本,如上例所示。
映射属性表示法
映射也像 bean 一样工作,因此只要键是有效的 Groovy 标识符字符串,您就可以使用属性表示法来获取/设置 Map
中的项目
def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.name == 'Gromit' // can be used instead of map.get('name')
assert map.id == 1234
def emptyMap = [:]
assert emptyMap.size() == 0
emptyMap.foo = 5
assert emptyMap.size() == 1
assert emptyMap.foo == 5
注意:根据设计,map.foo
总是会在映射中查找键 foo
。这意味着 foo.class
在不包含 class
键的映射上将返回 null
。如果您确实想知道类,则必须使用 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'
迭代映射
在Groovy 开发工具包中,通常使用 each
和 eachWithIndex
方法对映射进行惯用迭代。值得注意的是,使用映射字面量表示法创建的映射是**有序的**,也就是说,如果您迭代映射条目,则保证条目将按照它们添加到映射中的相同顺序返回。
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"
}
操作映射
向映射添加元素可以使用 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
方法可以删除映射中的所有元素
def m = [1:'a', 2:'b']
assert m.get(1) == 'a'
m.clear()
assert m == [:]
使用映射字面量语法生成的映射使用对象 equals
和 hashcode
方法。这意味着您**绝不能**使用哈希码随时间变化的对象,否则您将无法取回关联的值。
还值得注意的是,您**绝不能**使用 GString
作为映射的键,因为 GString
的哈希码与等效 String
的哈希码不同
def key = 'some key'
def map = [:]
def gstringKey = "${key.toUpperCase()}"
map.put(gstringKey,'value')
assert map.get('SOME KEY') == null
我们可以查看键、值和视图中的条目
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
强烈不鼓励修改视图返回的值(无论是映射条目、键还是值),因为操作的成功直接取决于被操作映射的类型。特别是,Groovy 依赖于 JDK 中的集合,这些集合通常不保证可以通过 `keySet`、`entrySet` 或 `values` 安全地操作集合。
Groovy 开发工具包包含与列表类似的过滤、搜索和收集方法
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']],
]
范围
范围允许您创建一系列连续值。由于 Range 扩展了 java.util.List,因此它们可以用作 List
。
使用 ..
符号定义的范围是包含性的(即列表包含 from 和 to 值)。
使用 ..<
符号定义的范围是半开的,它们包含第一个值但不包含最后一个值。
使用 <..
符号定义的范围也是半开的,它们包含最后一个值但不包含第一个值。
使用 <..<
符号定义的范围是全开的,它们不包含第一个值也不包含最后一个值。
// 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
请注意,整数范围的实现效率很高,创建了一个轻量级的 Java 对象,其中包含 from 和 to 值。
范围可用于实现 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}"
}
但或者,您可以通过使用 each
方法迭代范围,以更具 Groovy 惯用风格的方式实现相同的效果
(1..10).each { i ->
println "Hello ${i}"
}
范围也可以用于 switch
语句
switch (years) {
case 1..10: interestRate = 0.076; break;
case 11..25: interestRate = 0.052; break;
default: interestRate = 0.037;
}
集合的语法增强
GPath 支持
由于对列表和映射都支持属性表示法,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]
展开运算符
展开运算符可用于将一个集合“内联”到另一个集合中。它是一种语法糖,通常可以避免调用 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
使用下标运算符切片
您可以使用下标表达式对列表、数组、映射进行索引。有趣的是,在这种情况下,字符串被视为特殊类型的集合
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]
请注意,您可以使用范围来提取集合的一部分
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']
值得注意的是,允许使用负索引,以便更轻松地从集合末尾提取
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"
3.3.3. 使用数组
Groovy 提供了基于 Java 数组的数组支持,并在Groovy 开发工具包中包含了一些扩展。总体的意图是,无论您是使用数组还是集合,处理聚合的代码都保持不变。
数组
数组字面量
您可以按如下方式创建数组。请注意,当给定显式数组类型时,[]
也用作空数组表达式。
Integer[] nums = [5, 6, 7, 8]
assert nums[1] == 6
assert nums.getAt(2) == 7 // alternative syntax
assert nums[-1] == 8 // negative indices
assert nums instanceof Integer[]
int[] primes = [2, 3, 5, 7] // primitives
assert primes instanceof int[]
def evens = new int[]{2, 4, 6} // alt syntax 1
assert evens instanceof int[]
def odds = [1, 3, 5] as int[] // alt syntax 2
assert odds instanceof int[]
// empty array examples
Integer[] emptyNums = []
assert emptyNums instanceof Integer[] && emptyNums.size() == 0
def emptyStrings = new String[]{} // alternative syntax 1
assert emptyStrings instanceof String[] && emptyStrings.size() == 0
var emptyObjects = new Object[0] // alternative syntax 2
assert emptyObjects instanceof Object[] && emptyObjects.size() == 0
迭代列表
通常通过调用 each
和 eachWithIndex
方法来迭代列表元素,这些方法在列表的每个项上执行代码
String[] vowels = ['a', 'e', 'i', 'o', 'u']
var result = ''
vowels.each {
result += it
}
assert result == 'aeiou'
result = ''
vowels.eachWithIndex { v, i ->
result += v * i // index starts from 0
}
assert result == 'eiiooouuuu'
其他有用的方法
有许多其他 GDK 方法用于处理数组。只需小心阅读文档即可。对于集合,有一些变异方法会改变原始集合,而另一些方法会产生新集合,使原始集合保持不变。由于数组的大小是固定的,我们不会期望会改变数组大小的变异方法。相反,这些方法通常会返回集合。以下是一些有趣的数组 GDK 方法
int[] nums = [1, 2, 3]
def doubled = nums.collect { it * 2 }
assert doubled == [2, 4, 6] && doubled instanceof List
def tripled = nums*.multiply(3)
assert tripled == [3, 6, 9] && doubled instanceof List
assert nums.any{ it > 2 }
assert nums.every{ it < 4 }
assert nums.average() == 2
assert nums.min() == 1
assert nums.max() == 3
assert nums.sum() == 6
assert nums.indices == [0, 1, 2]
assert nums.swap(0, 2) == [3, 2, 1] as int[]
3.3.4. 使用旧版 Date/Calendar 类型
groovy-dateutil
模块支持许多用于处理 Java 经典 Date
和 Calendar
类的扩展。
您可以使用带有 Calendar
类的常量字段号的普通数组索引表示法访问 Date
或 Calendar
的属性,如下例所示
import static java.util.Calendar.* (1)
def cal = Calendar.instance
cal[YEAR] = 2000 (2)
cal[MONTH] = JANUARY (2)
cal[DAY_OF_MONTH] = 1 (2)
assert cal[DAY_OF_WEEK] == SATURDAY (3)
1 | 导入常量 |
2 | 设置日历的年、月和日 |
3 | 访问日历的星期几 |
Groovy 支持 Date
和 Calendar
实例之间的算术运算和迭代,如下例所示
def utc = TimeZone.getTimeZone('UTC')
Date date = Date.parse("yyyy-MM-dd HH:mm", "2010-05-23 09:01", utc)
def prev = date - 1
def next = date + 1
def diffInDays = next - prev
assert diffInDays == 2
int count = 0
prev.upto(next) { count++ }
assert count == 3
您可以将字符串解析为日期,并将日期输出为格式化的字符串
def orig = '2000-01-01'
def newYear = Date.parse('yyyy-MM-dd', orig)
assert newYear[DAY_OF_WEEK] == SATURDAY
assert newYear.format('yyyy-MM-dd') == orig
assert newYear.format('dd/MM/yyyy') == '01/01/2000'
您还可以根据现有日期或日历创建新的日期或日历
def newYear = Date.parse('yyyy-MM-dd', '2000-01-01')
def newYearsEve = newYear.copyWith(
year: 1999,
month: DECEMBER,
dayOfMonth: 31
)
assert newYearsEve[DAY_OF_WEEK] == FRIDAY
3.3.5. 使用日期/时间类型
groovy-datetime
模块支持许多用于处理 Java 8 中引入的 日期/时间 API 的扩展。本文档将此 API 定义的数据类型称为“JSR 310 类型”。
格式化和解析
处理日期/时间类型时常见的用例是将其转换为字符串(格式化)和从字符串转换(解析)。Groovy 提供了这些额外的格式化方法
方法 | 描述 | 示例 |
---|---|---|
|
对于 |
|
对于 |
|
|
对于 |
|
|
|
对于 |
|
对于 |
|
|
对于 |
|
|
|
对于 |
|
对于 |
|
|
对于 |
|
|
|
对于 |
|
对于 |
|
|
对于 |
|
|
|
|
为了解析,Groovy 为许多 JSR 310 类型添加了一个静态的 parse
方法。该方法接受两个参数:要格式化的值和要使用的模式。该模式由 java.time.format.DateTimeFormatter
API 定义。举例来说
def date = LocalDate.parse('Jun 3, 04', 'MMM d, yy')
assert date == LocalDate.of(2004, Month.JUNE, 3)
def time = LocalTime.parse('4:45', 'H:mm')
assert time == LocalTime.of(4, 45, 0)
def offsetTime = OffsetTime.parse('09:47:51-1234', 'HH:mm:ssZ')
assert offsetTime == OffsetTime.of(9, 47, 51, 0, ZoneOffset.ofHoursMinutes(-12, -34))
def dateTime = ZonedDateTime.parse('2017/07/11 9:47PM Pacific Standard Time', 'yyyy/MM/dd h:mma zzzz')
assert dateTime == ZonedDateTime.of(
LocalDate.of(2017, 7, 11),
LocalTime.of(21, 47, 0),
ZoneId.of('America/Los_Angeles')
)
请注意,这些 parse
方法的参数顺序与 Groovy 添加到 java.util.Date
的静态 parse
方法的参数顺序不同。这样做是为了与日期/时间 API 的现有 parse
方法保持一致。
操作日期/时间
加法和减法
Temporal
类型具有 plus
和 minus
方法,用于添加或减去提供的 java.time.temporal.TemporalAmount
参数。由于 Groovy 将 +
和 -
运算符映射到这些名称的单参数方法,因此可以使用更自然的表达式语法进行加减运算。
def aprilFools = LocalDate.of(2018, Month.APRIL, 1)
def nextAprilFools = aprilFools + Period.ofDays(365) // add 365 days
assert nextAprilFools.year == 2019
def idesOfMarch = aprilFools - Period.ofDays(17) // subtract 17 days
assert idesOfMarch.dayOfMonth == 15
assert idesOfMarch.month == Month.MARCH
Groovy 提供了额外的 plus
和 minus
方法,它们接受一个整数参数,从而可以更简洁地重写上述内容
def nextAprilFools = aprilFools + 365 // add 365 days
def idesOfMarch = aprilFools - 17 // subtract 17 days
这些整数的单位取决于 JSR 310 类型操作数。如上所示,与 ChronoLocalDate
类型(如 LocalDate
)一起使用的整数的单位是天。与 Year
和 YearMonth
一起使用的整数的单位分别是年和月。所有其他类型的单位都是秒,例如 LocalTime
def mars = LocalTime.of(12, 34, 56) // 12:34:56 pm
def thirtySecondsToMars = mars - 30 // go back 30 seconds
assert thirtySecondsToMars.second == 26
乘法和除法
*
运算符可用于将 Period
和 Duration
实例乘以整数值;/
运算符可用于将 Duration
实例除以整数值。
def period = Period.ofMonths(1) * 2 // a 1-month period times 2
assert period.months == 2
def duration = Duration.ofSeconds(10) / 5// a 10-second duration divided by 5
assert duration.seconds == 2
递增和递减
++
和 --
运算符可用于将日期/时间值递增和递减一个单位。由于 JSR 310 类型是不可变的,因此该操作将创建一个具有递增/递减值的新实例并将其重新分配给引用。
def year = Year.of(2000)
--year // decrement by one year
assert year.value == 1999
def offsetTime = OffsetTime.of(0, 0, 0, 0, ZoneOffset.UTC) // 00:00:00.000 UTC
offsetTime++ // increment by one second
assert offsetTime.second == 1
否定
Duration
和 Period
类型表示时间的正负长度。这些可以使用一元 -
运算符进行否定。
def duration = Duration.ofSeconds(-15)
def negated = -duration
assert negated.seconds == 15
与日期/时间值交互
属性表示法
TemporalAccessor
类型的 getLong(TemporalField)
方法(例如 LocalDate
、LocalTime
、ZonedDateTime
等)和 TemporalAmount
类型的 get(TemporalUnit)
方法(即 Period
和 Duration
)可以使用 Groovy 的属性表示法调用。例如
def date = LocalDate.of(2018, Month.MARCH, 12)
assert date[ChronoField.YEAR] == 2018
assert date[ChronoField.MONTH_OF_YEAR] == Month.MARCH.value
assert date[ChronoField.DAY_OF_MONTH] == 12
assert date[ChronoField.DAY_OF_WEEK] == DayOfWeek.MONDAY.value
def period = Period.ofYears(2).withMonths(4).withDays(6)
assert period[ChronoUnit.YEARS] == 2
assert period[ChronoUnit.MONTHS] == 4
assert period[ChronoUnit.DAYS] == 6
范围、upto
和 downto
JSR 310 类型可以与范围运算符一起使用。以下示例迭代今天和从现在起六天的 LocalDate
,打印出每次迭代的星期几。由于两个范围边界都是包含性的,因此这会打印出星期中的所有七天。
def start = LocalDate.now()
def end = start + 6 // 6 days later
(start..end).each { date ->
println date.dayOfWeek
}
upto
方法将实现与上述示例中的范围相同的功能。upto
方法从较早的 start
值(包含)迭代到较晚的 end
值(也包含),每次迭代都使用递增的 next
值调用闭包。
def start = LocalDate.now()
def end = start + 6 // 6 days later
start.upto(end) { next ->
println next.dayOfWeek
}
downto
方法则朝相反方向迭代,从较晚的 start
值到较早的 end
值。
upto
、downto
和范围的迭代单位与加减法的单位相同:LocalDate
每次迭代一天,YearMonth
每次迭代一个月,Year
每次迭代一年,其他所有类型每次迭代一秒。这两个方法还支持一个可选的 TemporalUnit
参数来更改迭代单位。
考虑以下示例,其中 2018 年 3 月 1 日使用月的迭代单位迭代到 2018 年 3 月 2 日。
def start = LocalDate.of(2018, Month.MARCH, 1)
def end = start + 1 // 1 day later
int iterationCount = 0
start.upto(end, ChronoUnit.MONTHS) { next ->
println next
++iterationCount
}
assert iterationCount == 1
由于 start
日期是包含的,闭包将以 3 月 1 日的 next
日期值调用。然后 upto
方法将日期递增一个月,得到 4 月 1 日。因为此日期在指定的 3 月 2 日 end
日期**之后**,迭代立即停止,只调用了一次闭包。此行为与 downto
方法相同,只是迭代将在 next
的值早于目标 end
日期时立即停止。
简而言之,当使用 upto
或 downto
方法进行自定义迭代单位迭代时,迭代的当前值永远不会超过结束值。
组合日期/时间值
左移运算符 (<<
) 可用于将两个 JSR 310 类型组合成一个聚合类型。例如,可以将 LocalDate
左移到 LocalTime
中,以生成复合 LocalDateTime
实例。
MonthDay monthDay = Month.JUNE << 3 // June 3rd
LocalDate date = monthDay << Year.of(2015) // 3-Jun-2015
LocalDateTime dateTime = date << LocalTime.NOON // 3-Jun-2015 @ 12pm
OffsetDateTime offsetDateTime = dateTime << ZoneOffset.ofHours(-5) // 3-Jun-2015 @ 12pm UTC-5
左移运算符是自反的;操作数的顺序无关紧要。
def year = Year.of(2000)
def month = Month.DECEMBER
YearMonth a = year << month
YearMonth b = month << year
assert a == b
创建周期和持续时间
右移运算符 (>>
) 产生一个表示操作数之间周期或持续时间的值。对于 ChronoLocalDate
、YearMonth
和 Year
,该运算符产生一个 Period
实例
def newYears = LocalDate.of(2018, Month.JANUARY, 1)
def aprilFools = LocalDate.of(2018, Month.APRIL, 1)
def period = newYears >> aprilFools
assert period instanceof Period
assert period.months == 3
该运算符为时间感知 JSR 类型生成 Duration
def duration = LocalTime.NOON >> (LocalTime.NOON + 30)
assert duration instanceof Duration
assert duration.seconds == 30
如果运算符左侧的值早于右侧的值,则结果为正。如果左侧的值晚于右侧的值,则结果为负
def decade = Year.of(2010) >> Year.of(2000)
assert decade.years == -10
旧版和 JSR 310 类型之间的转换
尽管 java.util
包中的 Date
、Calendar
和 TimeZone
类型存在缺点,但它们在 Java API 中相当常见(至少在 Java 8 之前的 API 中)。为了适应这些 API 的使用,Groovy 提供了在 JSR 310 类型和旧版类型之间转换的方法。
大多数 JSR 类型都配备了 toDate()
和 toCalendar()
方法,用于转换为相对等效的 java.util.Date
和 java.util.Calendar
值。ZoneId
和 ZoneOffset
都提供了 toTimeZone()
方法,用于转换为 java.util.TimeZone
。
// LocalDate to java.util.Date
def valentines = LocalDate.of(2018, Month.FEBRUARY, 14)
assert valentines.toDate().format('MMMM dd, yyyy') == 'February 14, 2018'
// LocalTime to java.util.Date
def noon = LocalTime.of(12, 0, 0)
assert noon.toDate().format('HH:mm:ss') == '12:00:00'
// ZoneId to java.util.TimeZone
def newYork = ZoneId.of('America/New_York')
assert newYork.toTimeZone() == TimeZone.getTimeZone('America/New_York')
// ZonedDateTime to java.util.Calendar
def valAtNoonInNY = ZonedDateTime.of(valentines, noon, newYork)
assert valAtNoonInNY.toCalendar().getTimeZone().toZoneId() == newYork
请注意,转换为旧版类型时
-
纳秒值被截断为毫秒。例如,
LocalTime
的ChronoUnit.NANOS
值为 999,999,999 纳秒,转换为 999 毫秒。 -
转换“本地”类型(
LocalDate
、LocalTime
和LocalDateTime
)时,返回的Date
或Calendar
的时区将是系统默认时区。 -
当转换仅时间类型(
LocalTime
或OffsetTime
)时,Date
或Calendar
的年/月/日将设置为当前日期。 -
当将
LocalDate
转换为Date
或Calendar
时,其时间值将清零,即00:00:00.000
。 -
当将
OffsetDateTime
转换为Calendar
时,只有ZoneOffset
的小时和分钟会传递到相应的TimeZone
。幸运的是,秒数非零的区域偏移量很少见。
Groovy 已向 Date
和 Calendar
添加了许多方法,用于转换为各种 JSR 310 类型
Date legacy = Date.parse('yyyy-MM-dd HH:mm:ss.SSS', '2010-04-03 10:30:58.999')
assert legacy.toLocalDate() == LocalDate.of(2010, 4, 3)
assert legacy.toLocalTime() == LocalTime.of(10, 30, 58, 999_000_000) // 999M ns = 999ms
assert legacy.toOffsetTime().hour == 10
assert legacy.toYear() == Year.of(2010)
assert legacy.toMonth() == Month.APRIL
assert legacy.toDayOfWeek() == DayOfWeek.SATURDAY
assert legacy.toMonthDay() == MonthDay.of(Month.APRIL, 3)
assert legacy.toYearMonth() == YearMonth.of(2010, Month.APRIL)
assert legacy.toLocalDateTime().year == 2010
assert legacy.toOffsetDateTime().dayOfMonth == 3
assert legacy.toZonedDateTime().zone == ZoneId.systemDefault()
3.3.6. 便捷工具
ConfigSlurper
ConfigSlurper
是一个实用类,用于读取以 Groovy 脚本形式定义的配置文件。与 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
。environments
方法可用于传递一个闭包实例,该实例本身可能包含多个部分。假设我们想为开发环境创建特定的配置值。在创建 ConfigSlurper
实例时,我们可以使用 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 环境不受任何特定环境名称的限制。它完全取决于 ConfigSlurper 客户端代码支持和相应解释哪些值。 |
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
实例期间,配置值会转换为 String
实例。
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
类可用于创建动态可扩展对象。尽管其名称如此,但它底层并不使用 ExpandoMetaClass
。每个 Expando
对象都表示一个独立的、动态创建的实例,可以在运行时通过属性(或方法)进行扩展。
def expando = new Expando()
expando.name = 'John'
assert expando.name == 'John'
当动态属性注册一个 Closure
代码块时,会发生一个特殊情况。一旦注册,就可以像调用方法一样调用它。
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'
可观察列表、映射和集合
Groovy 提供了可观察列表、映射和集合。当添加、删除或更改元素时,这些集合中的每一个都会触发 java.beans.PropertyChangeEvent
事件。请注意,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 | 从给定列表创建 ObservableList |
4 | 注册监听器 |
5 | 触发 ObservableList.ElementAddedEvent 事件 |
请注意,添加元素实际上会触发两个事件。第一个是 ObservableList.ElementAddedEvent 类型,第二个是普通的 PropertyChangeEvent ,它通知监听器 size 属性的更改。 |
ObservableList.ElementClearedEvent
事件类型是另一个有趣的类型。当删除多个元素时,例如调用 clear()
时,它会保存从列表中删除的元素。
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
要获取所有受支持事件类型的概述,建议读者查看 JavaDoc 文档或正在使用的可观察集合的源代码。
ObservableMap
和 ObservableSet
具有与本节中 ObservableList
相同的概念。
3.4. 元编程
Groovy 语言支持两种元编程:运行时和编译时。前者允许在运行时改变类模型和程序行为,而后者仅在编译时发生。两者都有优缺点,我们将在本节中详细介绍。
3.4.1. 运行时元编程
通过运行时元编程,我们可以将拦截、注入甚至合成类和接口方法延迟到运行时决定。为了深入理解 Groovy 的元对象协议 (MOP),我们需要理解 Groovy 对象和 Groovy 的方法处理。在 Groovy 中,我们处理三种对象:POJO、POGO 和 Groovy 拦截器。Groovy 允许对所有类型的对象进行元编程,但方式不同。
-
POJO - 一个普通的 Java 对象,其类可以用 Java 或 JVM 的任何其他语言编写。
-
POGO - 一个 Groovy 对象,其类用 Groovy 编写。它默认扩展
java.lang.Object
并实现 groovy.lang.GroovyObject 接口。 -
Groovy 拦截器 - 实现 groovy.lang.GroovyInterceptable 接口并具有方法拦截能力的 Groovy 对象,这将在 GroovyInterceptable 部分讨论。
对于每个方法调用,Groovy 都会检查对象是 POJO 还是 POGO。对于 POJO,Groovy 从 groovy.lang.MetaClassRegistry 获取其 MetaClass
并将方法调用委托给它。对于 POGO,Groovy 会执行更多步骤,如下图所示

GroovyObject 接口
groovy.lang.GroovyObject 是 Groovy 中的主要接口,就像 Object
类在 Java 中一样。GroovyObject
在 groovy.lang.GroovyObjectSupport 类中有一个默认实现,它负责将调用转移到 groovy.lang.MetaClass 对象。GroovyObject
源代码如下所示
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
此方法主要用于与 GroovyInterceptable 接口或对象的 MetaClass
结合使用,它将拦截所有方法调用。
当调用的方法不存在于 Groovy 对象上时,也会调用它。这是一个使用重写 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 []'
但是,不鼓励使用 invokeMethod
拦截缺失方法。在只想在方法调度失败时拦截方法调用的情况下,请改用 methodMissing。
get/setProperty
通过重写当前对象的 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 | 将请求转发给所有属性(除了 field3 )的 getter。 |
您可以通过覆盖 setProperty()
方法来拦截属性的写入访问
class POGO {
String property
void setProperty(String name, Object value) {
this.@"$name" = 'overridden'
}
}
def pogo = new POGO()
pogo.property = 'a'
assert pogo.property == 'overridden'
get/setMetaClass
您可以访问对象的 metaClass
或设置您自己的 MetaClass
实现来更改默认的拦截机制。例如,您可以编写自己的 MetaClass
接口实现并将其分配给对象以更改拦截机制
// getMetaclass
someObject.metaClass
// setMetaClass
someObject.metaClass = new OwnMetaClassImplementation()
您可以在 GroovyInterceptable 主题中找到更多示例。 |
get/setAttribute
此功能与 MetaClass
实现相关。在默认实现中,您可以在不调用其 getter 和 setter 的情况下访问字段。以下示例演示了此方法
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
的概念。此方法与 invokeMethod
的不同之处在于,它仅在方法调度失败时调用,即当无法找到给定名称和/或给定参数的方法时
class Foo {
def methodMissing(String name, def args) {
return "this is me"
}
}
assert new Foo().someUnknownMethod(42l) == 'this is me'
通常在使用 methodMissing
时,可以将结果缓存起来,以便下次调用相同方法时使用。
例如,考虑 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
Groovy 支持 propertyMissing
的概念,用于拦截否则会失败的属性解析尝试。在 getter 方法的情况下,propertyMissing
接受一个包含属性名称的 String
参数
class Foo {
def propertyMissing(String name) { name }
}
assert new Foo().boo == 'boo'
仅当 Groovy 运行时找不到给定属性的 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
methodMissing
方法的静态变体可以通过 ExpandoMetaClass 添加,或者可以通过 $static_methodMissing
方法在类级别实现。
class Foo {
static def $static_methodMissing(String name, Object args) {
return "Missing static method name is $name"
}
}
assert Foo.bar() == 'Missing static method name is bar'
静态 propertyMissing
propertyMissing
方法的静态变体可以通过 ExpandoMetaClass 添加,或者可以通过 $static_propertyMissing
方法在类级别实现。
class Foo {
static def $static_propertyMissing(String name) {
return "Missing static property name is $name"
}
}
assert Foo.foobar == 'Missing static property name is foobar'
GroovyInterceptable
groovy.lang.GroovyInterceptable
接口是一个标记接口,它扩展了 GroovyObject
,用于通知 Groovy 运行时所有方法都应通过 Groovy 运行时的方法分派机制进行拦截。
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'
}
}
我们不能使用默认的 Groovy 方法,如 println ,因为这些方法被注入到所有 Groovy 对象中,所以它们也会被拦截。 |
如果我们想拦截所有方法调用,但又不想实现 GroovyInterceptable
接口,我们可以在对象的 MetaClass
上实现 invokeMethod()
。这种方法适用于 POGO 和 POJO,如下面的示例所示:
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 的更多信息,请参阅元类部分。 |
类别(Categories)
在某些情况下,如果一个不受控制的类有额外的方法会很有用。为了实现这个功能,Groovy 实现了一个借鉴自 Objective-C 的特性,称为*类别(Categories)*。
类别是通过所谓的*类别类*实现的。类别类是特殊的,因为它需要满足某些预定义的规则来定义扩展方法。
系统包含了一些类别,用于为类添加功能,使其在 Groovy 环境中更具可用性:
类别类默认情况下不启用。要使用类别类中定义的方法,需要应用由 GDK 提供且在每个 Groovy 对象实例中都可用的作用域 use
方法。
use(TimeCategory) {
println 1.minute.from.now (1)
println 10.hours.ago
def someDate = new Date() (2)
println someDate - 3.months
}
1 | TimeCategory 向 Integer 添加方法 |
2 | TimeCategory 向 Date 添加方法 |
use
方法将类别类作为第一个参数,将闭包代码块作为第二个参数。在 Closure
内部可以访问类别方法。如上例所示,即使是 java.lang.Integer
或 java.util.Date
等 JDK 类也可以通过用户定义的方法进行丰富。
类别无需直接暴露给用户代码,以下方式也可以:
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
方法。事实上,这是类别类必须满足的要求之一,才能使其方法在 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
注解,它可以在编译时将带注解的类转换为类别类。
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)
如前所述,元类在方法解析中扮演着核心角色。对于来自 Groovy 代码的每次方法调用,Groovy 将为给定对象找到 MetaClass
并通过 groovy.lang.MetaClass#invokeMethod(java.lang.Class,java.lang.Object,java.lang.String,java.lang.Object,boolean,boolean) 将方法解析委托给元类,这不应与 groovy.lang.GroovyObject#invokeMethod(java.lang.String,java.lang.Object) 混淆,后者是元类最终可能调用的方法。
默认元类 MetaClassImpl
默认情况下,对象获得一个 MetaClassImpl
实例,它实现默认的方法查找。此方法查找包括在对象类中查找方法(“常规”方法),但如果以这种方式找不到方法,它将诉诸于调用 methodMissing
,最终调用 groovy.lang.GroovyObject#invokeMethod(java.lang.String,java.lang.Object)。
class Foo {}
def f = new Foo()
assert f.metaClass =~ /MetaClassImpl/
自定义元类
您可以更改任何对象或类的元类,并将其替换为 MetaClass
groovy.lang.MetaClass 的自定义实现。通常您会希望扩展现有元类之一,例如 MetaClassImpl
、DelegatingMetaClass
、ExpandoMetaClass
或 ProxyMetaClass
;否则您将需要实现完整的方法查找逻辑。在使用新的元类实例之前,您应该调用 groovy.lang.MetaClass#initialize(),否则元类可能不会按预期运行。
如果您只需要装饰现有元类,DelegatingMetaClass
简化了该用例。旧的元类实现仍然可以通过 super
访问,这使得对输入应用预转换、路由到其他方法和后处理输出变得容易。
class Foo { def bar() { "bar" } }
class MyFooMetaClass extends DelegatingMetaClass {
MyFooMetaClass(MetaClass metaClass) { super(metaClass) }
MyFooMetaClass(Class theClass) { super(theClass) }
Object invokeMethod(Object object, String methodName, Object[] args) {
def result = super.invokeMethod(object,methodName.toLowerCase(), args)
result.toUpperCase();
}
}
def mc = new MyFooMetaClass(Foo.metaClass)
mc.initialize()
Foo.metaClass = mc
def f = new Foo()
assert f.BAR() == "BAR" // the new metaclass routes .BAR() to .bar() and uppercases the result
可以在启动时通过给元类一个特殊构造的(魔法)类名和包名来改变元类。为了改变 java.lang.Integer
的元类,只需将类 groovy.runtime.metaclass.java.lang.IntegerMetaClass
放在类路径中即可。这很有用,例如,当使用框架时,如果您想在您的代码被框架执行之前进行元类更改。魔法包的通用形式是 groovy.runtime.metaclass.[package].[class]MetaClass
。在下面的示例中,[package]
是 java.lang
,[class]
是 Integer
。
// file: IntegerMetaClass.groovy
package groovy.runtime.metaclass.java.lang;
class IntegerMetaClass extends DelegatingMetaClass {
IntegerMetaClass(MetaClass metaClass) { super(metaClass) }
IntegerMetaClass(Class theClass) { super(theClass) }
Object invokeMethod(Object object, String name, Object[] args) {
if (name =~ /isBiggerThan/) {
def other = name.split(/isBiggerThan/)[1].toInteger()
object > other
} else {
return super.invokeMethod(object,name, args);
}
}
}
通过使用 groovyc IntegerMetaClass.groovy
编译上述文件,将生成 ./groovy/runtime/metaclass/java/lang/IntegerMetaClass.class
。下面的示例将使用这个新的元类:
// File testInteger.groovy
def i = 10
assert i.isBiggerThan5()
assert !i.isBiggerThan15()
println i.isBiggerThan5()
通过使用 groovy -cp . testInteger.groovy
运行该文件,IntegerMetaClass
将在类路径中,因此它将成为 java.lang.Integer
的元类,拦截对 isBiggerThan*()
方法的调用。
每个实例的元类
您可以单独更改单个对象的元类,因此可以有多个具有不同元类的相同类的对象。
class Foo { def bar() { "bar" }}
class FooMetaClass extends DelegatingMetaClass {
FooMetaClass(MetaClass metaClass) { super(metaClass) }
Object invokeMethod(Object object, String name, Object[] args) {
super.invokeMethod(object,name,args).toUpperCase()
}
}
def f1 = new Foo()
def f2 = new Foo()
f2.metaClass = new FooMetaClass(f2.metaClass)
assert f1.bar() == "bar"
assert f2.bar() == "BAR"
assert f1.metaClass =~ /MetaClassImpl/
assert f2.metaClass =~ /FooMetaClass/
assert f1.class.toString() == "class Foo"
assert f2.class.toString() == "class Foo"
ExpandoMetaClass
Groovy 提供了一个特殊的 MetaClass
,即所谓的 ExpandoMetaClass
。它特殊之处在于允许通过简洁的闭包语法动态添加或更改方法、构造函数、属性甚至静态方法。
如测试指南所示,在模拟或存根场景中,应用这些修改特别有用。
Groovy 为每个 java.lang.Class
提供了一个特殊的 metaClass
属性,它将为您提供一个 ExpandoMetaClass
实例的引用。然后可以使用此实例添加方法或更改现有方法的行为。
默认情况下,ExpandoMetaClass 不支持继承。要启用此功能,您必须在应用程序启动之前调用 ExpandoMetaClass#enableGlobally() ,例如在主方法或 servlet 引导程序中。 |
以下部分详细介绍了 ExpandoMetaClass
在各种场景中的使用方法。
一旦通过调用 metaClass
属性访问 ExpandoMetaClass
,就可以使用左移 <<
或 =
运算符添加方法。
请注意,左移运算符用于*追加*新方法。如果类或接口声明了具有相同名称和参数类型的公共方法,包括从超类和超接口继承的方法,但不包括在运行时添加到 metaClass 的方法,则会抛出异常。如果要*替换*类或接口声明的方法,可以使用 = 运算符。 |
这些运算符应用于 metaClass
的不存在属性,并传递 Closure
代码块的实例。
class Book {
String title
}
Book.metaClass.titleInUpperCase << {-> title.toUpperCase() }
def b = new Book(title:"The Stand")
assert "THE STAND" == b.titleInUpperCase()
上面的示例展示了如何通过访问 metaClass
属性并使用 <<
或 =
运算符来分配 Closure
代码块,从而向类添加新方法。Closure
参数被解释为方法参数。可以通过使用 {→ …}
语法添加无参数方法。
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 容器中,一种方法可能是将值作为请求属性存储在当前正在执行的请求中(就像 Grails 中的某些情况那样)。
可以使用特殊的 constructor
属性添加构造函数。可以使用 <<
或 =
运算符来分配 Closure
代码块。Closure
参数在运行时执行代码时将成为构造函数参数。
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
,可以使用 Groovy 的方法指针语法从其他类借用方法。
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 允许您将字符串用作属性名称,因此您可以动态创建运行时方法和属性名称。要创建具有动态名称的方法,只需使用将属性名称作为字符串引用的语言功能即可。
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 Web 应用程序框架中找到。“动态编解码器”的概念是通过使用动态方法名实现的。
class HTMLCodec {
static encode = { theTarget ->
HtmlUtils.htmlEscape(theTarget.toString())
}
static decode = { theTarget ->
HtmlUtils.htmlUnescape(theTarget.toString())
}
}
上例展示了一个编解码器实现。Grails 提供了各种编解码器实现,每个都定义在一个单独的类中。在运行时,应用程序类路径中会有多个编解码器类。在应用程序启动时,框架会将 encodeXXX
和 decodeXXX
方法添加到某些元类中,其中 XXX
是编解码器类名的第一部分(例如 encodeHTML
)。这种机制在下面以一些 Groovy 伪代码的形式展示:
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。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()
闭包代码的第一步是查找给定名称和参数的 MetaMethod
。如果找到该方法,则一切正常并将其委托给该方法。如果没有,则返回一个虚拟值。
MetaMethod 是已知存在于 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
这里需要注意的是,不是查找 MetaMethod
,而是查找 MetaProperty
实例。如果存在,则调用 MetaProperty
的 getProperty
方法,并传递委托对象。
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
以检索静态 MetaMethod
实例。
可以使用 ExpandoMetaClass
向接口添加方法。但是,要做到这一点,它必须在应用程序启动之前使用 ExpandoMetaClass.enableGlobally()
方法全局启用。
List.metaClass.sizeDoubled = {-> delegate.size() * 2 }
def list = []
list << 1
list << 2
assert 4 == list.sizeDoubled()
扩展模块
扩展现有类
扩展模块允许您向现有类添加新方法,包括预编译的类,如 JDK 中的类。这些新方法与通过元类或使用类别定义的方法不同,它们是全局可用的。例如,当您编写:
def file = new File(...)
def contents = file.getText('utf-8')
getText
方法在 File
类上不存在。然而,Groovy 知道它,因为它是在一个特殊的类 ResourceGroovyMethods
中定义的。
public static String getText(File file, String charset) throws IOException {
return IOGroovyMethods.getText(newReader(file, charset));
}
您可能会注意到扩展方法是使用帮助类中的静态方法定义的(其中定义了各种扩展方法)。getText
方法的第一个参数对应于接收者,而附加参数对应于扩展方法的参数。因此,在这里,我们正在 File
类上定义一个名为 getText 的方法(因为第一个参数是 File
类型),它接受一个参数(编码 String
)。
创建扩展模块的过程很简单:
-
像上面一样编写一个扩展类
-
编写一个模块描述符文件
然后您必须使扩展模块对 Groovy 可见,这就像在类路径中提供扩展模块类和描述符一样简单。这意味着您有两种选择:
-
直接在类路径中提供类和模块描述符
-
或将您的扩展模块打包成 jar 以便重用
扩展模块可以向类添加两种方法:
-
实例方法(在类的实例上调用)
-
静态方法(在类本身上调用)
实例方法
要向现有类添加实例方法,您需要创建一个扩展类。例如,假设您想在 Integer
上添加一个 maxRetries
方法,该方法接受一个闭包并最多执行 *n* 次,直到不再抛出异常。要做到这一点,您只需编写以下代码:
class MaxRetriesExtension { (1)
static void maxRetries(Integer self, Closure code) { (2)
assert self >= 0
int retries = self
Throwable e = null
while (retries > 0) {
try {
code.call()
break
} catch (Throwable err) {
e = err
retries--
}
}
if (retries == 0 && e) {
throw e
}
}
}
1 | 扩展类 |
2 | 静态方法的第一个参数对应于消息的接收者,即扩展实例。 |
然后,在声明您的扩展类之后,您可以这样调用它:
int i=0
5.maxRetries {
i++
}
assert i == 1
i=0
try {
5.maxRetries {
i++
throw new RuntimeException("oops")
}
} catch (RuntimeException e) {
assert i == 5
}
静态方法
也可以向类添加静态方法。在这种情况下,静态方法需要在其**自己的**文件中定义。静态和实例扩展方法**不能**存在于同一个类中。
class StaticStringExtension { (1)
static String greeting(String self) { (2)
'Hello, world!'
}
}
1 | 静态扩展类 |
2 | 静态方法的第一个参数对应于被扩展的类,并且**未使用**。 |
在这种情况下,您可以直接在 String
类上调用它:
assert String.greeting() == 'Hello, world!'
模块描述符
为了让 Groovy 能够加载您的扩展方法,您必须声明您的扩展辅助类。您必须在 META-INF/groovy
目录中创建一个名为 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:静态方法的扩展辅助类列表。您可以提供多个类,用逗号分隔。
请注意,模块不要求同时定义静态辅助类和实例辅助类,并且您可以向单个模块添加多个类。您也可以在单个模块中扩展不同的类,而不会出现问题。甚至可以在单个扩展类中使用不同的类,但建议按功能集将扩展方法分组到类中。
扩展模块和类路径
值得注意的是,您不能同时编译和使用扩展。这意味着要使用扩展,它**必须**在类路径中以编译后的类的形式提供,然后使用它的代码才能被编译。通常,这意味着您不能在与扩展类本身相同的源单元中包含*测试*类。由于通常测试源与正常源分离并在构建的另一个步骤中执行,因此这不是问题。
与类型检查的兼容性
与类别不同,扩展模块与类型检查兼容:如果在类路径中找到它们,则类型检查器会知道扩展方法,并且在您调用它们时不会抱怨。它还与静态编译兼容。
3.4.2. 编译时元编程
Groovy 中的编译时元编程允许在编译时生成代码。这些转换会改变程序的抽象语法树(AST),这就是为什么在 Groovy 中我们将其称为 AST 转换。AST 转换允许您挂接到编译过程,修改 AST 并继续编译过程以生成常规字节码。与运行时元编程相比,这具有将更改在类文件本身中可见的优点(即在字节码中)。在字节码中可见很重要,例如,如果您希望转换成为类契约的一部分(实现接口、扩展抽象类等),或者如果您需要您的类可以从 Java(或其他 JVM 语言)调用。例如,AST 转换可以向类添加方法。如果您使用运行时元编程来完成,新方法将只能从 Groovy 中可见。如果您使用编译时元编程来完成相同的操作,该方法也将从 Java 中可见。最后但并非最不重要的一点是,编译时元编程的性能可能会更好(因为不需要初始化阶段)。
在本节中,我们将首先解释 Groovy 发行版中捆绑的各种编译时转换。在后续章节中,我们将描述如何实现您自己的 AST 转换以及这种技术的缺点。
可用的 AST 转换
Groovy 提供了各种 AST 转换,涵盖了不同的需求:减少样板(代码生成)、实现设计模式(委托等)、日志记录、声明式并发、克隆、更安全的脚本编写、调整编译、实现 Swing 模式、测试以及最终管理依赖项。如果这些 AST 转换都不能满足您的需求,您仍然可以实现自己的转换,如开发您自己的 AST 转换一节所示。
AST 转换可以分为两类:
-
全局 AST 转换:只要在编译类路径中找到,就会透明地全局应用。
-
局部 AST 转换:通过使用标记注释源代码来应用。与全局 AST 转换不同,局部 AST 转换可能支持参数。
Groovy 不附带任何全局 AST 转换,但您可以在此处找到可在代码中使用的局部 AST 转换列表:
代码生成转换
这类转换包括有助于消除样板代码的 AST 转换。这通常是您必须编写但没有任何有用信息的代码。通过自动生成此样板代码,您必须编写的代码保持干净简洁,并且由于样板代码不正确而引入错误的可能性降低。
@groovy.transform.ToString
@ToString
AST 转换生成类的可读 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
注解接受几个参数,总结在下表中:
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
excludes |
空列表 |
要从 toString 中排除的属性列表 |
|
includes |
未定义标记列表(表示所有字段) |
要包含在 toString 中的字段列表 |
|
includeSuper |
False |
ToString 中是否包含超类 |
|
includeNames |
false |
是否在生成的 toString 中包含属性名称。 |
|
includeFields |
False |
除了属性之外,是否在 toString 中包含字段 |
|
includeSuperProperties |
False |
ToString 中是否包含超属性 |
|
includeSuperFields |
False |
ToString 中是否包含可见的超字段 |
|
ignoreNulls |
False |
是否显示值为 null 的属性/字段 |
|
includePackage |
True |
在 toString 中使用完全限定类名而不是简单名称 |
|
allProperties |
True |
在 toString 中包含所有 JavaBean 属性 |
|
cache |
False |
缓存 toString 字符串。仅当类不可变时才应设置为 true。 |
|
allNames |
False |
生成的 toString 中是否包含具有内部名称的字段和/或属性 |
|
@groovy.transform.EqualsAndHashCode
@EqualsAndHashCode
AST 转换旨在为您生成 equals
和 hashCode
方法。生成的哈希码遵循 Josh Bloch 在 Effective Java 中描述的最佳实践。
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
的行为:
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
excludes |
空列表 |
要从 equals/hashCode 中排除的属性列表 |
|
includes |
未定义标记列表(表示所有字段) |
要包含在 equals/hashCode 中的字段列表 |
|
cache |
False |
缓存 hashCode 计算。仅当类不可变时才应设置为 true。 |
|
callSuper |
False |
是否在 equals 和 hashCode 计算中包含 super |
|
includeFields |
False |
除了属性之外,是否在 equals/hashCode 中包含字段 |
|
useCanEqual |
True |
equals 是否调用 canEqual 辅助方法。 |
|
allProperties |
False |
在 equals 和 hashCode 计算中是否包含 JavaBean 属性 |
|
allNames |
False |
在 equals 和 hashCode 计算中是否包含具有内部名称的字段和/或属性 |
|
@groovy.transform.TupleConstructor
@TupleConstructor
注解旨在通过为您生成构造函数来消除样板代码。它会创建一个元组构造函数,其中每个属性(以及可能每个字段)都有一个参数。每个参数都有一个默认值(如果存在,则使用属性的初始值,否则使用 Java 根据属性类型提供的默认值)。
通常,您不需要理解生成的构造函数的实现细节;您只需以正常方式使用它们。但是,如果您想添加多个构造函数,了解 Java 集成选项或满足某些依赖注入框架的要求,那么一些细节就很有用了。
如前所述,生成的构造函数已应用默认值。在后续编译阶段,Groovy 编译器的标准默认值处理行为将应用于此。最终结果是,多个构造函数被放置在类的字节码中。这提供了明确的语义,并且对于 Java 集成也很有用。例如,以下代码将生成 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')
第一个构造函数是一个无参构造函数,只要您没有最终属性,它就允许传统的映射式构造。Groovy 在内部调用无参构造函数,然后调用相关的 setter。值得注意的是,如果第一个属性(或字段)的类型是 LinkedHashMap,或者只有一个 Map、AbstractMap 或 HashMap 属性(或字段),那么映射式命名参数将不可用。
其他构造函数是按照它们定义的顺序获取属性而生成的。Groovy 将生成与属性(或字段,取决于选项)数量相同的构造函数。
将 defaults
属性(参见可用配置选项表)设置为 false
,将禁用正常的默认值行为,这意味着:
-
只生成一个构造函数
-
尝试使用初始值将导致错误
-
映射式命名参数将不可用
此属性通常仅用于其他 Java 框架期望只有一个构造函数的情况,例如注入框架或 JUnit 参数化运行器。
如果 @TupleConstructor
注解的类上同时存在 @PropertyOptions
注解,则生成的构造函数可能包含自定义属性处理逻辑。例如,@PropertyOptions
注解上的 propertyHandler
属性可以设置为 ImmutablePropertyHandler
,这将导致添加不可变类所需的逻辑(防御性复制、克隆等)。当您使用 @Immutable
元注解时,这通常会自动在幕后发生。某些注解属性可能不支持所有属性处理器。
@TupleConstructor
AST 转换接受以下几个注解属性:
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
excludes |
空列表 |
要从元组构造函数生成中排除的属性列表 |
|
includes |
未定义列表(表示所有字段) |
要包含在元组构造函数生成中的字段列表 |
|
includeProperties |
True |
是否将属性包含在元组构造函数生成中 |
|
includeFields |
False |
除了属性之外,是否将字段包含在元组构造函数生成中 |
|
includeSuperProperties |
True |
是否将超类中的属性包含在元组构造函数生成中 |
|
includeSuperFields |
False |
是否将超类中的字段包含在元组构造函数生成中 |
|
callSuper |
False |
是否在调用父构造函数时调用超属性,而不是将其设置为属性 |
|
force |
False |
默认情况下,如果已定义构造函数,则转换将不执行任何操作。将此属性设置为 true,将生成构造函数,并且您有责任确保没有定义重复的构造函数。 |
|
defaults |
True |
指示构造函数参数启用默认值处理。设置为 false 以获得正好一个构造函数,但禁用初始值支持和命名参数。 |
|
useSetters |
False |
默认情况下,转换将直接从其相应的构造函数参数设置每个属性的后备字段。将此属性设置为 true,如果存在 setter,则构造函数将改为调用 setter。通常认为在构造函数中调用可被覆盖的 setter 是不良风格。避免这种不良风格是您的责任。 |
|
allNames |
False |
构造函数中是否包含具有内部名称的字段和/或属性 |
|
allProperties |
False |
构造函数中是否包含 JavaBean 属性 |
|
pre |
空 |
一个闭包,包含要插入到生成构造函数开头的语句 |
|
post |
空 |
一个闭包,包含要插入到生成构造函数末尾的语句 |
|
将 defaults
注解属性设置为 false
,并将 force
注解属性设置为 true
,允许通过对不同情况使用不同的自定义选项(前提是每种情况都有不同的类型签名)来创建多个元组构造函数,如以下示例所示:
class Named {
String name
}
@ToString(includeSuperProperties=true, ignoreNulls=true, includeNames=true, includeFields=true)
@TupleConstructor(force=true, defaults=false)
@TupleConstructor(force=true, defaults=false, includeFields=true)
@TupleConstructor(force=true, defaults=false, includeSuperProperties=true)
class Book extends Named {
Integer published
private Boolean fiction
Book() {}
}
assert new Book("Regina", 2015).toString() == 'Book(published:2015, name:Regina)'
assert new Book(2015, false).toString() == 'Book(published:2015, fiction:false)'
assert new Book(2015).toString() == 'Book(published:2015)'
assert new Book().toString() == 'Book()'
assert Book.constructors.size() == 4
同样,下面是另一个使用不同 includes
选项的示例:
@ToString(includeSuperProperties=true, ignoreNulls=true, includeNames=true, includeFields=true)
@TupleConstructor(force=true, defaults=false, includes='name,year')
@TupleConstructor(force=true, defaults=false, includes='year,fiction')
@TupleConstructor(force=true, defaults=false, includes='name,fiction')
class Book {
String name
Integer year
Boolean fiction
}
assert new Book("Regina", 2015).toString() == 'Book(name:Regina, year:2015)'
assert new Book(2015, false).toString() == 'Book(year:2015, fiction:false)'
assert new Book("Regina", false).toString() == 'Book(name:Regina, fiction:false)'
assert Book.constructors.size() == 3
@groovy.transform.MapConstructor
@MapConstructor
注解旨在通过为您生成映射构造函数来消除样板代码。生成的映射构造函数将类中的每个属性设置为基于所提供映射中具有属性名称的键的值。用法如本例所示:
import groovy.transform.*
@ToString
@MapConstructor
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack, Nicholson)'
生成的构造函数大致如下所示:
public Person(Map args) {
if (args.containsKey('firstName')) {
this.firstName = args.get('firstName')
}
if (args.containsKey('lastName')) {
this.lastName = args.get('lastName')
}
}
@groovy.transform.Canonical
@Canonical
元注解结合了 @ToString、@EqualsAndHashCode 和 @TupleConstructor 注解。
import groovy.transform.Canonical
@Canonical
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack, Nicholson)' // Effect of @ToString
def p2 = new Person('Jack','Nicholson') // Effect of @TupleConstructor
assert p2.toString() == 'Person(Jack, Nicholson)'
assert p1==p2 // Effect of @EqualsAndHashCode
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode
类似的不可变类可以使用 @Immutable 元注解生成。@Canonical
元注解支持它所聚合的注解中的配置选项。有关更多详细信息,请参阅这些注解。
import groovy.transform.Canonical
@Canonical(excludes=['lastName'])
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack)' // Effect of @ToString(excludes=['lastName'])
def p2 = new Person('Jack') // Effect of @TupleConstructor(excludes=['lastName'])
assert p2.toString() == 'Person(Jack)'
assert p1==p2 // Effect of @EqualsAndHashCode(excludes=['lastName'])
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode(excludes=['lastName'])
@Canonical
元注解可以与显式使用一个或多个其组件注解结合使用,如下所示:
import groovy.transform.Canonical
@Canonical(excludes=['lastName'])
class Person {
String firstName
String lastName
}
def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
assert p1.toString() == 'Person(Jack)' // Effect of @ToString(excludes=['lastName'])
def p2 = new Person('Jack') // Effect of @TupleConstructor(excludes=['lastName'])
assert p2.toString() == 'Person(Jack)'
assert p1==p2 // Effect of @EqualsAndHashCode(excludes=['lastName'])
assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode(excludes=['lastName'])
@Canonical
中任何适用的注解属性都会传递给显式注解,但显式注解中已存在的属性优先。
@groovy.transform.InheritConstructors
@InheritConstructor
AST 转换旨在为您生成与超构造函数匹配的构造函数。这在覆盖异常类时特别有用。
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
AST 转换支持以下配置选项:
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
constructorAnnotations |
False |
复制时是否保留构造函数中的注解 |
|
parameterAnnotations |
False |
复制构造函数时是否保留构造函数参数中的注解 |
|
@groovy.lang.Category
@Category
AST 转换简化了 Groovy 类别的创建。历史上,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()
}
请注意,混合类可以使用 this
引用。还值得注意的是,在类别类中使用实例字段本质上是不安全的:类别是无状态的(像特性一样)。
@groovy.transform.IndexedProperty
@IndexedProperty
注解旨在为列表/数组类型的属性生成索引 getter/setter。如果您想从 Java 使用 Groovy 类,这特别有用。虽然 Groovy 支持 GPath 访问属性,但 Java 不支持。@IndexedProperty
注解将生成以下形式的索引属性:
class SomeBean {
@IndexedProperty String[] someArray = new String[2]
@IndexedProperty List someList = []
}
def bean = new SomeBean()
bean.setSomeArray(0, 'value')
bean.setSomeList(0, 123)
assert bean.someArray[0] == 'value'
assert bean.someList == [123]
@groovy.lang.Lazy
@Lazy
AST 转换实现了字段的惰性初始化。例如,以下代码:
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 } }
如果字段被声明为 volatile,则初始化将使用双重检查锁定模式同步。
使用 soft=true
参数,辅助字段将改用 SoftReference
,提供了一种简单的缓存实现方式。在这种情况下,如果垃圾回收器决定回收引用,则下次访问字段时将发生初始化。
@groovy.lang.Newify
@Newify
AST 转换用于引入替代语法来构造对象。
-
使用
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'))) }
通过将 auto
标志设置为 false
,可以禁用 Ruby
版本。
@groovy.transform.Sortable
@Sortable
AST 转换用于帮助编写可比较且易于排序的类,通常按多个属性排序。它易于使用,如下例所示,我们注释了 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
类可以在任何需要 Comparable
的地方使用,而生成的比较器可以在任何需要 Comparator
的地方使用,如下例所示:
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
}
它将有两个比较器方法 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']
@Sortable
AST 转换的行为可以通过以下附加参数进一步更改:
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
allProperties |
True |
是否使用 JavaBean 属性(在原生属性之后排序) |
|
allNames |
False |
是否使用“内部”名称的属性 |
|
includeSuperProperties |
False |
是否也使用超属性(首先排序) |
|
@groovy.transform.builder.Builder
@Builder
AST 转换用于帮助编写可以使用*流式* API 调用创建的类。该转换支持多种构建策略以涵盖各种情况,并且有许多配置选项可以自定义构建过程。如果您是 AST 黑客,您还可以定义自己的策略类。下表列出了 Groovy 中捆绑的可用策略以及每种策略支持的配置选项。
策略 |
描述 |
builderClassName |
builderMethodName |
buildMethodName |
prefix |
includes/excludes |
includeSuperProperties |
allNames |
|
链式setter |
不适用 |
不适用 |
不适用 |
是,默认“set” |
是 |
不适用 |
是,默认 |
|
显式构建器类,正在构建的类不受影响 |
不适用 |
不适用 |
是,默认“build” |
是,默认“” |
是 |
是,默认 |
是,默认 |
|
创建一个嵌套辅助类 |
是,默认 <TypeName>Builder |
是,默认“builder” |
是,默认“build” |
是,默认“” |
是 |
是,默认 |
是,默认 |
|
创建一个提供类型安全流畅创建的嵌套辅助类 |
是,默认 <TypeName>Initializer |
是,默认“createInitializer” |
是,默认“create”,但通常仅在内部使用 |
是,默认“” |
是 |
是,默认 |
是,默认 |
要使用 SimpleStrategy
,请使用 @Builder
注解您的 Groovy 类,并指定策略,如本例所示:
import groovy.transform.builder.*
@Builder(builderStrategy=SimpleStrategy)
class Person {
String first
String last
Integer born
}
然后,只需像这样以链式方式调用 setter:
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
}
调用链式 setter 将如下所示:
def p = new Person().first('Johnny').last('Depp').born(1963)
assert "$p.first $p.last" == 'Johnny Depp'
您可以将 SimpleStrategy
与 @TupleConstructor
结合使用。如果您的 @Builder
注解没有显式的 includes
或 excludes
注解属性,但您的 @TupleConstructor
注解有,那么 @TupleConstructor
中的属性将被 @Builder
重用。这同样适用于任何组合 @TupleConstructor
的注解别名,例如 @Canonical
。
如果您希望在构造过程中调用某个 setter,可以使用 useSetters
注解属性。详情请参阅 JavaDoc。
此策略不支持注解属性 builderClassName
、buildMethodName
、builderMethodName
、forClass
和 includeSuperProperties
。
Groovy 已经内置了构建机制。如果内置机制满足您的需求,请不要急于使用 @Builder 。以下是一些示例: |
def p2 = new Person(first: 'Keira', last: 'Knightley', born: 1985)
def p3 = new Person().with {
first = 'Geoffrey'
last = 'Rush'
born = 1951
}
要使用 ExternalStrategy
,请使用 @Builder
注解创建和注解 Groovy 构建器类,使用 forClass
指定构建器适用的类,并指示使用 ExternalStrategy
。假设您有以下想要为其构建器的类:
class Person {
String first
String last
int born
}
您显式创建并使用构建器类,如下所示:
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'
请注意,您提供的(通常为空的)构建器类将填充适当的 setter 和构建方法。生成的构建方法将类似于:
public Person build() {
Person _thePerson = new Person()
_thePerson.first = first
_thePerson.last = last
_thePerson.born = born
return _thePerson
}
您正在为其创建构建器的类可以是遵循正常 JavaBean 约定的任何 Java 或 Groovy 类,例如,无参数构造函数和属性的 setter。这是一个使用 Java 类的示例:
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
注解属性进行自定义。这是一个说明各种自定义的示例:
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
的 builderMethodName
和 builderClassName
注解属性。
您可以将 ExternalStrategy
与 @TupleConstructor
结合使用。如果您的 @Builder
注解没有显式的 includes
或 excludes
注解属性,但您正在为其创建构建器的类的 @TupleConstructor
注解有,那么 @TupleConstructor
中的属性将被 @Builder
重用。这同样适用于任何组合 @TupleConstructor
的注解别名,例如 @Canonical
。
要使用 DefaultStrategy
,请使用 @Builder
注解您的 Groovy 类,如本例所示:
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
注解(在类、方法或构造函数位置),则由您来确保生成的辅助类和工厂方法具有唯一的名称(即不能有多个使用默认名称值)。有关方法和构造函数使用示例,但使用 DefaultStrategy
策略,请参阅该策略的文档。
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
,请使用 @Builder
注解您的 Groovy 类,并指定策略,如本例所示:
import groovy.transform.builder.*
import groovy.transform.*
@ToString
@Builder(builderStrategy=InitializerStrategy)
class Person {
String firstName
String lastName
int age
}
您的类将被锁定为只有一个公共构造函数,该构造函数接受一个“完全设置”的初始化器。它还将有一个工厂方法来创建初始化器。它们的使用方式如下:
@CompileStatic
def firstLastAge() {
assert new Person(Person.createInitializer().firstName("John").lastName("Smith").age(21)).toString() == 'Person(John, Smith, 21)'
}
firstLastAge()
任何涉及设置所有属性(尽管顺序不重要)的初始化器使用尝试都将导致编译错误。如果您不需要这种严格性级别,则无需使用 @CompileStatic
。
您可以将 InitializerStrategy
与 @Canonical
和 @Immutable
结合使用。如果您的 @Builder
注解没有显式的 includes
或 excludes
注解属性,但您的 @Canonical
注解有,则 @Canonical
中的属性将重新用于 @Builder
。这里有一个使用 @Builder
与 @Immutable
的示例:
import groovy.transform.builder.*
import groovy.transform.*
import static groovy.transform.options.Visibility.PRIVATE
@Builder(builderStrategy=InitializerStrategy)
@Immutable
@VisibilityOptions(PRIVATE)
class Person {
String first
String last
int born
}
def publicCons = Person.constructors
assert publicCons.size() == 1
@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()
如果您希望在构造过程中调用某个 setter,可以使用 useSetters
注解属性。详情请参阅 JavaDoc。
此策略还支持注释静态方法和构造函数。在这种情况下,静态方法或构造函数参数将成为用于构建目的的属性,在静态方法的情况下,方法的返回类型将成为正在构建的目标类。如果一个类中使用了多个 @Builder
注解(在类、方法或构造函数位置),则由您来确保生成的辅助类和工厂方法具有唯一的名称(即不能有多个使用默认名称值)。有关方法和构造函数使用示例,但使用 DefaultStrategy
策略,请参阅该策略的文档。
此策略不支持 forClass
注解属性。
@groovy.transform.AutoImplement
@AutoImplement
AST 转换提供了对超类或接口中找到的任何抽象方法的虚拟实现。虚拟实现对于找到的所有抽象方法都相同,可以是:
-
基本上为空(对于 void 方法和有返回类型的方法,返回该类型的默认值)
-
抛出指定异常的语句(可选消息)
-
一些用户提供的代码
第一个示例说明了默认情况。我们的类使用 @AutoImplement
注解,具有一个超类和一个接口,如下所示:
import groovy.transform.AutoImplement
@AutoImplement
class MyNames extends AbstractList<String> implements Closeable { }
提供了 Closeable
接口中的一个 void close()
方法并将其留空。还为超类中的三个抽象方法提供了实现。get
、addAll
和 size
方法的返回类型分别为 String
、boolean
和 int
,默认值分别为 null
、false
和 0
。我们可以使用我们的类(并检查其中一个方法的预期返回类型),使用以下代码:
assert new MyNames().size() == 0
同样值得检查等效的生成代码:
class MyNames implements Closeable extends AbstractList<String> {
String get(int param0) {
return null
}
boolean addAll(Collection<? extends String> param0) {
return false
}
void close() throws Exception {
}
int size() {
return 0
}
}
第二个示例说明了最简单的异常情况。我们的类用 @AutoImplement
注解,有一个超类,并且有一个注解属性指示如果调用任何“虚拟”方法,则应抛出 IOException
。类定义如下:
@AutoImplement(exception=IOException)
class MyWriter extends Writer { }
我们可以使用该类(并检查其中一个方法是否抛出预期异常),使用以下代码:
import static groovy.test.GroovyAssert.shouldFail
shouldFail(IOException) {
new MyWriter().flush()
}
同样值得检查等效的生成代码,其中提供了三个 void 方法,所有这些方法都抛出提供的异常:
class MyWriter extends Writer {
void flush() throws IOException {
throw new IOException()
}
void write(char[] param0, int param1, int param2) throws IOException {
throw new IOException()
}
void close() throws Exception {
throw new IOException()
}
}
第三个例子说明了带有消息的异常情况。我们的类被 @AutoImplement
注解,实现了一个接口,并且具有注解属性,指示对于任何提供的方法,都应该抛出一个带有 Not supported by MyIterator
作为消息的 UnsupportedOperationException
。类定义如下:
@AutoImplement(exception=UnsupportedOperationException, message='Not supported by MyIterator')
class MyIterator implements Iterator<String> { }
我们可以使用该类(并检查是否抛出了预期的异常并且具有正确的消息),使用以下代码:
def ex = shouldFail(UnsupportedOperationException) {
new MyIterator().hasNext()
}
assert ex.message == 'Not supported by MyIterator'
同样值得检查等效的生成代码,其中提供了三个 void 方法,所有这些方法都抛出提供的异常:
class MyIterator implements Iterator<String> {
boolean hasNext() {
throw new UnsupportedOperationException('Not supported by MyIterator')
}
String next() {
throw new UnsupportedOperationException('Not supported by MyIterator')
}
}
同样值得检查等效的生成代码,其中提供了 next
方法:
@AutoImplement(code = { throw new UnsupportedOperationException('Should never be called but was called on ' + new Date()) })
class EmptyIterator implements Iterator<String> {
boolean hasNext() { false }
}
第四个示例说明了用户提供的代码的情况。我们的类被 @AutoImplement
注解,实现了一个接口,具有显式覆盖的 hasNext
方法,并且具有包含为任何提供的方法提供的代码的注解属性。类定义如下:
def ex = shouldFail(UnsupportedOperationException) {
new EmptyIterator().next()
}
assert ex.message.startsWith('Should never be called but was called on ')
我们可以使用该类(并检查是否抛出了预期的异常并且具有预期形式的消息),使用以下代码:
class EmptyIterator implements java.util.Iterator<String> {
boolean hasNext() {
false
}
String next() {
throw new UnsupportedOperationException('Should never be called but was called on ' + new Date())
}
}
@groovy.transform.NullCheck
@NullCheck
AST 转换在构造函数和方法中添加空检查守卫语句,当提供空参数时,这些方法会导致早期失败。它可以被视为一种防御性编程形式。该注解可以添加到单个方法或构造函数,也可以添加到类中,在这种情况下它将应用于所有方法/构造函数。
@NullCheck
String longerOf(String first, String second) {
first.size() >= second.size() ? first : second
}
assert longerOf('cat', 'canary') == 'canary'
def ex = shouldFail(IllegalArgumentException) {
longerOf('cat', null)
}
assert ex.message == 'second cannot be null'
类设计注解
此类注解旨在通过使用声明式风格简化众所周知的设计模式(委托、单例等)的实现。
@groovy.transform.BaseScript
@BaseScript
在脚本中使用,表示脚本应该扩展自自定义脚本基类而不是 groovy.lang.Script
。有关更多详细信息,请参阅领域特定语言的文档。
@groovy.lang.Delegate
@Delegate
AST 转换旨在实现委托设计模式。在下面的类中:
class Event {
@Delegate Date when
String title
}
when
属性使用 @Delegate
注解,这意味着 Event
类将把对 Date
方法的调用委托给 when
属性。在这种情况下,生成的代码如下所示:
class Event {
Date when
String title
boolean before(Date other) {
when.before(other)
}
// ...
}
然后您可以直接在 Event
类上调用 before
方法,例如:
def ev = new Event(title:'Groovy keynote', when: Date.parse('yyyy/MM/dd', '2013/09/10'))
def now = new Date()
assert ev.before(now)
除了注释属性(或字段)之外,您还可以注释方法。在这种情况下,该方法可以被认为是委托的 getter 或工厂方法。例如,这里有一个类(相当不寻常)有一个委托池,它们以循环方式访问:
class Test {
private int robinCount = 0
private List<List> items = [[0], [1], [2]]
@Delegate
List getRoundRobinList() {
items[robinCount++ % items.size()]
}
void checkItems(List<List> testValue) {
assert items == testValue
}
}
以下是该类的用法示例:
def t = new Test()
t << 'fee'
t << 'fi'
t << 'fo'
t << 'fum'
t.checkItems([[0, 'fee', 'fum'], [1, 'fi'], [2, 'fo']])
以这种循环方式使用标准列表会违反列表的许多预期属性,因此不要指望上述类能做任何有用的事情,除了这个微不足道的示例。
@Delegate
AST 转换的行为可以通过以下参数更改:
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
interfaces |
True |
字段实现的接口是否也应由类实现 |
|
deprecated |
false |
如果为 true,也委托使用 @Deprecated 注解的方法 |
|
methodAnnotations |
False |
是否将注解从委托方法携带到您的委托方法。 |
|
parameterAnnotations |
False |
是否将注解从委托方法的参数携带到您的委托方法。 |
|
excludes |
空数组 |
要从委托中排除的方法列表。有关更精细的控制,另请参阅 |
|
includes |
未定义标记数组(默认表示所有方法) |
要包含在委托中的方法列表。有关更精细的控制,另请参阅 |
|
excludeTypes |
空数组 |
包含要从委托中排除的方法签名的接口列表 |
|
includeTypes |
未定义标记数组(默认表示无列表) |
包含要包含在委托中的方法签名的接口列表 |
|
allNames |
False |
是否也将委托模式应用于具有内部名称的方法 |
|
@groovy.transform.Immutable
@Immutable
元注解结合了以下注解:
@Immutable
元注解简化了不可变类的创建。不可变类很有用,因为它们通常更容易推理,并且本质上是线程安全的。有关如何在 Java 中实现不可变类的所有详细信息,请参阅Effective Java, Minimize Mutability。@Immutable
元注解自动为您完成 Effective Java 中描述的大部分事情。要使用该元注解,您只需像以下示例中那样注解类:
import groovy.transform.Immutable
@Immutable
class Point {
int x
int y
}
不可变类的要求之一是无法修改类中的任何状态信息。实现这一点的一个要求是为每个属性使用不可变类,或者在构造函数和属性 getter 中对任何可变属性执行特殊编码,例如防御性复制和防御性复制出。在 @ImmutableBase
、@MapConstructor
和 @TupleConstructor
之间,属性要么被识别为不可变,要么自动处理许多已知情况的特殊编码。为您提供了多种机制来扩展允许的属性类型。有关详细信息,请参阅 @ImmutableOptions
和 @KnownImmutable
。
将 @Immutable
应用于类的结果与应用 @Canonical 元注解的结果非常相似,但生成的类将包含额外的逻辑来处理不可变性。您将通过尝试修改属性来观察这一点,这将导致抛出 ReadOnlyPropertyException
,因为属性的后备字段将自动设置为 final。
@Immutable
元注解支持其聚合的注解中找到的配置选项。有关更多详细信息,请参阅这些注解。
@groovy.transform.ImmutableBase
使用 @ImmutableBase
生成的不可变类会自动声明为 final。此外,还会检查每个属性的类型,并对类进行各种检查,例如,当前不允许公共实例字段。如果需要,它还会生成一个 copyWith
构造函数。
支持以下注解属性:
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
copyWith |
false |
一个布尔值,指示是否生成 |
|
@groovy.transform.PropertyOptions
此注解允许您指定一个自定义属性处理器,供转换在类构建期间使用。它被主 Groovy 编译器忽略,但被其他转换(如 @TupleConstructor
、@MapConstructor
和 @ImmutableBase
)引用。它经常被 @Immutable
元注解在幕后使用。
@groovy.transform.VisibilityOptions
此注解允许您为另一个转换生成的构造指定自定义可见性。它被主 Groovy 编译器忽略,但被其他转换(如 @TupleConstructor
、@MapConstructor
和 @NamedVariant
)引用。
@groovy.transform.ImmutableOptions
Groovy 的不可变性支持依赖于已知不可变类的预定义列表(例如 java.net.URI
或 java.lang.String
),如果您使用的类型不在该列表中,它将失败。由于 @ImmutableOptions
注解的以下注解属性,您被允许添加到已知不可变类型的列表中:
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
knownImmutableClasses |
空列表 |
被视为不可变的类列表。 |
|
knownImmutables |
空列表 |
被视为不可变的属性名称列表。 |
|
如果您将某个类型视为不可变,但它不是自动处理的类型之一,那么您有责任正确编写该类以确保不可变性。
@groovy.transform.KnownImmutable
@KnownImmutable
注解实际上并不会触发任何 AST 转换。它只是一个标记注解。您可以用该注解(包括 Java 类)注解您的类,它们将被识别为不可变类中成员的 acceptable 类型。这使您不必显式使用 @ImmutableOptions
中的 knownImmutables
或 knownImmutableClasses
注解属性。
@groovy.transform.Memoized
@Memoized
AST 转换通过简单地使用 @Memoized
注解方法,简化了缓存的实现,从而可以缓存方法调用的结果。假设有以下方法:
long longComputation(int seed) {
// slow computation
Thread.sleep(100*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(100*seed)
System.nanoTime()
}
def x = longComputation(1) // returns after 100 milliseconds
def y = longComputation(1) // returns immediately
def z = longComputation(2) // returns after 200 milliseconds
assert x==y
assert x!=z
缓存的大小可以通过两个可选参数进行配置:
-
protectedCacheSize:保证不会在垃圾回收后清除的结果数量
-
maxCacheSize:内存中可保留的最大结果数量
默认情况下,缓存大小无限,并且没有缓存结果受到垃圾回收的保护。设置 protectedCacheSize>0 将创建一个无限缓存,其中一些结果受到保护。设置 maxCacheSize>0 将创建一个有限缓存,但不受任何垃圾保护。同时设置两者将创建一个有限的受保护缓存。
@groovy.transform.TailRecursive
@TailRecursive
注解可用于将方法末尾的递归调用自动转换为等效的迭代版本。这可以避免因过多递归调用而导致的堆栈溢出。以下是计算阶乘时的使用示例:
import groovy.transform.CompileStatic
import groovy.transform.TailRecursive
@CompileStatic
class Factorial {
@TailRecursive
static BigInteger factorial( BigInteger i, BigInteger product = 1) {
if( i == 1) {
return product
}
return factorial(i-1, product*i)
}
}
assert Factorial.factorial(1) == 1
assert Factorial.factorial(3) == 6
assert Factorial.factorial(5) == 120
assert Factorial.factorial(50000).toString().size() == 213237 // Big number and no Stack Overflow
目前,此注解仅适用于自递归方法调用,即对完全相同方法的单个递归调用。如果遇到涉及简单相互递归的场景,请考虑使用闭包和 trampoline()
。另请注意,目前仅处理非 void 方法(void 调用将导致编译错误)。
目前,某些形式的方法重载可能会欺骗编译器,并且一些非尾递归调用被错误地视为尾递归。 |
@groovy.lang.Singleton
@Singleton
注解可用于在类上实现单例设计模式。单例实例默认通过类初始化急切定义,或者懒惰定义,在这种情况下,字段使用双重检查锁定进行初始化。
@Singleton
class GreetingService {
String greeting(String name) { "Hello, $name!" }
}
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'
默认情况下,单例在类初始化时急切创建,并通过 instance
属性可用。可以使用 property
参数更改单例的名称:
@Singleton(property='theOne')
class GreetingService {
String greeting(String name) { "Hello, $name!" }
}
assert GreetingService.theOne.greeting('Bob') == 'Hello, Bob!'
也可以使用 lazy
参数进行延迟初始化:
class Collaborator {
public static boolean init = false
}
@Singleton(lazy=true,strict=false)
class GreetingService {
static void init() {}
GreetingService() {
Collaborator.init = true
}
String greeting(String name) { "Hello, $name!" }
}
GreetingService.init() // make sure class is initialized
assert Collaborator.init == false
GreetingService.instance
assert Collaborator.init == true
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'
在此示例中,我们还将 strict
参数设置为 false,这允许我们定义自己的构造函数。
@groovy.lang.Mixin
已弃用。请考虑改用 traits。
日志改进
Groovy 提供了一系列 AST 转换,有助于集成最广泛使用的日志框架。每个常见的框架都有一个转换和关联的注解。这些转换提供了使用日志框架的流线型声明式方法。在每种情况下,转换都将:
-
向带注解的类添加一个与日志器对应的静态 final
log
字段 -
根据底层框架,将所有对
log.level()
的调用包装到适当的log.isLevelEnabled
守卫中
这些转换支持两个参数:
-
value
(默认log
)对应于日志器字段的名称 -
category
(默认为类名)是日志器类别的名称
值得注意的是,用这些注解之一注释一个类并不会阻止您使用正常的冗长方法来使用日志框架。
@groovy.util.logging.Log
可用的第一个日志 AST 转换是 @Log
注解,它依赖于 JDK 日志框架。编写:
@groovy.util.logging.Log
class Greeter {
void greet() {
log.info 'Called greeter'
println 'Hello, world!'
}
}
等同于编写:
import java.util.logging.Level
import java.util.logging.Logger
class Greeter {
private static final Logger log = Logger.getLogger(Greeter.name)
void greet() {
if (log.isLoggable(Level.INFO)) {
log.info 'Called greeter'
}
println 'Hello, world!'
}
}
@groovy.util.logging.Commons
Groovy 使用 @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!'
}
}
您仍然需要将适当的 commons-logging jar 添加到您的类路径中。
@groovy.util.logging.Log4j
Groovy 使用 @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!'
}
}
您仍然需要将适当的 log4j jar 添加到您的类路径中。此注解也可以与兼容的 reload4j log4j 替代品一起使用,只需使用该项目中的 jar 而不是 log4j jar。
@groovy.util.logging.Log4j2
Groovy 使用 @Log4j2
注解支持 Apache 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!'
}
}
您仍然需要将适当的 log4j2 jar 添加到您的类路径中。
@groovy.util.logging.Slf4j
Groovy 使用 @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!'
}
}
您仍然需要将适当的 slf4j jar(s) 添加到您的类路径中。
@groovy.util.logging.PlatformLog
Groovy 使用 @PlatformLog
注解支持 Java 平台日志 API 和服务框架。编写:
@groovy.util.logging.PlatformLog
class Greeter {
void greet() {
log.info 'Called greeter'
println 'Hello, world!'
}
}
等同于编写:
import java.lang.System.Logger
import java.lang.System.LoggerFinder
import static java.lang.System.Logger.Level.INFO
class Greeter {
private static final transient Logger log =
LoggerFinder.loggerFinder.getLogger(Greeter.class.name, Greeter.class.module)
void greet() {
log.log INFO, 'Called greeter'
println 'Hello, world!'
}
}
您需要使用 JDK 9+ 才能使用此功能。
声明式并发
Groovy 语言提供了一组注解,旨在以声明式方式简化常见的并发模式。
@groovy.transform.Synchronized
@Synchronized
AST 转换的工作方式与 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
(或静态方法的 $LOCK
)的字段,但您可以通过指定 value 属性使其使用任何您想要的字段,如下例所示:
import groovy.transform.Synchronized
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class Counter {
int cpt
private final Object myLock = new Object()
@Synchronized('myLock')
int incrementAndGet() {
cpt++
}
int get() {
cpt
}
}
@groovy.transform.WithReadLock
和 @groovy.transform.WithWriteLock
@WithReadLock
AST 转换与 @WithWriteLock
转换结合使用,以提供使用 JDK 提供的 ReentrantReadWriteLock
工具的读/写同步。该注解可以添加到方法或静态方法。它将透明地创建一个 $reentrantLock
final 字段(或静态方法的 $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
和 @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 的 Javadoc
-
请参阅 groovy.transform.WithWriteLock 的 Javadoc
更简单的克隆和外部化
Groovy 提供了两个注解,旨在促进 Cloneable
和 Externalizable
接口的实现,分别命名为 @AutoClone
和 @AutoExternalize
。
@groovy.transform.AutoClone
@AutoClone
注解旨在通过各种策略实现 @java.lang.Cloneable
接口,这要归功于 style
参数:
-
默认的
AutoCloneStyle.CLONE
策略首先调用super.clone()
,然后对每个可克隆属性调用clone()
-
AutoCloneStyle.SIMPLE
策略使用常规构造函数调用,并将属性从源复制到克隆体 -
AutoCloneStyle.COPY_CONSTRUCTOR
策略创建并使用复制构造函数 -
AutoCloneStyle.SERIALIZATION
策略使用序列化(或外部化)来克隆对象
这些策略各有利弊,在 groovy.transform.AutoClone 和 groovy.transform.AutoCloneStyle 的 Javadoc 中进行了讨论。
例如,以下示例:
import groovy.transform.AutoClone
@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
}
}
请注意,字符串属性没有被显式处理,因为字符串是不可变的,并且 Object
的 clone()
方法将复制字符串引用。这同样适用于原始字段和 java.lang.Number
的大多数具体子类。
除了克隆样式,@AutoClone
还支持多个选项
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
excludes |
空列表 |
需要从克隆中排除的属性或字段名列表。也可以使用由逗号分隔的字段/属性名组成的字符串。详见groovy.transform.AutoClone#excludes |
|
includeFields |
false |
默认情况下,只克隆属性。将此标志设置为true也将克隆字段。 |
|
@groovy.transform.AutoExternalize
@AutoExternalize
AST转换将帮助创建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
注解支持两个参数,可以稍微自定义其行为
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
excludes |
空列表 |
需要从外部化中排除的属性或字段名列表。也可以使用由逗号分隔的字段/属性名组成的字符串。详见groovy.transform.AutoExternalize#excludes |
|
includeFields |
false |
默认情况下,只外部化属性。将此标志设置为true也将克隆字段。 |
|
更安全的脚本编写
Groovy语言使得在运行时执行用户脚本变得容易(例如使用groovy.lang.GroovyShell),但你如何确保脚本不会耗尽所有CPU(无限循环)或并发脚本不会慢慢消耗线程池中所有可用线程?Groovy提供了几个注解,旨在实现更安全的脚本编写,生成允许你自动中断执行的代码。
@groovy.transform.ThreadInterrupt
JVM世界中一个复杂的情况是线程无法停止。Thread#stop
方法存在但已被废弃(且不可靠),所以你唯一的选择是Thread#interrupt
。调用后者会设置线程上的interrupt
标志,但它不会停止线程的执行。这带来了问题,因为检查中断标志并正确退出是线程中执行的代码的责任。当你作为开发人员知道你正在执行的代码旨在在独立线程中运行时,这很有意义,但通常情况下,你并不知道。对于用户脚本来说更糟糕,他们可能甚至不知道线程是什么(想想DSL)。
@ThreadInterrupt
通过在代码的关键位置添加线程中断检查来简化此过程
-
循环(for,while)
-
方法的第一个指令
-
闭包体的第一个指令
让我们想象一下以下用户脚本
while (true) {
i++
}
这是一个明显的无限循环。如果此代码在自己的线程中执行,中断将无济于事:如果你在线程上join
,则调用代码能够继续,但线程仍将存活,在后台运行而无法停止,慢慢导致线程耗尽。
解决这个问题的一种可能性是这样设置你的shell
def config = new CompilerConfiguration()
config.addCompilationCustomizers(
new ASTTransformationCustomizer(ThreadInterrupt)
)
def binding = new Binding(i:0)
def shell = new GroovyShell(binding,config)
然后,shell被配置为自动对所有脚本应用@ThreadInterrupt
AST转换。这允许你这样执行用户脚本
def t = Thread.start {
shell.evaluate(userCode)
}
t.join(1000) // give at most 1000ms 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++
}
循环内部引入的检查保证,如果当前线程设置了interrupt
标志,将抛出异常,中断线程的执行。
@ThreadInterrupt
支持多个选项,你可以进一步自定义转换的行为
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
抛出 |
|
指定线程中断时抛出的异常类型。 |
|
checkOnMethodStart |
true |
是否在每个方法体开头插入中断检查。详见groovy.transform.ThreadInterrupt。 |
|
applyToAllClasses |
true |
是否将转换应用于同一源单元(在同一源文件中)的所有类。详见groovy.transform.ThreadInterrupt。 |
|
applyToAllMembers |
true |
是否将转换应用于类的所有成员。详见groovy.transform.ThreadInterrupt。 |
|
@groovy.transform.TimedInterrupt
@TimedInterrupt
AST转换试图解决与@groovy.transform.ThreadInterrupt
略有不同的问题:它不会检查线程的interrupt
标志,而是如果线程运行时间过长,则会自动抛出异常。
此注解不会生成监视线程。相反,它以类似于@ThreadInterrupt 的方式工作,通过在代码的适当位置放置检查。这意味着如果线程被I/O阻塞,它将不会被中断。 |
想象以下用户代码
def fib(int n) { n<2?n:fib(n-1)+fib(n-2) }
result = fib(600)
这里著名的斐波那契数列计算的实现远未优化。如果用很高的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
注解类,如下所示
@TimedInterrupt(value=1, unit=TimeUnit.SECONDS)
class MyClass {
def fib(int n) {
n<2?n:fib(n-1)+fib(n-2)
}
}
@TimedInterrupt
支持多个选项,可以让你进一步自定义转换的行为
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
值 |
Long.MAX_VALUE |
与 |
|
单位 |
TimeUnit.SECONDS |
与 |
|
抛出 |
|
指定达到超时时抛出的异常类型。 |
|
checkOnMethodStart |
true |
是否在每个方法体开头插入中断检查。详见groovy.transform.TimedInterrupt。 |
|
applyToAllClasses |
true |
是否将转换应用于同一源单元(在同一源文件中)的所有类。详见groovy.transform.TimedInterrupt。 |
|
applyToAllMembers |
true |
是否将转换应用于类的所有成员。详见groovy.transform.TimedInterrupt。 |
|
@TimedInterrupt 目前与静态方法不兼容! |
@groovy.transform.ConditionalInterrupt
最后一个用于更安全脚本的注解是当你想要使用自定义策略中断脚本时的基本注解。特别是,如果你想使用资源管理(限制对API的调用次数等),这是首选注解。在以下示例中,用户代码正在使用无限循环,但@ConditionalInterrupt
将允许我们检查配额管理器并自动中断脚本
@ConditionalInterrupt({Quotas.disallow('user')})
class UserCode {
void doSomething() {
int i=0
while (true) {
println "Consuming resources ${++i}"
}
}
}
这里的配额检查非常基础,但可以是任何代码
class Quotas {
static def quotas = [:].withDefault { 10 }
static boolean disallow(String userName) {
println "Checking quota for $userName"
(quotas[userName]--)<0
}
}
我们可以使用以下测试代码确保@ConditionalInterrupt
正常工作
assert Quotas.quotas['user'] == 10
def t = Thread.start {
new UserCode().doSomething()
}
t.join(5000)
assert !t.alive
assert Quotas.quotas['user'] < 0
当然,在实践中,@ConditionalInterrupt
不太可能手动添加到用户代码中。它可以以与ThreadInterrupt部分所示示例类似的方式注入,使用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
支持多个选项,可以让你进一步自定义转换的行为
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
值 |
将调用的闭包,用于检查是否允许执行。如果闭包返回false,则允许执行。如果返回true,则将抛出异常。 |
|
|
抛出 |
|
指定执行应中止时抛出的异常类型。 |
|
checkOnMethodStart |
true |
是否在每个方法体开头插入中断检查。详见groovy.transform.ConditionalInterrupt。 |
|
applyToAllClasses |
true |
是否将转换应用于同一源单元(在同一源文件中)的所有类。详见groovy.transform.ConditionalInterrupt。 |
|
applyToAllMembers |
true |
是否将转换应用于类的所有成员。详见groovy.transform.ConditionalInterrupt。 |
|
编译器指令
此类别别的AST转换将直接影响代码语义的注解分组,而不是专注于代码生成。在这方面,它们可以被视为编译器指令,在编译时或运行时改变程序的行为。
@groovy.transform.Field
@Field
注解仅在脚本上下文中有意义,旨在解决脚本中常见的范围错误。例如,以下示例将在运行时失败
def x
String line() {
"="*x
}
x=3
assert "===" == line()
x=5
assert "=====" == line()
抛出的错误可能难以解释:groovy.lang.MissingPropertyException: No such property: x。原因是脚本被编译成类,脚本体本身被编译成一个单独的run()方法。脚本中定义的方法是独立的,所以上面的代码等价于这样
class MyScript extends Script {
String line() {
"="*x
}
public def run() {
def x
x=3
assert "===" == line()
x=5
assert "=====" == line()
}
}
因此def x
实际上被解释为局部变量,在line
方法的范围之外。@Field
AST转换旨在通过将变量的范围更改为封闭脚本的字段来修复此问题
@Field def x
String line() {
"="*x
}
x=3
assert "===" == line()
x=5
assert "=====" == line()
结果的等效代码现在是
class MyScript extends Script {
def x
String line() {
"="*x
}
public def run() {
x=3
assert "===" == line()
x=5
assert "=====" == line()
}
}
@groovy.transform.PackageScope
默认情况下,Groovy可见性规则意味着如果你创建字段时没有指定修饰符,则该字段被解释为属性
class Person {
String name // this is a property
}
如果你想创建一个包私有字段而不是属性(私有字段+getter/setter),那么用@PackageScope
注解你的字段
class Person {
@PackageScope String name // not a property anymore
}
@PackageScope
注解也可以用于类、方法和构造函数。此外,通过在类级别指定PackageScopeTarget
值列表作为注解属性,该类中没有显式修饰符且与提供的PackageScopeTarget
匹配的所有成员都将保持包保护。例如,要应用于类中的字段,请使用以下注解
import static groovy.transform.PackageScopeTarget.FIELDS
@PackageScope(FIELDS)
class Person {
String name // not a property, package protected
Date dob // not a property, package protected
private int age // explicit modifier, so won't be touched
}
@PackageScope
注解在正常Groovy约定中很少使用,但有时对于应在包内部可见的工厂方法、用于测试目的提供的方法或构造函数,或与需要此类可见性约定的第三方库集成时非常有用。
@groovy.transform.Final
@Final
本质上是final
修饰符的别名。其意图是,你几乎永远不会直接使用@Final
注解(只需使用final
)。但是,在创建应该将final修饰符应用于被注解节点的元注解时,你可以混合使用@Final
,例如:
@AnnotationCollector([Singleton,Final]) @interface MySingleton {}
@MySingleton
class GreetingService {
String greeting(String name) { "Hello, $name!" }
}
assert GreetingService.instance.greeting('Bob') == 'Hello, Bob!'
assert Modifier.isFinal(GreetingService.modifiers)
@groovy.transform.AutoFinal
@AutoFinal
注解指示编译器在被注解节点内的许多位置自动插入final修饰符。如果应用于方法(或构造函数),则该方法(或构造函数)的参数将被标记为final。如果应用于类定义,则该类中所有声明的方法和构造函数都将受到相同的处理。
通常认为,在方法或构造函数体中重新分配参数是不良实践。通过向所有参数声明添加final修饰符,可以完全避免这种做法。一些程序员认为,到处添加final会增加样板代码的数量,并使方法签名有些嘈杂。替代方法可能是使用代码审查过程或应用codenarc规则,以便在观察到这种做法时发出警告,但这些替代方法可能导致质量检查期间的延迟反馈,而不是在IDE中或编译期间。@AutoFinal
注解旨在最大限度地提高编译器/IDE反馈,同时保持简洁的代码和最小的样板噪音。
以下示例说明了在类级别应用注解
import groovy.transform.AutoFinal
@AutoFinal
class Person {
private String first, last
Person(String first, String last) {
this.first = first
this.last = last
}
String fullName(String separator) {
"$first$separator$last"
}
String greeting(String salutation) {
"$salutation, $first"
}
}
在此示例中,构造函数的两个参数以及fullname
和greeting
方法的单个参数都将是final。在构造函数或方法体中尝试修改这些参数将由编译器标记。
以下示例说明了在方法级别应用注解
class Calc {
@AutoFinal
int add(int a, int b) { a + b }
int mult(int a, int b) { a * b }
}
这里,add
方法将具有final参数,但mult
方法将保持不变。
@groovy.transform.AnnotationCollector
@AnnotationCollector
允许创建元注解,这在专门章节中描述。
@groovy.transform.TypeChecked
@TypeChecked
激活Groovy代码的编译时类型检查。详见类型检查章节。
@groovy.transform.CompileStatic
@CompileStatic
激活Groovy代码的静态编译。详见类型检查章节。
@groovy.transform.CompileDynamic
@CompileDynamic
禁用Groovy代码某些部分的静态编译。详见类型检查章节。
@groovy.transform.SelfType
@SelfType
不是AST转换,而是与特征一起使用的标记接口。更多详细信息请参阅特征文档。
Swing模式
@groovy.beans.Bindable
@Bindable
是一种AST转换,它将普通属性转换为绑定属性(根据JavaBeans规范)。@Bindable
注解可以放置在属性或类上。要将类的所有属性转换为绑定属性,可以像此示例一样注解类
import groovy.beans.Bindable
@Bindable
class Person {
String name
int age
}
这等同于这样写
import java.beans.PropertyChangeListener
import java.beans.PropertyChangeSupport
class Person {
final private PropertyChangeSupport this$propertyChangeSupport
String name
int age
public void addPropertyChangeListener(PropertyChangeListener listener) {
this$propertyChangeSupport.addPropertyChangeListener(listener)
}
public void addPropertyChangeListener(String name, PropertyChangeListener listener) {
this$propertyChangeSupport.addPropertyChangeListener(name, listener)
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
this$propertyChangeSupport.removePropertyChangeListener(listener)
}
public void removePropertyChangeListener(String name, PropertyChangeListener listener) {
this$propertyChangeSupport.removePropertyChangeListener(name, listener)
}
public void firePropertyChange(String name, Object oldValue, Object newValue) {
this$propertyChangeSupport.firePropertyChange(name, oldValue, newValue)
}
public PropertyChangeListener[] getPropertyChangeListeners() {
return this$propertyChangeSupport.getPropertyChangeListeners()
}
public PropertyChangeListener[] getPropertyChangeListeners(String name) {
return this$propertyChangeSupport.getPropertyChangeListeners(name)
}
}
@Bindable
因此从你的类中消除了大量样板,大大提高了可读性。如果注解放在单个属性上,则只有该属性被绑定
import groovy.beans.Bindable
class Person {
String name
@Bindable int age
}
@groovy.beans.ListenerList
@ListenerList
AST转换生成用于向类添加、移除和获取侦听器列表的代码,只需注解一个集合属性
import java.awt.event.ActionListener
import groovy.beans.ListenerList
class Component {
@ListenerList
List<ActionListener> listeners;
}
该转换将根据列表的泛型类型生成适当的add/remove方法。此外,它还将根据类上声明的公共方法创建fireXXX
方法
import java.awt.event.ActionEvent
import java.awt.event.ActionListener as ActionListener
import groovy.beans.ListenerList as ListenerList
public class Component {
@ListenerList
private List<ActionListener> listeners
public void addActionListener(ActionListener listener) {
if ( listener == null) {
return
}
if ( listeners == null) {
listeners = []
}
listeners.add(listener)
}
public void removeActionListener(ActionListener listener) {
if ( listener == null) {
return
}
if ( listeners == null) {
listeners = []
}
listeners.remove(listener)
}
public ActionListener[] getActionListeners() {
Object __result = []
if ( listeners != null) {
__result.addAll(listeners)
}
return (( __result ) as ActionListener[])
}
public void fireActionPerformed(ActionEvent param0) {
if ( listeners != null) {
ArrayList<ActionListener> __list = new ArrayList<ActionListener>(listeners)
for (def listener : __list ) {
listener.actionPerformed(param0)
}
}
}
}
@Bindable
支持多个选项,可以让你进一步自定义转换的行为
属性 | 默认值 | 描述 | 示例 |
---|---|---|---|
名称 |
泛型类型名称 |
默认情况下,将附加到add/remove/…方法的后缀是列表泛型类型的简单类名。 |
|
同步 |
false |
如果设置为true,生成的方法将同步 |
|
@groovy.beans.Vetoable
@Vetoable
注解的工作方式与@Bindable
类似,但根据JavaBeans规范生成受约束的属性,而不是绑定属性。该注解可以放置在类上,这意味着所有属性都将转换为受约束的属性,也可以放置在单个属性上。例如,用@Vetoable
注解此类的效果
import groovy.beans.Vetoable
import java.beans.PropertyVetoException
import java.beans.VetoableChangeListener
@Vetoable
class Person {
String name
int age
}
等同于这样写
public class Person {
private String name
private int age
final private java.beans.VetoableChangeSupport this$vetoableChangeSupport
public void addVetoableChangeListener(VetoableChangeListener listener) {
this$vetoableChangeSupport.addVetoableChangeListener(listener)
}
public void addVetoableChangeListener(String name, VetoableChangeListener listener) {
this$vetoableChangeSupport.addVetoableChangeListener(name, listener)
}
public void removeVetoableChangeListener(VetoableChangeListener listener) {
this$vetoableChangeSupport.removeVetoableChangeListener(listener)
}
public void removeVetoableChangeListener(String name, VetoableChangeListener listener) {
this$vetoableChangeSupport.removeVetoableChangeListener(name, listener)
}
public void fireVetoableChange(String name, Object oldValue, Object newValue) throws PropertyVetoException {
this$vetoableChangeSupport.fireVetoableChange(name, oldValue, newValue)
}
public VetoableChangeListener[] getVetoableChangeListeners() {
return this$vetoableChangeSupport.getVetoableChangeListeners()
}
public VetoableChangeListener[] getVetoableChangeListeners(String name) {
return this$vetoableChangeSupport.getVetoableChangeListeners(name)
}
public void setName(String value) throws PropertyVetoException {
this.fireVetoableChange('name', name, value)
name = value
}
public void setAge(int value) throws PropertyVetoException {
this.fireVetoableChange('age', age, value)
age = value
}
}
如果注解放在单个属性上,则只有该属性是可否决的
import groovy.beans.Vetoable
class Person {
String name
@Vetoable int age
}
测试协助
@groovy.test.NotYetImplemented
@NotYetImplemented
用于反转JUnit 3/4测试用例的结果。如果某个功能尚未实现但测试已存在,则特别有用。在这种情况下,预期测试会失败。用@NotYetImplemented
标记它将反转测试结果,例如此示例
import groovy.test.GroovyTestCase
import groovy.test.NotYetImplemented
class Maths {
static int fib(int n) {
// todo: implement later
}
}
class MathsTest extends GroovyTestCase {
@NotYetImplemented
void testFib() {
def dataTable = [
1:1,
2:1,
3:2,
4:3,
5:5,
6:8,
7:13
]
dataTable.each { i, r ->
assert Maths.fib(i) == r
}
}
}
使用这种技术的另一个优点是,你可以在知道如何修复bug之前编写bug的测试用例。如果将来某个时候,代码中的修改通过副作用修复了bug,你将收到通知,因为预期会失败的测试通过了。
@groovy.transform.ASTTest
@ASTTest
是一种特殊的AST转换,旨在帮助调试其他AST转换或Groovy编译器本身。它将允许开发人员在编译期间“探索”AST,并对AST执行断言,而不是对编译结果执行断言。这意味着此AST转换可以在生成字节码之前访问AST。@ASTTest
可以放置在任何可注解节点上,并需要两个参数
-
phase:设置
@ASTTest
将被触发的阶段。测试代码将在此阶段结束时在AST树上工作。 -
value:到达该阶段后,在被注解节点上执行的代码
编译阶段必须从org.codehaus.groovy.control.CompilePhase中选择一个。但是,由于无法用相同的注解两次注解同一个节点,因此你将无法在两个不同的编译阶段对同一个节点使用@ASTTest 。 |
value
是一个闭包表达式,它可以访问一个特殊变量node
(对应于被注解的节点)和一个辅助方法lookup
,后者将在此处讨论。例如,你可以像这样注解一个类节点
import groovy.transform.ASTTest
import org.codehaus.groovy.ast.ClassNode
@ASTTest(phase=CONVERSION, value={ (1)
assert node instanceof ClassNode (2)
assert node.name == 'Person' (3)
})
class Person {
}
1 | 我们正在检查转换阶段(CONVERSION)之后抽象语法树(Abstract Syntax Tree)的状态 |
2 | node 指的是被 @ASTTest 注解的 AST 节点 |
3 | 它可以在编译时用于执行断言 |
@ASTTest
的一个有趣特性是,如果断言失败,那么编译将失败。现在想象一下,我们想要在编译时检查AST转换的行为。我们在这里将使用@PackageScope
,我们想验证用@PackageScope
注解的属性是否成为包私有字段。为此,我们必须知道转换在哪个阶段运行,这可以在org.codehaus.groovy.transform.PackageScopeASTTransformation中找到:语义分析。然后可以像这样编写测试
import groovy.transform.ASTTest
import groovy.transform.PackageScope
@ASTTest(phase=SEMANTIC_ANALYSIS, value={
def nameNode = node.properties.find { it.name == 'name' }
def ageNode = node.properties.find { it.name == 'age' }
assert nameNode
assert ageNode == null // shouldn't be a property anymore
def ageField = node.getDeclaredField 'age'
assert ageField.modifiers == 0
})
class Person {
String name
@PackageScope int age
}
@ASTTest
注解只能放置在语法允许的位置。有时,你可能想测试不可注解的AST节点的内容。在这种情况下,@ASTTest
提供了一个方便的lookup
方法,它将在AST中搜索用特殊标记标记的节点
def list = lookup('anchor') (1)
Statement stmt = list[0] (2)
1 | 返回标签为“anchor”的AST节点列表 |
2 | 由于 lookup 总是返回一个列表,因此总是需要选择要处理的元素 |
例如,假设你想测试for循环变量的声明类型。那么你可以这样做
import groovy.transform.ASTTest
import groovy.transform.PackageScope
import org.codehaus.groovy.ast.ClassHelper
import org.codehaus.groovy.ast.expr.DeclarationExpression
import org.codehaus.groovy.ast.stmt.ForStatement
class Something {
@ASTTest(phase=SEMANTIC_ANALYSIS, value={
def forLoop = lookup('anchor')[0]
assert forLoop instanceof ForStatement
def decl = forLoop.collectionExpression.expressions[0]
assert decl instanceof DeclarationExpression
assert decl.variableExpression.name == 'i'
assert decl.variableExpression.originType == ClassHelper.int_TYPE
})
void someMethod() {
int x = 1;
int y = 10;
anchor: for (int i=0; i<x+y; i++) {
println "$i"
}
}
}
@ASTTest
还在测试闭包中暴露了这些变量
-
node
照常对应于被注解的节点 -
compilationUnit
允许访问当前的org.codehaus.groovy.control.CompilationUnit
-
compilePhase
返回当前编译阶段(org.codehaus.groovy.control.CompilePhase
)
如果你没有指定phase
属性,后者会很有趣。在这种情况下,闭包将在SEMANTIC_ANALYSIS
(包含)之后的每个编译阶段之后执行。转换的上下文在每个阶段之后保持不变,让你有机会检查两个阶段之间发生了什么变化。
作为一个例子,下面是如何dump类节点上注册的AST转换列表
import groovy.transform.ASTTest
import groovy.transform.CompileStatic
import groovy.transform.Immutable
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.control.CompilePhase
@ASTTest(value={
System.err.println "Compile phase: $compilePhase"
ClassNode cn = node
System.err.println "Global AST xforms: ${compilationUnit?.ASTTransformationsContext?.globalTransformNames}"
CompilePhase.values().each {
def transforms = cn.getTransforms(it)
if (transforms) {
System.err.println "Ast xforms for phase $it:"
transforms.each { map ->
System.err.println(map)
}
}
}
})
@CompileStatic
@Immutable
class Foo {
}
以下是如何在两个阶段之间记忆变量以进行测试
import groovy.transform.ASTTest
import groovy.transform.ToString
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.control.CompilePhase
@ASTTest(value={
if (compilePhase == CompilePhase.INSTRUCTION_SELECTION) { (1)
println "toString() was added at phase: ${added}"
assert added == CompilePhase.CANONICALIZATION (2)
} else {
if (node.getDeclaredMethods('toString') && added == null) { (3)
added = compilePhase (4)
}
}
})
@ToString
class Foo {
String name
}
1 | 如果当前编译阶段是指令选择 |
2 | 那么我们要确保在CANONICALIZATION 中添加了toString |
3 | 否则,如果toString 存在且上下文中的变量added 为null |
4 | 那意味着这个编译阶段是添加toString 的阶段 |
Grape 处理
@groovy.lang.Grapes
Grape
是Groovy中嵌入的依赖管理引擎,它依赖于本指南章节中详细描述的几个注解。
开发AST转换
转换有两种:全局转换和局部转换。
-
全局转换由编译器应用于正在编译的代码,无论转换适用于何处。实现全局转换的已编译类位于添加到编译器类路径的JAR中,并包含服务定位器文件
META-INF/services/org.codehaus.groovy.transform.ASTTransformation
,其中包含转换类的名称。转换类必须具有无参数构造函数并实现org.codehaus.groovy.transform.ASTTransformation
接口。它将针对编译中的每个源运行,因此请务必不要创建以昂贵且耗时的方式扫描所有AST的转换,以保持编译器快速。 -
局部转换是通过注解要转换的代码元素而局部应用的转换。为此,我们重用注解表示法,并且这些注解应该实现
org.codehaus.groovy.transform.ASTTransformation
。编译器将发现它们并将转换应用于这些代码元素。
编译阶段指南
Groovy AST转换必须在九个定义的编译阶段之一中执行(org.codehaus.groovy.control.CompilePhase)。
全局转换可以在任何阶段应用,但局部转换只能在语义分析阶段或之后应用。简而言之,编译器阶段是
-
初始化:打开源文件并配置环境
-
解析:使用语法生成表示源代码的令牌树
-
转换:从令牌树创建抽象语法树(AST)。
-
语义分析:执行语法无法检查的一致性和有效性检查,并解析类。
-
规范化:完成构建AST
-
指令选择:选择指令集,例如Java 6或Java 7字节码级别
-
类生成:在内存中创建类的字节码
-
输出:将二进制输出写入文件系统
-
终结:执行任何最后的清理工作
一般来说,在编译阶段后期,可用的类型信息更多。如果你的转换关注读取AST,那么信息更丰富的后期阶段可能是一个不错的选择。如果你的转换关注写入AST,那么树更稀疏的早期阶段可能更方便。
局部转换
局部AST转换与其应用的上下文相关。在大多数情况下,上下文由注解定义,该注解将定义转换的范围。例如,注解一个字段将意味着转换适用于该字段,而注解类将意味着转换适用于整个类。
作为一个简单且天真的例子,考虑编写一个@WithLogging
转换,它将在方法调用开始和结束时添加控制台消息。因此,以下“Hello World”示例实际上将打印“Hello World”以及开始和停止消息
@WithLogging
def greet() {
println "Hello World"
}
greet()
局部AST转换是实现此目的的简单方法。它需要两件事
-
@WithLogging
注解的定义 -
添加日志表达式到方法的org.codehaus.groovy.transform.ASTTransformation实现
ASTTransformation是一个回调,它允许你通过org.codehaus.groovy.control.SourceUnit访问org.codehaus.groovy.ast.ModuleNode (AST)。
AST(抽象语法树)是一种树形结构,主要由org.codehaus.groovy.ast.expr.Expression(表达式)或org.codehaus.groovy.ast.expr.Statement(语句)组成。了解AST的简单方法是在调试器中探索它。一旦有了AST,你就可以分析它以找出有关代码的信息或重写它以添加新功能。
局部转换注解是简单的部分。这是@WithLogging
import org.codehaus.groovy.transform.GroovyASTTransformationClass
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(["gep.WithLoggingASTTransformation"])
public @interface WithLogging {
}
注解保留可以为SOURCE
,因为你不需要在该之后使用注解。这里的元素类型是METHOD
,即@WithLogging
,因为注解适用于方法。
但最重要的部分是@GroovyASTTransformationClass
注解。这将@WithLogging
注解链接到你将编写的ASTTransformation
类。gep.WithLoggingASTTransformation
是我们将要编写的ASTTransformation
的完全限定类名。此行将注解与转换连接起来。
有了这个,Groovy编译器将在源单元中找到@WithLogging
时调用gep.WithLoggingASTTransformation
。现在,在IDE中运行示例脚本时,在LoggingASTTransformation
中设置的任何断点都将被触发。
ASTTransformation
类有点复杂。以下是非常简单且非常天真的转换,用于为@WithLogging
添加方法开始和停止消息
@CompileStatic (1)
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS) (2)
class WithLoggingASTTransformation implements ASTTransformation { (3)
@Override
void visit(ASTNode[] nodes, SourceUnit sourceUnit) { (4)
MethodNode method = (MethodNode) nodes[1] (5)
def startMessage = createPrintlnAst("Starting $method.name") (6)
def endMessage = createPrintlnAst("Ending $method.name") (7)
def existingStatements = ((BlockStatement)method.code).statements (8)
existingStatements.add(0, startMessage) (9)
existingStatements.add(endMessage) (10)
}
private static Statement createPrintlnAst(String message) { (11)
new ExpressionStatement(
new MethodCallExpression(
new VariableExpression("this"),
new ConstantExpression("println"),
new ArgumentListExpression(
new ConstantExpression(message)
)
)
)
}
}
1 | 即使不是强制性的,如果你用Groovy编写AST转换,也强烈建议使用CompileStatic ,因为它会提高编译器的性能。 |
2 | 用org.codehaus.groovy.transform.GroovyASTTransformation注解,以告知转换需要在哪个编译阶段运行。此处,在语义分析阶段。 |
3 | 实现ASTTransformation 接口 |
4 | 它只有一个visit 方法 |
5 | nodes 参数是一个2个AST节点数组,其中第一个是注解节点(@WithLogging ),第二个是被注解的节点(方法节点) |
6 | 创建一条语句,该语句将在我们进入方法时打印一条消息 |
7 | 创建一条语句,该语句将在我们退出方法时打印一条消息 |
8 | 获取方法体,在这种情况下是BlockStatement |
9 | 在现有代码的第一条语句之前添加进入方法消息 |
10 | 在现有代码的最后一条语句之后附加退出方法消息 |
11 | 创建包装MethodCallExpression 的ExpressionStatement ,对应于this.println("message") |
值得注意的是,为了本示例的简洁性,我们没有进行必要的检查,例如检查被注解的节点是否确实是MethodNode
,或者方法体是否是BlockStatement
的实例。这项练习留给读者。
请注意在createPrintlnAst(String)
方法中创建新的println语句。为代码创建AST并不总是那么简单。在这种情况下,我们需要构造一个新的方法调用,传入接收者/变量、方法名和参数列表。创建AST时,将你尝试创建的代码写入Groovy文件,然后在调试器中检查该代码的AST,以了解要创建什么,这可能有所帮助。然后使用你从调试器中学到的知识编写一个像createPrintlnAst
这样的函数。
最后
@WithLogging
def greet() {
println "Hello World"
}
greet()
产生
Starting greet Hello World Ending greet
重要的是要注意,AST转换直接参与编译过程。初学者常犯的一个错误是将AST转换代码与使用该转换的类放在同一个源代码树中。通常,在同一个源代码树中意味着它们同时编译。由于转换本身将分阶段编译,并且每个编译阶段在进入下一个阶段之前处理同一源单元的所有文件,因此会产生直接后果:转换将不会在使用它的类之前编译!总之,AST转换在使用之前需要预编译。一般来说,将它们放在单独的源代码树中就很容易实现。 |
全局转换
全局AST转换与局部转换类似,但有一个主要区别:它们不需要注解,这意味着它们是全局应用的,也就是说,应用于每个正在编译的类。因此,将它们的使用限制在最后手段非常重要,因为它可能对编译器性能产生显著影响。
根据局部AST转换的例子,想象一下我们想跟踪所有方法,而不仅仅是那些用@WithLogging
注解的方法。基本上,我们需要这段代码与之前用@WithLogging
注解的代码行为相同
def greet() {
println "Hello World"
}
greet()
为了使之生效,需要两个步骤
-
在
META-INF/services
目录下创建org.codehaus.groovy.transform.ASTTransformation
描述符 -
创建
ASTTransformation
实现
描述符文件是必需的,并且必须在类路径上找到。它将包含一行
gep.WithLoggingASTTransformation
转换的代码看起来与局部情况类似,但需要使用SourceUnit
而不是ASTNode[]
参数
@CompileStatic (1)
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS) (2)
class WithLoggingASTTransformation implements ASTTransformation { (3)
@Override
void visit(ASTNode[] nodes, SourceUnit sourceUnit) { (4)
def methods = sourceUnit.AST.methods (5)
methods.each { method -> (6)
def startMessage = createPrintlnAst("Starting $method.name") (7)
def endMessage = createPrintlnAst("Ending $method.name") (8)
def existingStatements = ((BlockStatement)method.code).statements (9)
existingStatements.add(0, startMessage) (10)
existingStatements.add(endMessage) (11)
}
}
private static Statement createPrintlnAst(String message) { (12)
new ExpressionStatement(
new MethodCallExpression(
new VariableExpression("this"),
new ConstantExpression("println"),
new ArgumentListExpression(
new ConstantExpression(message)
)
)
)
}
}
1 | 即使不是强制性的,如果你用Groovy编写AST转换,也强烈建议使用CompileStatic ,因为它会提高编译器的性能。 |
2 | 用org.codehaus.groovy.transform.GroovyASTTransformation注解,以告知转换需要在哪个编译阶段运行。此处,在语义分析阶段。 |
3 | 实现ASTTransformation 接口 |
4 | 它只有一个visit 方法 |
5 | sourceUnit 参数允许访问正在编译的源,因此我们获取当前源的AST并从该文件中检索方法列表 |
6 | 我们迭代源文件中的每个方法 |
7 | 创建一条语句,该语句将在我们进入方法时打印一条消息 |
8 | 创建一条语句,该语句将在我们退出方法时打印一条消息 |
9 | 获取方法体,在这种情况下是BlockStatement |
10 | 在现有代码的第一条语句之前添加进入方法消息 |
11 | 在现有代码的最后一条语句之后附加退出方法消息 |
12 | 创建包装MethodCallExpression 的ExpressionStatement ,对应于this.println("message") |
AST API指南
虽然你已经看到可以直接实现ASTTransformation
接口,但在几乎所有情况下你都不会这样做,而是扩展org.codehaus.groovy.transform.AbstractASTTransformation类。此类提供了几个实用方法,使AST转换更容易编写。Groovy中包含的几乎所有AST转换都扩展了此类。
将一个表达式转换为另一个表达式是一个常见的用例。Groovy提供了一个类,使得实现这一点非常容易:org.codehaus.groovy.ast.ClassCodeExpressionTransformer
为了说明这一点,让我们创建一个@Shout
转换,它将方法调用参数中的所有String
常量转换为它们的大写版本。例如
@Shout
def greet() {
println "Hello World"
}
greet()
应该打印
HELLO WORLD
然后,转换的代码可以使用ClassCodeExpressionTransformer
来简化此操作
@CompileStatic
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)
class ShoutASTTransformation implements ASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
ClassCodeExpressionTransformer trn = new ClassCodeExpressionTransformer() { (1)
private boolean inArgList = false
@Override
protected SourceUnit getSourceUnit() {
sourceUnit (2)
}
@Override
Expression transform(final Expression exp) {
if (exp instanceof ArgumentListExpression) {
inArgList = true
} else if (inArgList &&
exp instanceof ConstantExpression && exp.value instanceof String) {
return new ConstantExpression(exp.value.toUpperCase()) (3)
}
def trn = super.transform(exp)
inArgList = false
trn
}
}
trn.visitMethod((MethodNode)nodes[1]) (4)
}
}
1 | 内部,转换创建了一个ClassCodeExpressionTransformer |
2 | 转换器需要返回源单元 |
3 | 如果在参数列表中检测到字符串类型的常量表达式,则将其转换为大写版本 |
4 | 在被注解的方法上调用转换器 |
编写AST转换需要对Groovy内部API有深入的了解。特别是,它需要了解AST类。由于这些类是内部的,因此将来API可能会发生变化,这意味着你的转换可能会损坏。尽管有此警告,但AST在过去一直非常稳定,这种情况很少发生。 |
抽象语法树的类属于org.codehaus.groovy.ast
包。建议读者使用Groovy控制台,特别是AST浏览器工具,以获取有关这些类的知识。另一个学习资源是AST Builder测试套件。
宏
直到2.5.0版本,在开发AST转换时,开发人员应该对编译器如何构建AST(抽象语法树)有深入的了解,以便知道如何在编译时添加新的表达式或语句。
虽然使用org.codehaus.groovy.ast.tool.GeneralUtils
静态方法可以减轻直接编写这些AST节点的负担,但它仍然是一种低级的方式。我们需要一些东西来抽象我们直接编写AST,而这正是Groovy宏的用途。它们允许你在编译期间直接添加代码,而无需将你心中的代码翻译成org.codehaus.groovy.ast.*
节点相关类。
让我们看一个例子,创建一个局部AST转换:@AddMessageMethod
。当应用于给定类时,它将向该类添加一个名为getMessage
的新方法。该方法将返回“42”。注解非常直观
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.TYPE])
@GroovyASTTransformationClass(["metaprogramming.AddMethodASTTransformation"])
@interface AddMethod { }
不使用宏,AST转换会是什么样子?大概是这样
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class AddMethodASTTransformation extends AbstractASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
ClassNode classNode = (ClassNode) nodes[1]
ReturnStatement code =
new ReturnStatement( (1)
new ConstantExpression("42")) (2)
MethodNode methodNode =
new MethodNode(
"getMessage",
ACC_PUBLIC,
ClassHelper.make(String),
[] as Parameter[],
[] as ClassNode[],
code) (3)
classNode.addMethod(methodNode) (4)
}
}
1 | 创建返回语句 |
2 | 创建常量表达式“42” |
3 | 将代码添加到新方法 |
4 | 将新方法添加到被注解的类 |
如果你不习惯AST API,那肯定不像你心中所想的代码。现在看看使用宏后,前面的代码是如何简化的。
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class AddMethodWithMacrosASTTransformation extends AbstractASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
ClassNode classNode = (ClassNode) nodes[1]
ReturnStatement simplestCode = macro { return "42" } (1)
MethodNode methodNode =
new MethodNode(
"getMessage",
ACC_PUBLIC,
ClassHelper.make(String),
[] as Parameter[],
[] as ClassNode[],
simplestCode) (2)
classNode.addMethod(methodNode) (3)
}
}
1 | 简单多了。你想要添加一个返回“42”的返回语句,而这正是你在macro 实用方法中可以读到的内容。你的普通代码将为你转换为org.codehaus.groovy.ast.stmt.ReturnStatement |
2 | 将返回语句添加到新方法 |
3 | 将新代码添加到被注解的类 |
虽然本示例中使用macro
方法创建语句,但macro
方法也可以用于创建表达式,这取决于你使用的macro
签名
-
macro(Closure)
: 使用闭包中的代码创建一个给定语句。 -
macro(Boolean,Closure)
: 如果为true,则将闭包中的表达式包装在语句中;如果为false,则返回一个表达式。 -
macro(CompilePhase, Closure)
: 在特定编译阶段使用闭包中的代码创建给定语句 -
macro(CompilePhase, Boolean, Closure)
: 在特定编译阶段创建语句或表达式(true == 语句,false == 表达式)。
所有这些签名都可以在org.codehaus.groovy.macro.runtime.MacroGroovyMethods 找到 |
有时我们可能只对创建一个给定的表达式感兴趣,而不是整个语句,为了做到这一点,我们应该使用带有布尔参数的任何macro
调用
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class AddGetTwoASTTransformation extends AbstractASTTransformation {
BinaryExpression onePlusOne() {
return macro(false) { 1 + 1 } (1)
}
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
ClassNode classNode = nodes[1]
BinaryExpression expression = onePlusOne() (2)
ReturnStatement returnStatement = GeneralUtils.returnS(expression) (3)
MethodNode methodNode =
new MethodNode("getTwo",
ACC_PUBLIC,
ClassHelper.Integer_TYPE,
[] as Parameter[],
[] as ClassNode[],
returnStatement (4)
)
classNode.addMethod(methodNode) (5)
}
}
1 | 我们告诉宏不要将表达式包装在语句中,我们只对表达式感兴趣 |
2 | 分配表达式 |
3 | 使用GeneralUtils 中的方法创建ReturnStatement 并返回表达式 |
4 | 将代码添加到新方法 |
5 | 将方法添加到类 |
宏很棒,但如果我们的宏无法接收参数或解析周围变量,我们就无法创建任何有用或可重用的东西。
在以下示例中,我们创建一个AST转换@MD5
,当应用于给定字符串字段时,它将添加一个返回该字段MD5值的方法。
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.FIELD])
@GroovyASTTransformationClass(["metaprogramming.MD5ASTTransformation"])
@interface MD5 { }
以及转换
@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
class MD5ASTTransformation extends AbstractASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
FieldNode fieldNode = nodes[1]
ClassNode classNode = fieldNode.declaringClass
String capitalizedName = fieldNode.name.capitalize()
MethodNode methodNode = new MethodNode(
"get${capitalizedName}MD5",
ACC_PUBLIC,
ClassHelper.STRING_TYPE,
[] as Parameter[],
[] as ClassNode[],
buildMD5MethodCode(fieldNode))
classNode.addMethod(methodNode)
}
BlockStatement buildMD5MethodCode(FieldNode fieldNode) {
VariableExpression fieldVar = GeneralUtils.varX(fieldNode.name) (1)
return macro(CompilePhase.SEMANTIC_ANALYSIS, true) { (2)
return java.security.MessageDigest
.getInstance('MD5')
.digest($v { fieldVar }.getBytes()) (3)
.encodeHex()
.toString()
}
}
}
1 | 我们需要一个对变量表达式的引用 |
2 | 如果使用标准包之外的类,我们应该添加任何必要的导入或使用限定名。当使用给定静态方法的限定名时,你需要确保它在适当的编译阶段解析。在这个特定情况下,我们指示宏在SEMANTIC_ANALYSIS阶段解析它,这是具有类型信息的第一个编译阶段。 |
3 | 为了替换宏中的任何expression ,我们需要使用$v 方法。$v 接收一个闭包作为参数,并且闭包只允许替换表达式,这意味着继承org.codehaus.groovy.ast.expr.Expression 的类。 |
正如我们之前提到的,macro
方法只能生成statements
和expressions
。但是如果我们想生成其他类型的节点,比如方法、字段等等呢?
org.codehaus.groovy.macro.transform.MacroClass
可以用来在我们的转换中创建类(ClassNode实例),就像我们之前使用macro
方法创建语句和表达式一样。
下一个示例是一个局部转换@Statistics
。当应用于给定类时,它将添加两个方法getMethodCount()和getFieldCount(),它们分别返回类中的方法和字段数量。这是标记注解。
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.TYPE])
@GroovyASTTransformationClass(["metaprogramming.StatisticsASTTransformation"])
@interface Statistics {}
以及AST转换
@CompileStatic
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class StatisticsASTTransformation extends AbstractASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
ClassNode classNode = (ClassNode) nodes[1]
ClassNode templateClass = buildTemplateClass(classNode) (1)
templateClass.methods.each { MethodNode node -> (2)
classNode.addMethod(node)
}
}
@CompileDynamic
ClassNode buildTemplateClass(ClassNode reference) { (3)
def methodCount = constX(reference.methods.size()) (4)
def fieldCount = constX(reference.fields.size()) (5)
return new MacroClass() {
class Statistics {
java.lang.Integer getMethodCount() { (6)
return $v { methodCount }
}
java.lang.Integer getFieldCount() { (7)
return $v { fieldCount }
}
}
}
}
}
1 | 创建模板类 |
2 | 将模板类方法添加到被注解的类 |
3 | 传递引用类 |
4 | 提取参考类方法计数表达式 |
5 | 提取参考类字段计数表达式 |
6 | 使用引用方法计数表达式构建getMethodCount()方法 |
7 | 使用引用字段计数表达式构建getFieldCount()方法 |
基本上,我们创建了**Statistics**类作为模板,以避免编写低级AST API,然后我们将模板类中创建的方法复制到它们的最终目的地。
MacroClass 实现中的类型应该在内部解析,这就是为什么我们必须写java.lang.Integer 而不是简单地写Integer 。 |
请注意,我们正在使用@CompileDynamic 。这是因为我们使用MacroClass 的方式就像我们实际上正在实现它一样。所以如果你使用@CompileStatic ,它会抱怨,因为抽象类的实现不能是另一个不同的类。 |
你已经看到,通过使用macro
,你可以省去大量工作,但你可能想知道这个方法从何而来。你没有声明它或静态导入它。你可以将其视为一个特殊的全局方法(或者如果你喜欢,一个在每个Object
上的方法)。这很像println
扩展方法的定义方式。但与println
不同的是,macro
扩展是在编译过程的早期完成的,而println
是在编译过程的后期选择执行的方法。通过用@Macro
注解macro
方法的定义,并使用与扩展模块类似��机制使该方法可用,即可将macro
声明为这种早期扩展的可用方法之一。此类方法称为宏方法,好消息是你可以定义自己的宏方法。
要定义自己的宏方法,请以类似于扩展模块的方式创建一个类,并添加一个方法,例如
public class ExampleMacroMethods {
@Macro
public static Expression safe(MacroContext macroContext, MethodCallExpression callExpression) {
return ternaryX(
notNullX(callExpression.getObjectExpression()),
callExpression,
constX(null)
);
}
...
}
现在你将通过在META-INF/groovy
目录中放置org.codehaus.groovy.runtime.ExtensionModule
文件来将其注册为扩展模块。
现在,假设类和元信息文件都在你的类路径中,你可以按以下方式使用宏方法
def nullObject = null
assert null == safe(safe(nullObject.hashcode()).toString())
测试AST转换
本节讨论有关测试AST转换的最佳实践。前面几节强调了要执行AST转换,它必须预编译。这可能听起来很明显,但很多人都被它困住,试图在定义AST转换的同一个源树中使用它。
因此,测试AST转换的第一个技巧是将测试源与转换源分开。同样,这只是最佳实践,但你必须确保你的构建工具也确实将它们分开编译。Apache Maven和Gradle都默认支持这种情况。
在AST转换中设置断点以便在IDE中调试代码非常方便。但是,你可能会惊讶地发现IDE没有在断点处停止。原因其实很简单:如果你的IDE使用Groovy编译器编译AST转换的单元测试,那么编译是从IDE触发的,但将编译文件的进程没有调试选项。只有当测试用例执行时,调试选项才会在虚拟机上设置。简而言之:为时已晚,类已经编译,并且你的转换已经应用。
一个非常简单的解决方案是使用GroovyTestCase
类,它提供了一个assertScript
方法。这意味着你不需要在测试用例中这样写
static class Subject {
@MyTransformToDebug
void methodToBeTested() {}
}
void testMyTransform() {
def c = new Subject()
c.methodToBeTested()
}
你应该这样写
void testMyTransformWithBreakpoint() {
assertScript '''
import metaprogramming.MyTransformToDebug
class Subject {
@MyTransformToDebug
void methodToBeTested() {}
}
def c = new Subject()
c.methodToBeTested()
'''
}
区别在于,当你使用assertScript
时,assertScript
块中的代码是**在单元测试执行时**编译的。也就是说,此时Subject
类将在调试活动的情况下编译,并且将命中断点。
有时你可能希望对AST节点进行断言;或许是为了过滤节点,或者为了确保给定的转换已构建预期的AST节点。
过滤节点
例如,如果你只想将给定转换应用于特定的AST节点集,则可以使用**ASTMatcher**来过滤这些节点。以下示例显示了如何将给定表达式转换为另一个表达式。使用**ASTMatcher**,它查找特定表达式1 + 1
并将其转换为3
。这就是我们将其称为@Joking
示例的原因。
首先我们创建@Joking
注解,它只能应用于方法
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(["metaprogramming.JokingASTTransformation"])
@interface Joking { }
然后是转换,它只将org.codehaus.groovy.ast.ClassCodeExpressionTransformer
的实例应用于方法代码块中的所有表达式。
@CompileStatic
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class JokingASTTransformation extends AbstractASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
MethodNode methodNode = (MethodNode) nodes[1]
methodNode
.getCode()
.visit(new ConvertOnePlusOneToThree(source)) (1)
}
}
1 | 获取方法的代码语句并应用表达式转换器 |
此时,将使用**ASTMatcher**将转换仅应用于与表达式1 + 1
匹配的那些表达式。
class ConvertOnePlusOneToThree extends ClassCodeExpressionTransformer {
SourceUnit sourceUnit
ConvertOnePlusOneToThree(SourceUnit sourceUnit) {
this.sourceUnit = sourceUnit
}
@Override
Expression transform(Expression exp) {
Expression ref = macro { 1 + 1 } (1)
if (ASTMatcher.matches(ref, exp)) { (2)
return macro { 3 } (3)
}
return super.transform(exp)
}
}
1 | 构建用作参考模式的表达式 |
2 | 检查当前评估的表达式是否与参考表达式匹配 |
3 | 如果匹配,则将当前表达式替换为使用macro 构建的表达式 |
然后你可以如下测试实现
package metaprogramming
class Something {
@Joking
Integer getResult() {
return 1 + 1
}
}
assert new Something().result == 3
单元测试AST转换
通常,我们只是检查转换的最终使用是否符合预期来测试AST转换。但如果能有一种简单的方法来检查,例如,转换添加的节点是否与我们最初预期的一致,那将非常棒。
以下转换将一个名为giveMeTwo
的新方法添加到被注解的类中。
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class TwiceASTTransformation extends AbstractASTTransformation {
static final String VAR_X = 'x'
@Override
void visit(ASTNode[] nodes, SourceUnit source) {
ClassNode classNode = (ClassNode) nodes[1]
MethodNode giveMeTwo = getTemplateClass(sumExpression)
.getDeclaredMethods('giveMeTwo')
.first()
classNode.addMethod(giveMeTwo) (1)
}
BinaryExpression getSumExpression() { (2)
return macro {
$v{ varX(VAR_X) } +
$v{ varX(VAR_X) }
}
}
ClassNode getTemplateClass(Expression expression) { (3)
return new MacroClass() {
class Template {
java.lang.Integer giveMeTwo(java.lang.Integer x) {
return $v { expression }
}
}
}
}
}
1 | 将方法添加到被注解的类 |
2 | 构建一个二进制表达式。二进制表达式在+ 标记的两侧使用相同的变量表达式(检查org.codehaus.groovy.ast.tool.GeneralUtils 中的varX 方法)。 |
3 | 构建一个新的ClassNode,其中包含一个名为giveMeTwo 的方法,该方法返回作为参数传递的表达式的结果。 |
现在,不再创建对给定示例代码执行转换的测试。我想检查二进制表达式的构造是否正确完成
void testTestingSumExpression() {
use(ASTMatcher) { (1)
TwiceASTTransformation sample = new TwiceASTTransformation()
Expression referenceNode = macro {
a + a (2)
}.withConstraints { (3)
placeholder 'a' (4)
}
assert sample
.sumExpression
.matches(referenceNode) (5)
}
}
1 | 将ASTMatcher用作类别 |
2 | 构建模板节点 |
3 | 对该模板节点施加一些约束 |
4 | 告诉编译器a 是一个占位符。 |
5 | 断言参考节点和当前节点相等 |
当然,你总是可以/应该检查实际执行
void testASTBehavior() {
assertScript '''
package metaprogramming
@Twice
class AAA {
}
assert new AAA().giveMeTwo(1) == 2
'''
}
3.5. 使用Grape进行依赖管理
3.5.1. 快速入门
添加依赖项
Grape是嵌入在Groovy中的JAR依赖管理器。Grape允许你快速将maven仓库依赖项添加到你的类路径中,使脚本编写变得更加容易。最简单的用法就像向脚本添加注解一样简单
@Grab(group='org.springframework', module='spring-orm', version='5.2.8.RELEASE')
import org.springframework.jdbc.core.JdbcTemplate
@Grab
还支持速记符号
@Grab('org.springframework:spring-orm:5.2.8.RELEASE')
import org.springframework.jdbc.core.JdbcTemplate
请注意,我们在这里使用带有注解的导入,这是推荐的方式。你也可以在mvnrepository.com上搜索依赖项,它将为你提供pom.xml
条目的@Grab
注解形式。
指定其他仓库
并非所有依赖项都在Maven Central中。你可以像这样添加新的依赖项
@GrabResolver(name='restlet', root='http://maven.restlet.org/')
@Grab(group='org.restlet', module='org.restlet', version='1.1.6')
Maven 分类器
一些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 驱动程序
由于JDBC驱动程序的加载方式,你需要配置Grape将JDBC驱动程序依赖项附加到系统类加载器。即
@GrabConfig(systemClassLoader=true)
@Grab(group='mysql', module='mysql-connector-java', version='5.1.6')
从Groovy Shell中使用Grape
从 groovysh 使用方法调用变体
groovy.grape.Grape.grab(group:'org.springframework', module:'spring', version:'2.5.6')
代理设置
如果你在防火墙后面和/或需要通过代理服务器使用Groovy/Grape,你可以在命令行中通过http.proxyHost
和http.proxyPort
系统属性指定这些设置
groovy -Dhttp.proxyHost=yourproxy -Dhttp.proxyPort=8080 yourscript.groovy
或者你可以通过将这些属性添加到你的JAVA_OPTS环境变量来使其在系统范围内生效
JAVA_OPTS = -Dhttp.proxyHost=yourproxy -Dhttp.proxyPort=8080
日志记录
如果你想查看Grape正在做什么,请将系统属性groovy.grape.report.downloads
设置为true
(例如,在调用或JAVA_OPTS中添加-Dgroovy.grape.report.downloads=true
),Grape会将以下信息打印到System.error
-
开始解析依赖项
-
开始下载工件
-
重试下载工件
-
下载工件的大小和时间
要获得更详细的日志,请增加Ivy日志级别(默认为-1
)。例如,-Divy.message.logger.level=4
。
3.5.2. 细节
Grape(Groovy Adaptable Packaging Engine 或 Groovy Advanced Packaging Engine)是Groovy中实现grab()
调用的基础设施,它是一组利用Ivy的类,允许Groovy实现基于仓库的模块系统。这使得开发人员可以编写一个脚本,该脚本具有基本上任意的库需求,然后只发布该脚本。Grape将在运行时,在脚本运行时根据需要下载并链接命名的库以及形成传递闭包的所有依赖项,这些依赖项来自现有的仓库,如Maven Central。
Grape遵循Ivy的模块版本标识约定,并进行了命名更改。
-
group
- 模块所属的模块组。直接转换为Maven groupId或Ivy Organization。任何匹配/groovy[x][\..*]^/
的组都已保留,并且可能对groovy认可的模块具有特殊含义。 -
module
- 要加载的模块的名称。直接转换为Maven artifactId或Ivy artifact。 -
version
- 要使用的模块版本。可以是字面版本`1.1-RC3',也可以是Ivy范围`[2.2.1,)'(表示2.2.1或任何更高版本)。 -
classifier
- 要使用的可选分类器(例如,jdk15)
下载的模块将按照Ivy的标准机制存储,缓存根目录为~/.groovy/grapes
3.5.3. 用法
注解
可以在接受注解的任何位置添加一个或多个groovy.lang.Grab
注解,以告诉编译器此代码依赖于特定库。这将把库添加到groovy编译器的类加载器中。此注解在脚本中任何其他类的解析之前被检测和评估,因此导入的类可以由@Grab
注解正确解析。
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
}
}
一个适当的grab(…)
调用将被添加到包含类的静态初始化器(或脚本元素被注解时,添加到脚本类)中。
多个Grape注解
在Groovy的早期版本中,如果你想在同一个节点上多次使用Grab注解,你必须使用@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
但在最新版本中,@Grapes是纯粹可选的。
技术说明
-
最初,Groovy存储Grab注解以便在运行时访问,并且字节码中不允许重复。在当前版本中,@Grab只有SOURCE保留,因此多次出现不是问题。
-
Grape的未来版本可能会支持使用Grapes注解来提供一定程度的结构化,例如,允许GrabExclude或GrabResolver注解仅应用于Grab注解的子集。
方法调用
通常,对grab的调用会在脚本的早期或类初始化时发生。这是为了确保在groovy代码依赖代码之前,库已可用于ClassLoader。一些典型的调用可能如下所示
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'])
-
在相同上下文中使用相同参数多次调用 grab 应该是幂等的。但是,如果使用不同的
ClassLoader
上下文调用相同的代码,则可能会重新运行解析。 -
如果传递给
grab
调用的args
映射具有noExceptions
属性,且该属性评估为true,则不会抛出异常。 -
grab
要求指定RootLoader
或GroovyClassLoader
,或者它位于调用类的ClassLoader
链中。默认情况下,未能提供此类ClassLoader
将导致模块解析和抛出异常-
通过
classLoader:
参数及其父类加载器传入的类加载器。 -
作为
referenceObject:
参数传入的对象的类的类加载器,以及其父类加载器。 -
发出
grab
调用的类的类加载器
-
grab(HashMap) 参数
-
group:
- <String> - 模块所属的模块组。直接转换为Maven groupId。任何匹配/groovy(|\..|x|x\..)/
的组都已保留,并且可能对groovy认可的模块具有特殊含义。 -
module:
- <String> - 要加载的模块的名称。直接转换为Maven artifactId。 -
version:
- <String> 和可选的 <Range> - 要使用的模块版本。可以是字面版本 `1.1-RC3' 或 Ivy 范围 `[2.2.1,)'(表示 2.2.1 或任何更大版本)。 -
classifier:
- <String> - 用于解析的Maven分类器。 -
conf:
- <String>,默认值default' - 要下载的模块的配置或范围。默认配置是`default:
,它映射到maven的runtime
和master
范围。 -
force:
- <boolean>,默认为true - 用于指示在发生冲突时必须使用此修订版,而不管 -
冲突管理器
-
changing:
- <boolean>,默认值为 false - 表示该工件是否可以在其版本标识不改变的情况下发生变化。 -
transitive:
- <boolean>,默认值为 true - 是否解析此模块的其他依赖项。
grab
有两种主要变体,一种只有一个Map,另一种有一个arguments Map和多个dependencies Map。调用单个Map的grab与两次传入相同Map的grab相同,因此grab参数和依赖项可以混合在同一个Map中,并且grab可以作为带命名参数的单个方法调用。
这些参数有同义词。提交多个将导致运行时异常。
-
group:
,groupId:
,organisation:
,organization:
,org:
-
module:
,artifactId:
,artifact:
-
version:
,revision:
,rev:
-
conf:
,scope:
,configuration:
参数映射参数
-
classLoader:
- <GroovyClassLoader> 或 <RootClassLoader> - 要添加已解析Jar的类加载器 -
refObject:
- <Object> - 对象的类的最近父类加载器将被视为已作为classLoader:
传入。 -
validate:
- <boolean>,默认为 false - pom 或 ivy 文件是否应被验证(true),或者我们是否应信任缓存(false)。 -
noExceptions:
- <boolean>,默认为 false - 如果 ClassLoader 解析或仓库查询失败,我们是否应该抛出异常(false)或静默失败(true)。
命令行工具
Grape添加了一个命令行可执行文件`grape`,允许检查和管理本地grape缓存。
grape install [-hv] <group> <module> [<version>] [<classifier>]
这将安装指定的groovy模块或maven工件。如果指定了版本,则将安装该特定版本,否则将使用最新版本(如同传入`*')。
grape list
列出本地安装的模块(在groovy模块的情况下包含其完整的maven名称)和版本。
grape resolve [-adhisv] (<groupId> <artifactId> <version>)+
这将返回表示指定模块的工件以及各自传递依赖项的jar文件位置。你可以选择传入-ant、-dos或-shell,以获取适用于ant脚本、windows批处理文件或unix shell脚本的格式表示的依赖项。可以传入-ivy以查看以ivy类似格式表示的依赖项。
grape uninstall [-hv] <group> <module> <version>
这将卸载特定的grape:它将非传递地从grape缓存中移除相应的jar文件。
高级配置
仓库目录
如果需要更改grape用于下载库的目录,可以指定grape.root系统属性来更改默认值(默认为~/.groovy/grapes)
groovy -Dgrape.root=/repo/grapes yourscript.groovy
更多示例
使用 Apache Commons Collections
// create and use a primitive array list
@Grab(group='commons-primitives', module='commons-primitives', version='1.0')
import org.apache.commons.collections.primitives.ArrayIntList
def createEmptyInts() { new ArrayIntList() }
def ints = createEmptyInts()
ints.add(0, 42)
assert ints.size() == 1
assert ints.get(0) == 42
使用 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 }
使用 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'
启动 Jetty 服务器以提供 Groovy 模板
@Grab('org.eclipse.jetty.aggregate:jetty-server:8.1.19.v20160209')
@Grab('org.eclipse.jetty.aggregate:jetty-servlet:8.1.19.v20160209')
@Grab('javax.servlet:javax.servlet-api:3.0.1')
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.servlet.ServletContextHandler
import groovy.servlet.TemplateServlet
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 及其依赖项,并缓存它们。我们创建一个监听 8080 端口的新 Jetty 服务器,然后将 Groovy 的 TemplateServlet 暴露在上下文的根路径上——Groovy 拥有自己强大的模板引擎机制。我们启动服务器并让它运行一段时间。每次有人访问 https://:8080/somepage.gsp 时,它都会向用户显示 somepage.gsp 模板——这些模板页面应该位于与此服务器脚本相同的目录中。
3.6. 测试指南
3.6.1. 简介
Groovy 编程语言对编写测试提供了极大的支持。除了语言特性以及与最先进的测试库和框架的测试集成之外,Groovy 生态系统还诞生了一套丰富的测试库和框架。
本章将从语言特定的测试特性开始,然后更深入地探讨 JUnit 集成、用于规范的 Spock 和用于功能测试的 Geb。最后,我们将概述其他已知与 Groovy 配合使用的测试库。
3.6.2. 语言特性
除了集成支持 JUnit 之外,Groovy 编程语言还提供了已被证明对测试驱动开发非常有价值的特性。本节将深入探讨它们。
Power 断言
编写测试意味着通过使用断言来 формулировать 假设。在 Java 中,这可以通过使用 J2SE 1.4 中添加的 assert
关键字来完成。在 Java 中,assert
语句可以通过 JVM 参数 -ea
(或 -enableassertions
) 和 -da
(或 -disableassertions
) 启用。Java 中的断言语句默认是禁用的。
Groovy 提供了一种相当强大的 assert
变体,也称为power 断言语句。Groovy 的 power assert
与 Java 版本不同之处在于当布尔表达式验证为 false
时的输出。
def x = 1
assert x == 2
// Output: (1)
//
// Assertion failed:
// assert x == 2
// | |
// 1 false
1 | 本节显示标准错误输出 |
当断言无法成功验证时抛出的 java.lang.AssertionError
包含原始异常消息的扩展版本。power 断言输出显示从外部表达式到内部表达式的评估结果。
power 断言语句的真正威力体现在复杂的布尔语句,或包含集合或其他启用了 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 中,断言默认是启用的。取消断言的可能性一直是语言设计决定。或者,正如 Bertrand Meyer 所说,如果你把脚伸进真正的水里,就没必要脱掉你的游泳圈
。
需要注意的一点是 power 断言语句中布尔表达式内带副作用的方法。由于内部错误消息构建机制只存储目标实例的引用,因此在涉及带副作用的方法时,错误消息文本在渲染时可能会无效。
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 是自定义错误消息。但请注意,这将禁用 power 断言,并完全回退到断言错误时的自定义错误消息。 |
模拟和存根
Groovy 对各种模拟和存根替代方案提供了出色的内置支持。在使用 Java 时,动态模拟框架非常流行。一个主要原因是,使用 Java 创建自定义手工模拟非常困难。如果您愿意,这些框架可以很容易地与 Groovy 一起使用,但在 Groovy 中创建自定义模拟要容易得多。您通常可以使用简单的映射或闭包来构建自定义模拟。
以下部分展示了仅使用 Groovy 语言特性创建模拟和存根的方法。
映射强制转换
通过使用映射或扩展对象,我们可以非常容易地整合协作者的所需行为,如下所示:
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
运算符可用于将映射强制转换为特定类。给定的映射键被解释为方法名,而值(groovy.lang.Closure
块)被解释为方法代码块。
请注意,如果自定义 java.util.Map 后代类与 as 运算符结合使用,映射强制转换可能会阻碍您。映射强制转换机制直接针对某些集合类,它不考虑自定义类。 |
闭包强制转换
'as' 运算符可以巧妙地与闭包一起使用,这对于简单场景中的开发人员测试非常有用。我们还没有发现这种技术强大到足以让我们放弃动态模拟,但它在简单情况下仍然非常有用。
持有单个方法的类或接口,包括 SAM(Single Abstract Method)类,可用于将闭包块强制转换为给定类型的对象。请注意,为了实现此目的,Groovy 内部会为给定类创建一个代理对象。因此,该对象不会是给定类的直接实例。如果例如,生成代理对象的元类随后被更改,这一点很重要。
我们来看一个将闭包强制转换为特定类型的例子
def service = { String key -> 'some text' } as TranslationService
assert 'some text' == service.convert('key.text')
Groovy 支持一种称为隐式 SAM 强制转换的特性。这意味着在运行时可以推断目标 SAM 类型的情况下,不需要 as
运算符。这种类型的强制转换在测试中可能有助于模拟整个 SAM 类。
abstract class BaseService {
abstract void doSomething()
}
BaseService service = { -> println 'doing something' }
service.doSomething()
MockFor 和 StubFor
Groovy 的模拟和存根类可以在 groovy.mock.interceptor
包中找到。
MockFor
类通过允许定义协作对象行为的严格有序预期,支持对类进行(通常是单元)独立测试。典型的测试场景涉及一个被测类和一个或多个协作对象。在这种场景中,通常只测试被测类的业务逻辑是可取的。一种实现方法是用简化的模拟对象替换协作对象实例,以帮助隔离测试目标中的逻辑。MockFor 允许使用元编程创建此类模拟。协作对象的期望行为被定义为行为规范。该行为被强制执行并自动检查。
假设我们的目标类如下所示
class Person {
String first, last
}
class Family {
Person father, mother
def nameOfMother() { "$mother.first $mother.last" }
}
使用 MockFor
,模拟期望始终依赖于序列,并且其使用会自动以调用 verify
结束。
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 | 一个新的模拟是通过 MockFor 的新实例创建的。 |
2 | 一个 Closure 被传递给 use ,它启用模拟功能。 |
3 | 对 verify 的调用检查方法调用的序列和数量是否符合预期。 |
StubFor
类通过允许定义协作对象行为的松散有序期望,支持对类进行(通常是单元)独立测试。典型的测试场景涉及一个被测类和一个或多个协作对象。在这种场景中,通常只测试 CUT(被测类)的业务逻辑是可取的。一种实现方法是用简化的存根对象替换协作对象实例,以帮助隔离目标类中的逻辑。StubFor
允许使用元编程创建此类存根。协作对象的期望行为被定义为行为规范。
与 MockFor
相反,使用 verify
检查的存根期望与序列无关,并且其使用是可选的。
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 | 一个新的存根是通过 StubFor 的新实例创建的。 |
2 | with 方法用于将闭包内的所有调用委托给 StubFor 实例。 |
3 | 一个 Closure 被传递给 use ,它启用存根功能。 |
4 | 调用 verify (可选)检查方法调用次数是否符合预期。 |
MockFor
和 StubFor
不能用于测试静态编译的类,例如 Java 类或使用了 @CompileStatic
的 Groovy 类。要存根和/或模拟这些类,您可以使用 Spock 或其中一个 Java 模拟库。
Expando Meta-Class (EMC)
Groovy 包含一个特殊的 MetaClass
,即所谓的 ExpandoMetaClass
(EMC)。它允许使用简洁的闭包语法动态添加方法、构造函数、属性和静态方法。
每个 java.lang.Class
都提供了一个特殊的 metaClass
属性,该属性将给出 ExpandoMetaClass
实例的引用。扩展元类不仅限于自定义类,它也可以用于 JDK 类,例如 java.lang.String
。
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
是模拟功能的一个相当好的选择,因为它允许更高级的模拟,例如模拟静态方法。
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'
甚至构造函数
Book.metaClass.constructor << { String title -> new Book(title:title) }
def b = new Book("The Stand")
assert b.title == 'The Stand'
模拟构造函数可能看起来像一个最好不要考虑的黑客行为,但即使如此,也可能存在有效的用例。一个例子可以在 Grails 中找到,其中域类构造函数在运行时借助 ExpandoMetaClass 添加。这使得域对象可以在 Spring 应用程序上下文中注册自己,并允许注入服务或其他由依赖注入容器控制的 bean。 |
如果您想在每个测试方法级别更改 metaClass
属性,则需要删除对元类所做的更改,否则这些更改将在跨测试方法调用时持续存在。通过替换 GroovyMetaClassRegistry
中的元类来删除更改。
GroovySystem.metaClassRegistry.removeMetaClass(String)
另一种选择是注册一个 MetaClassRegistryChangeEventListener
,跟踪更改的类,并在您选择的测试运行时清理方法中删除更改。一个很好的例子可以在 Grails Web 开发框架中找到。
除了在类级别使用 ExpandoMetaClass
之外,还支持在每个对象级别使用元类。
def b = new Book(title: "The Stand")
b.metaClass.getTitle {-> 'My Title' }
assert b.title == 'My Title'
在这种情况下,元类更改仅与实例相关。根据测试场景,这可能比全局元类更改更适合。
GDK 方法
以下部分简要概述了可以在测试用例场景中利用的 GDK 方法,例如用于测试数据生成。
Iterable#combinations
添加到 java.lang.Iterable
兼容类上的 combinations
方法可用于从包含两个或更多子列表的列表中获取组合列表。
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
添加到 java.lang.Iterable
上的 eachCombination
方法可用于将函数(或在此处为 groovy.lang.Closure
)应用于 combinations
方法已构建的每个组合。
eachCombination
是一个 GDK 方法,它被添加到所有符合 java.lang.Iterable
接口的类中。它对输入列表的每个组合应用一个函数。
void testEachCombination() {
[[2, 3],[4, 5, 6]].eachCombination { println it[0] + it[1] }
}
该方法可在测试上下文中使用,以使用每个生成的组合调用方法。
工具支持
测试代码覆盖率
代码覆盖率是衡量(单元)测试有效性的有用指标。具有高代码覆盖率的程序比没有或低覆盖率的程序发生关键错误的几率更低。为了获取代码覆盖率指标,生成的字节码通常需要在测试执行之前进行插桩。一个支持 Groovy 执行此任务的工具是 Cobertura。
以下代码清单显示了如何在 Groovy 项目的 Gradle 构建脚本中启用 Cobertura 测试覆盖率报告的示例。
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 覆盖率报告可以选择多种输出格式,并且可以将测试代码覆盖率报告添加到持续集成构建任务中。
3.6.3. 使用 JUnit 进行测试
Groovy 通过以下方式简化了 JUnit 测试:
-
您使用的整体实践与使用 Java 进行测试时相同,但您可以在测试中采用 Groovy 简洁的语法,使其更加简洁。如果您愿意,甚至可以使用编写测试领域特定语言 (DSL) 的功能。
-
有许多辅助类简化了许多测试活动。详细信息在某些情况下因您使用的 JUnit 版本而异。我们将很快介绍这些细节。
-
Groovy 的 PowerAssert 机制在您的测试中非常棒。
-
Groovy 认为测试非常重要,您应该能够像运行脚本或类一样轻松地运行它们。这就是为什么 Groovy 在使用
groovy
命令或 GroovyConsole 时包含一个自动测试运行器。这为您提供了运行测试之外的一些附加选项。
在以下部分中,我们将更深入地探讨 JUnit 3、4 和 5 的 Groovy 集成。
JUnit 3
也许支持 JUnit 3 测试最著名的 Groovy 类之一是 GroovyTestCase
类。它派生自 junit.framework.TestCase
,提供了大量额外的方法,使 Groovy 中的测试变得轻而易举。
尽管 GroovyTestCase 继承自 TestCase ,但这并不意味着您不能在项目中使用 JUnit 4 功能。事实上,最新的 Groovy 版本捆绑了 JUnit 4,并且附带了向后兼容的 TestCase 实现。在 Groovy 邮件列表中有一些关于是否使用 GroovyTestCase 或 JUnit 4 的讨论,结果是这主要是一个品味问题,但使用 GroovyTestCase 您可以免费获得大量方法,这些方法使某些类型的测试更容易编写。 |
在本节中,我们将介绍 GroovyTestCase
提供的一些方法。这些方法的完整列表可以在 groovy.test.GroovyTestCase 的 JavaDoc 文档中找到,不要忘记它继承自 junit.framework.TestCase
,而 TestCase
又继承了所有 assert*
方法。
断言方法
GroovyTestCase
继承自 junit.framework.TestCase
,因此它继承了大量可用于在每个测试方法中调用的断言方法。
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 方法
shouldFail
可用于检查给定代码块是否失败。如果失败,则断言成立,否则断言失败。
void testInvalidIndexAccess1() {
def numbers = [1,2,3,4]
shouldFail {
numbers.get(4)
}
}
上面的示例使用基本的 shouldFail
方法接口,该接口将 groovy.lang.Closure
作为单个参数。Closure
实例包含预期在运行时中断的代码。
如果我们想对特定的 java.lang.Exception
类型断言 shouldFail
,我们可以使用接受 Exception
类作为第一个参数和 Closure
作为第二个参数的 shouldFail
实现。
void testInvalidIndexAccess2() {
def numbers = [1,2,3,4]
shouldFail IndexOutOfBoundsException, {
numbers.get(4)
}
}
如果抛出 IndexOutOfBoundsException
(或其子类)以外的任何异常,则测试用例将失败。
shouldFail
一个非常好的特性到目前为止还没有出现:它返回异常消息。如果您想对异常错误消息进行断言,这真的很有用。
void testInvalidIndexAccess3() {
def numbers = [1,2,3,4]
def msg = shouldFail IndexOutOfBoundsException, {
numbers.get(4)
}
assert msg.contains('Index: 4, Size: 4') ||
msg.contains('Index 4 out-of-bounds for length 4') ||
msg.contains('Index 4 out of bounds for length 4')
}
notYetImplemented 方法
notYetImplemented
方法受到了 HtmlUnit 的极大影响。它允许编写一个测试方法,但将其标记为尚未实现。只要测试方法失败并标记为 notYetImplemented
,测试就会通过。
void testNotYetImplemented1() {
if (notYetImplemented()) return (1)
assert 1 == 2 (2)
}
1 | 对于 GroovyTestCase ,需要调用 notYetImplemented 才能获取当前方法堆栈。 |
2 | 只要测试结果为 false ,测试执行就会成功。 |
notYetImplemented
方法的替代方案是 @NotYetImplemented
注解。它允许将方法标记为尚未实现,其行为与 GroovyTestCase#notYetImplemented
完全相同,但不需要调用 notYetImplemented
方法。
@NotYetImplemented
void testNotYetImplemented2() {
assert 1 == 2
}
JUnit 4
Groovy 可用于编写 JUnit 4 测试用例,没有任何限制。groovy.test.GroovyAssert
包含各种静态方法,可用作 JUnit 4 测试中 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
中找到的静态方法在类定义开始处导入,因此 shouldFail
的使用方式与在 GroovyTestCase
中相同。
groovy.test.GroovyAssert 继承自 org.junit.Assert ,这意味着它继承了所有 JUnit 断言方法。然而,随着 power 断言语句的引入,依赖断言语句而不是使用 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'
}
JUnit 5
JUnit4 中描述的大多数方法和辅助类也适用于 JUnit5,但是 JUnit5 在编写测试时使用了一些略有不同的类注解。有关更多详细信息,请参阅 JUnit5 文档。
按照正常的 JUnit5 指南创建您的测试类,如本示例所示:
class MyTest {
@Test
void streamSum() {
assertTrue(Stream.of(1, 2, 3)
.mapToInt(i -> i)
.sum() > 5, () -> "Sum should be greater than 5")
}
@RepeatedTest(value=2, name = "{displayName} {currentRepetition}/{totalRepetitions}")
void streamSumRepeated() {
assert Stream.of(1, 2, 3).mapToInt(i -> i).sum() == 6
}
private boolean isPalindrome(s) { s == s.reverse() }
@ParameterizedTest (1)
@ValueSource(strings = [ "racecar", "radar", "able was I ere I saw elba" ])
void palindromes(String candidate) {
assert isPalindrome(candidate)
}
@TestFactory
def dynamicTestCollection() {[
dynamicTest("Add test") { -> assert 1 + 1 == 2 },
dynamicTest("Multiply Test", () -> { assert 2 * 3 == 6 })
]}
}
1 | 如果项目中尚未存在,此测试需要额外的 org.junit.jupiter:junit-jupiter-params 依赖。 |
如果您的 IDE 或构建工具支持并配置了 JUnit5,您可以在其中运行测试。如果您在 GroovyConsole 中或通过 groovy
命令运行上述测试,您将看到运行测试结果的简短文本摘要。
JUnit5 launcher: passed=8, failed=0, skipped=0, time=246ms
更多详细信息可在 FINE
日志级别获取。您可以配置日志以显示此类信息,或按如下方式以编程方式进行:
@BeforeAll
static void init() {
def logger = Logger.getLogger(LoggingListener.name)
logger.level = Level.FINE
logger.addHandler(new ConsoleHandler(level: Level.FINE))
}
3.6.4. 使用 Spock 进行测试
Spock 是一个针对 Java 和 Groovy 应用程序的测试和规范框架。它之所以脱颖而出,是因为其优美且极具表现力的规范 DSL。实际上,Spock 规范是作为 Groovy 类编写的。虽然用 Groovy 编写,但它们可以用于测试 Java 类。Spock 可用于单元测试、集成测试或 BDD(行为驱动开发)测试,它不将自己归入特定类别的测试框架或库。
除了这些令人赞叹的功能外,Spock 还是如何利用第三方库中高级 Groovy 编程语言功能的一个很好的例子,例如,通过使用 Groovy AST 转换。 |
本节不应作为 Spock 使用的详细指南,而应提供 Spock 的大致印象以及如何将其用于单元测试、集成测试、功能测试或任何其他类型的测试。 |
下一节我们将初步了解 Spock 规范的结构。它应该能很好地让您了解 Spock 的目标。
规范
Spock 允许您编写描述感兴趣系统所展现的特性(属性、方面)的规范。“系统”可以是单个类和整个应用程序之间的任何内容,更高级的术语是待规范系统。特性描述从系统及其协作者的特定快照开始,此快照称为特性的夹具。
Spock 规范类派生自 spock.lang.Specification
。一个具体的规范类可能由字段、夹具方法、特性方法和辅助方法组成。
我们来看一个简单的规范,它包含一个针对虚构 Stack
类的单个特性方法。
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: (3)
stack.push 42
then: (4)
stack.size() == 1
}
}
1 | 特性方法,约定使用字符串字面量命名。 |
2 | Setup 块,这里需要完成此功能的任何设置工作。 |
3 | When 块描述了一个刺激,一个在此特性规范下的特定目标动作。 |
4 | Then 块,任何可用于验证由 when 块触发的代码结果的表达式。 |
Spock 特性规范在 spock.lang.Specification
类中定义为方法。它们通过使用字符串字面量而不是方法名来描述特性。
一个特性方法包含多个块,在我们的例子中使用了 setup
、when
和 then
。setup
块比较特殊,它是可选的,并允许配置在特性方法中可见的局部变量。when
块定义刺激,是 then
块的伴侣,then
块描述对刺激的响应。
请注意,上面 StackSpec
中的 setup
方法还带有一个描述字符串。描述字符串是可选的,可以在块标签(如 setup
、when
、then
)之后添加。
更多 Spock
Spock 提供了更多功能,如数据表或高级模拟功能。请随意查阅 Spock GitHub 页面以获取更多文档和下载信息。
3.6.5. 使用 Geb 进行功能测试
Geb 是一个功能性 Web 测试和抓取库,它与 JUnit 和 Spock 集成。它基于 Selenium Web 驱动,并且像 Spock 一样,提供了一个 Groovy DSL 来为 Web 应用程序编写功能测试。
Geb 具有出色的功能,使其非常适合作为功能测试库:
-
通过类似 JQuery 的
$
函数访问 DOM -
实现页面模式
-
通过模块支持某些 Web 组件(例如菜单栏等)的模块化
-
通过 JS 变量与 JavaScript 集成
本节不应作为如何使用 Geb 的详细指南,而应提供 Geb 的大致印象以及如何将其用于功能测试。 |
下一节将举例说明如何使用 Geb 为具有单个搜索字段的简单网页编写功能测试。
一个 Geb 脚本
尽管 Geb 可以独立用于 Groovy 脚本中,但在许多场景中,它与其它测试框架结合使用。Geb 提供了各种基类,可用于 JUnit 3、4、TestNG 或 Spock 测试。这些基类是需要添加为依赖项的额外 Geb 模块的一部分。
例如,以下 @Grab
依赖项可用于在 JUnit4 测试中运行带有 Selenium Firefox 驱动程序的 Geb。JUnit 3/4 支持所需的模块是 geb-junit4
。
@Grab('org.gebish:geb-core:0.9.2')
@Grab('org.gebish:geb-junit4:0.9.2')
@Grab('org.seleniumhq.selenium:selenium-firefox-driver:2.26.0')
@Grab('org.seleniumhq.selenium:selenium-support:2.26.0')
Geb 中的核心类是 geb.Browser
类。顾名思义,它用于浏览页面和访问 DOM 元素。
import geb.Browser
import org.openqa.selenium.firefox.FirefoxDriver
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 | 创建了一个新的 Browser 实例。在此示例中,它使用 Selenium FirefoxDriver 并设置了 baseUrl 。 |
2 | go 用于导航到 URL 或相对 URI。 |
3 | $ 与 CSS 选择器一起用于访问 username 和 password DOM 字段。 |
Browser
类带有一个 drive
方法,该方法将所有方法/属性调用委托给当前的 browser
实例。Browser
配置不必内联完成,也可以将其外部化到 GebConfig.groovy
配置文件中,例如。实际上,Browser
类的使用通常被 Geb 测试基类隐藏起来。它们将所有缺失的属性和方法调用委托给后台存在的当前 browser
实例。
class SearchTests extends geb.junit4.GebTest {
@Test
void executeSearch() {
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 接受相对或绝对链接并调用页面。 |
2 | Browser#$ 用于访问 DOM 内容。底层 Selenium 驱动程序支持的任何 CSS 选择器都允许使用。 |
3 | click 用于点击按钮。 |
4 | $ 用于从 searchResult 块中获取第一个链接。 |
上面的示例展示了一个使用 JUnit 4 基类 geb.junit4.GebTest
的简单 Geb Web 测试。请注意,在此示例中,Browser
配置是外部化的。GebTest
将 go
和 $
等方法委托给底层的 browser
实例。
3.7. 调整 Parrot 解析器性能
Parrot 解析器基于 antlr4,自 Groovy 3.0.0 以来引入。它提供了以下选项来调整解析性能:
选项 | 描述 | 默认 | 版本 | 示例 |
---|---|---|---|---|
groovy.parallel.parse |
并行解析 Groovy 源文件。 |
|
3.0.5+ |
-Dgroovy.parallel.parse=true |
groovy.antlr4.cache.threshold |
antlr4 严重依赖 DFA 缓存以获得更好的性能,因此 antlr4 不会清除 DFA 缓存,从而可能发生 OutOfMemoryError。Groovy 在解析性能和内存使用之间进行权衡,当解析的 Groovy 源文件数量达到缓存阈值时,DFA 缓存将被清除。注意: |
|
3.0.5+ |
-Dgroovy.antlr4.cache.threshold=200 |
groovy.antlr4.sll.threshold |
Parrot 解析器将尝试 SLL 模式,如果 SLL 失败,则尝试 LL 模式。但是,要解析的令牌越多,SLL 失败的可能性就越大。如果达到 SLL 阈值,SLL 将被跳过。将阈值设置为 |
|
3.0.9+ |
-Dgroovy.antlr4.sll.threshold=1000 |
groovy.antlr4.clear.lexer.dfa.cache |
清除词法分析器的 DFA 缓存。词法分析器的 DFA 缓存总是很小且对解析性能很重要,因此强烈建议保持原样,直到真正发生 OutOfMemoryError。 |
|
3.0.9+ |
-Dgroovy.antlr4.clear.lexer.dfa.cache=true |
3.8. 处理 JSON
Groovy 内置了在 Groovy 对象和 JSON 之间进行转换的支持。用于 JSON 序列化和解析的类位于 groovy.json
包中。
3.8.1. JsonSlurper
JsonSlurper
是一个类,它将 JSON 文本或读取器内容解析为 Groovy 数据结构(对象),如映射、列表以及 Integer
、Double
、Boolean
和 String
等基本类型。
该类带有一系列重载的 parse
方法以及一些特殊方法,例如 parseText
、parseFile
等。对于下一个示例,我们将使用 parseText
方法。它解析一个 JSON String
并将其递归转换为对象列表或映射。其他 parse*
方法类似,它们返回一个 JSON String
,但适用于不同的参数类型。
def jsonSlurper = new JsonSlurper()
def object = jsonSlurper.parseText('{ "name": "John Doe" } /* some comment */')
assert object instanceof Map
assert object.name == 'John Doe'
请注意,结果是一个普通映射,可以像普通的 Groovy 对象实例一样处理。JsonSlurper
解析的 JSON 符合 ECMA-404 JSON 交换标准,并支持 JavaScript 注释和日期。
除了映射之外,JsonSlurper
还支持 JSON 数组,这些数组会被转换为列表。
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 标准支持以下基本数据类型:字符串、数字、对象、true
、false
和 null
。JsonSlurper
将这些 JSON 类型转换为相应的 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 对象实例,而没有任何特殊的 JSON 类在后台,因此其使用是透明的。事实上,JsonSlurper
结果符合 GPath 表达式。GPath 是一种强大的表达式语言,受多种不同数据格式解析器支持(例如用于 XML 的 XmlSlurper
)。
有关更多详细信息,请参阅 GPath 表达式 一节。 |
下表概述了 JSON 类型和相应的 Groovy 数据类型
JSON | Groovy |
---|---|
字符串 |
|
数字 |
|
对象 |
|
数组 |
|
true |
|
false |
|
null |
|
日期 |
|
无论 JSON 中的值何时为 null ,JsonSlurper 都会将其补充为 Groovy null 值。这与将 null 值表示为库提供的单例对象的其他 JSON 解析器不同。 |
解析器变体
JsonSlurper
附带了几个解析器实现。每个解析器都适用于不同的要求,很可能在某些情况下,JsonSlurper
默认解析器并不是所有情况的最佳选择。以下是随附解析器实现的概述:
-
JsonParserCharArray
解析器基本接受一个 JSON 字符串并在底层字符数组上操作。在值转换期间,它会复制字符子数组(一种称为“切片”的机制)并在其上操作。 -
JsonFastParser
是JsonParserCharArray
的一个特殊变体,也是最快的解析器。然而,它不是默认解析器是有原因的。JsonFastParser
是一种所谓的索引覆盖解析器。在解析给定的 JSONString
时,它会尽可能地避免创建新的字符数组或String
实例。它只保留对底层原始字符数组的指针。此外,它会尽可能晚地推迟对象创建。如果解析的映射被放入长期缓存,则必须小心,因为映射对象可能尚未创建,并且仍然只包含指向原始字符缓冲区的指针。然而,JsonFastParser
附带一个特殊的切片模式,该模式会及早将字符缓冲区切碎,以保留原始缓冲区的小副本。建议将JsonFastParser
用于 2MB 以下的 JSON 缓冲区,并记住长期缓存限制。 -
JsonParserLax
是JsonParserCharArray
解析器的一个特殊变体。它具有与JsonFastParser
相似的性能特征,但不同之处在于它不完全依赖于 ECMA-404 JSON 语法。例如,它允许注释、无引号字符串等。 -
JsonParserUsingCharacterSource
是一个用于超大文件的特殊解析器。它使用一种称为“字符窗口”的技术,以恒定的性能特征解析大型 JSON 文件(此处的大型指超过 2MB 大小的文件)。
JsonSlurper
的默认解析器实现是 JsonParserCharArray
。JsonParserType
枚举包含上述解析器实现的常量:
实现 | 常量 |
---|---|
|
|
|
|
|
|
|
|
更改解析器实现非常简单,只需调用 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]
3.8.2. JsonOutput
JsonOutput
负责将 Groovy 对象序列化为 JSON 字符串。它可以被视为 JsonSlurper(一个 JSON 解析器)的伴随对象。
JsonOutput
带有重载的静态 toJson
方法。每个 toJson
实现都接受不同的参数类型。静态方法可以直接使用,也可以通过静态导入语句导入。
toJson
调用的结果是一个包含 JSON 代码的 String
。
def json = JsonOutput.toJson([name: 'John Doe', age: 42])
assert json == '{"name":"John Doe","age":42}'
JsonOutput
不仅支持将基本类型、映射或列表数据类型序列化为 JSON,它甚至还支持序列化 POGO(即普通 Groovy 对象)。
class Person { String name }
def json = JsonOutput.toJson([ new Person(name: 'John'), new Person(name: 'Max') ])
assert json == '[{"name":"John"},{"name":"Max"}]'
自定义输出
如果您需要控制序列化输出,可以使用 JsonGenerator
。JsonGenerator.Options
构建器可用于创建自定义生成器。可以在此构建器上设置一个或多个选项,以更改最终输出。设置完选项后,只需调用 build()
方法即可获得一个完全配置的实例,该实例将根据所选选项生成输出。
class Person {
String name
String title
int age
String password
Date dob
URL favoriteUrl
}
Person person = new Person(name: 'John', title: null, age: 21, password: 'secret',
dob: Date.parse('yyyy-MM-dd', '1984-12-15'),
favoriteUrl: new URL('https://groovy-lang.cn/'))
def generator = new JsonGenerator.Options()
.excludeNulls()
.dateFormat('yyyy@MM')
.excludeFieldsByName('age', 'password')
.excludeFieldsByType(URL)
.build()
assert generator.toJson(person) == '{"name":"John","dob":"1984@12"}'
闭包可用于转换类型。这些闭包转换器针对给定类型注册,并在遇到该类型或子类型时调用。闭包的第一个参数是与注册转换器的类型匹配的对象,此参数是必需的。闭包可以接受可选的第二个 String
参数,如果有可用的键名,则会设置此参数。
class Person {
String name
URL favoriteUrl
}
Person person = new Person(name: 'John', favoriteUrl: new URL('https://groovy-lang.cn/json.html#_jsonoutput'))
def generator = new JsonGenerator.Options()
.addConverter(URL) { URL u, String key ->
if (key == 'favoriteUrl') {
u.getHost()
} else {
u
}
}
.build()
assert generator.toJson(person) == '{"name":"John","favoriteUrl":"groovy-lang.org"}'
// No key available when generating a JSON Array
def list = [new URL('https://groovy-lang.cn/json.html#_jsonoutput')]
assert generator.toJson(list) == '["https://groovy-lang.cn/json.html#_jsonoutput"]'
// First parameter to the converter must match the type for which it is registered
shouldFail(IllegalArgumentException) {
new JsonGenerator.Options()
.addConverter(Date) { Calendar cal -> }
}
格式化输出
正如我们在前面的示例中看到的,JSON 输出默认不会进行漂亮打印。然而,JsonOutput
中的 prettyPrint
方法可以解决此任务。
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
接受一个 String
作为单个参数;因此,它可以应用于任意 JSON String
实例,而不仅仅是 JsonOutput.toJson
的结果。
构建器
另一种从 Groovy 创建 JSON 的方法是使用 JsonBuilder
或 StreamingJsonBuilder
。这两个构建器都提供了一个 DSL,允许构建一个对象图,然后将其转换为 JSON。
有关构建器的更多详细信息,请参阅构建器章节,其中涵盖了 JsonBuilder 和 StreamingJsonBuilder。 |
3.9. 与 SQL 数据库交互
Groovy 的 groovy-sql
模块提供了比 Java 的 JDBC 技术更高层次的抽象。JDBC 本身提供了一个较低层次但相当全面的 API,它为各种支持的关系数据库系统提供统一访问。我们在这里的示例中将使用 HSQLDB,但您也可以使用 Oracle、SQL Server、MySQL 和许多其他数据库。groovy-sql
模块中最常用的类是 groovy.sql.Sql
类,它将 JDBC 抽象提升了一个层次。我们将首先介绍它。
3.9.1. 连接到数据库
使用 Groovy 的 Sql
类连接到数据库需要四部分信息:
-
数据库统一资源定位符(URL)
-
用户名
-
密码
-
驱动程序类名(在某些情况下可以自动推导)
对于我们的 HSQLDB 数据库,值将类似于下表所示:
属性 | 值 |
---|---|
url |
|
用户 |
sa (或您的用户名) |
密码 |
yourPassword |
驱动 |
|
查阅您计划使用的 JDBC 驱动程序的文档,以确定适用于您情况的正确值。
Sql
类有一个 newInstance
工厂方法,它接受这些参数。您通常会这样使用它:
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()
如果您不想自己处理资源(即手动调用 close()
),那么您可以使用 withInstance
变体,如下所示:
withInstance
变体)Sql.withInstance(url, user, password, driver) { sql ->
// use 'sql' instance ...
}
使用 DataSource 连接
通常更倾向于使用 DataSource。您可能有一个来自连接池的 DataSource。这里我们将使用 HSQLDB 驱动 jar 中提供的那个,如下所示:
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 ...
如果您有自己的连接池,详细信息会有所不同,例如对于 Apache Commons DBCP:
@Grab('org.apache.commons:commons-dbcp2:2.7.0')
import groovy.sql.Sql
import org.apache.commons.dbcp2.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 ...
使用 @Grab 连接
前面的示例假设所需的数据库驱动程序 jar 已经位于您的类路径中。对于一个独立的脚本,您可以在脚本顶部添加 @Grab
语句,以自动下载所需的 jar,如下所示:
<<<<<<< HEAD
@Grab('org.hsqldb:hsqldb:2.7.2:jdk8')
=======
@Grab('org.hsqldb:hsqldb:2.7.3')
>>>>>>> 35be169b6c (GROOVY-11418: Bump hsqldb to 2.7.3 (test dependency))
@GrabConfig(systemClassLoader=true)
// create, use, and then close sql instance ...
@GrabConfig
语句是必要的,以确保使用系统类加载器。这确保了驱动程序类和系统类(如 java.sql.DriverManager
)位于相同的类加载器中。
3.9.2. 执行 SQL
您可以使用 execute()
方法执行任意 SQL 命令。让我们看看如何使用它来创建表。
创建表
执行 SQL 最简单的方法是调用 execute()
方法,并将您希望执行的 SQL 作为字符串传递,如下所示:
// ... create 'sql' instance
sql.execute '''
CREATE TABLE Author (
id INTEGER GENERATED BY DEFAULT AS IDENTITY,
firstname VARCHAR(64),
lastname VARCHAR(64)
);
'''
// close 'sql' instance ...
该方法有一个接受 GString 的变体,以及另一个接受参数列表的变体。还有其他名称相似的变体:executeInsert
和 executeUpdate
。我们将在本节的其他示例中看到这些变体的示例。
3.9.3. 基本 CRUD 操作
数据库的基本操作是创建、读取、更新和删除(即所谓的 CRUD 操作)。我们将依次检查这些操作。
创建/插入数据
您可以使用我们前面看到的相同的 execute()
语句,但通过使用 SQL 插入语句来插入行,如下所示:
sql.execute "INSERT INTO Author (firstname, lastname) VALUES ('Dierk', 'Koenig')"
您可以使用特殊的 executeInsert
方法而不是 execute
。这将返回所有生成的键的列表。execute
和 executeInsert
方法都允许您在 SQL 字符串中放置“?”占位符并提供参数列表。在这种情况下,使用 PreparedStatement,可以避免任何 SQL 注入的风险。以下示例说明了使用占位符和参数的 executeInsert
。
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
允许您提供一个键列名列表。这是一个片段,说明了键名规范和 GString。
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
和 rows
。
如果要迭代底层 JDBC API 返回的 ResultSet
,请使用 query
方法,如下所示:
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"
}
}
如果您想要一个稍微高层次的抽象,为 ResultSet
提供 Groovy 友好的类似映射的抽象,请使用 eachRow
方法,如下所示:
eachRow
读取数据rowNum = 0
sql.eachRow('SELECT firstname, lastname FROM Author') { row ->
def first = row[0]
def last = row.lastname
assert expected[rowNum++] == "$first $last"
}
请注意,在访问数据行时,您可以使用 Groovy 列表样式和映射样式表示法。
如果需要与 eachRow
类似的功能,但只返回一行数据,请使用 firstRow
方法,如下所示:
firstRow
读取数据def first = sql.firstRow('SELECT lastname, firstname FROM Author')
assert first.values().sort().join(',') == 'Dierk,Koenig'
如果要处理映射状数据结构的列表,请使用 rows
方法,如下所示:
rows
读取数据List authors = sql.rows('SELECT firstname, lastname FROM Author')
assert authors.size() == 3
assert authors.collect { "$it.FIRSTNAME ${it[-1]}" } == expected
请注意,映射状抽象具有不区分大小写的键(因此我们可以使用“FIRSTNAME”或“firstname”作为键),并且当使用索引值(从右侧计算列号)时,负索引(Groovy 标准特性)也有效。
您还可以使用上述任何方法返回标量值,尽管在这种情况下通常只需要 firstRow
。以下示例显示了返回行数的示例:
assert sql.firstRow('SELECT COUNT(*) AS num FROM Author').num == 3
更新行
更新行同样可以使用 execute()
方法完成。只需将 SQL 更新语句作为方法的参数。您可以只插入一个只有姓的作者,然后更新该行使其也具有名,如下所示:
sql.execute "INSERT INTO Author (lastname) VALUES ('Thorvaldsson')"
sql.execute "UPDATE Author SET firstname='Erik' where lastname='Thorvaldsson'"
还有一个特殊的 executeUpdate
变体,它返回执行 SQL 后更新的行数。例如,您可以按如下方式更改作者的姓氏:
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
方法也用于删除行。
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
3.9.4. 高级 SQL 操作
处理事务
在事务中执行数据库操作最简单的方法是将数据库操作包含在 withTransaction
闭包中,如以下示例所示:
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
这里,数据库最初是空的,操作成功完成后有两行。在事务范围之外,数据库从未被视为只有一行。
如果出现问题,withTransaction
块中的所有早期操作都将回滚。我们可以在以下示例中看到这一点,其中我们使用数据库元数据(稍后将详细介绍)查找 firstname
列的最大允许大小,然后尝试输入比该最大值大一的值,如下所示:
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
即使第一个 SQL 执行最初成功,它也会被回滚,并且行数将保持不变。
使用批处理
在处理大量数据时,特别是在插入此类数据时,将数据分块处理效率更高。这可以通过使用 withBatch
语句来完成,如下例所示:
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 条新记录。实际上,它们将以批处理方式添加,尽管之后您无法轻易分辨。如果您想确认幕后发生了什么,可以在程序中添加一些额外的日志记录。在 withBatch
语句之前添加以下行:
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
启用此额外日志记录,并按照 logging.properties 文件的上述注释进行更改后,您应该会看到如下输出:
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 注入,我们鼓励您使用预处理语句,这可以通过使用接受 GString 或额外参数列表的方法变体来实现。预处理语句可以与批处理结合使用,如下例所示:
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')
}
如果数据可能来自用户,例如通过脚本或网页表单,这提供了一个更安全的选项。当然,鉴于正在使用预处理语句,您只能对单个表执行相同的 SQL 操作(在我们的示例中是插入)的批处理。
执行分页
在向用户呈现大型数据表时,通常方便一页一页地呈现信息。Groovy 的许多 SQL 检索方法都具有额外的参数,可用于选择特定感兴趣的页面。起始位置和页面大小指定为整数,如下例使用 rows
所示:
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 元数据可以通过多种方式检索。最基本的方法可能从任何行中提取元数据,如以下示例所示,该示例检查表名、列名和列类型名。
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']
}
上一个示例的另一个略微变体,这次也查看列标签:
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'
}
访问元数据很常见,因此 Groovy 还提供了许多方法的变体,让您提供一个闭包,除了为每行调用的正常行闭包之外,该闭包还会随行元数据一起调用一次。以下示例说明了 eachRow
的两个闭包变体。
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 还提供了每个连接的元数据(不仅仅是行)。您也可以从 Groovy 访问此类元数据,如下例所示:
def md = sql.connection.metaData
assert md.driverName == 'HSQL Database Engine Driver'
assert md.databaseProductVersion == '2.7.3'
assert ['JDBCMajorVersion', 'JDBCMinorVersion'].collect{ md[it] } == [4, 2]
assert md.stringFunctions.tokenize(',').contains('CONCAT')
def rs = md.getTables(null, null, 'AUTH%', null)
assert rs.next()
assert rs.getString('TABLE_NAME').startsWith('AUTHOR')
查阅驱动程序的 JavaDoc,了解可供您访问的元数据信息。
命名和命名序数参数
Groovy 支持一些额外的替代占位符语法变体。GString 变体通常优于这些替代方案,但这些替代方案对于 Java 集成目的以及有时在 GString 可能已经大量用作模板一部分的模板场景中很有用。命名参数变体与字符串加参数列表变体非常相似,但不是具有一系列 ?
占位符后跟参数列表,而是具有一个或多个形式为 :propName
或 ?.propName
的占位符以及单个映射、命名参数或领域对象作为参数。映射或领域对象应具有一个名为 propName
的属性,对应于每个提供的占位符。
这是一个使用冒号形式的示例:
sql.execute "INSERT INTO Author (firstname, lastname) VALUES (:first, :last)", first: 'Dierk', last: 'Koenig'
另一个使用问号形式的例子
sql.execute "INSERT INTO Author (firstname, lastname) VALUES (?.first, ?.last)", first: 'Jon', last: 'Skeet'
如果需要提供的信息分布在多个映射或领域对象中,您可以使用带附加序数索引的问号形式,如下所示:
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 数据库,我们可以创建一个存储函数,它返回表中所有作者的首字母缩写,如下所示:
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
)
"""
我们可以使用 SQL CALL
语句,通过 Groovy 的普通 SQL 检索方法调用函数。这是一个使用 eachRow
的示例。
def result = []
sql.eachRow('CALL SELECT_AUTHOR_INITIALS()') {
result << "$it.firstInitial$it.lastInitial"
}
assert result == ['DK', 'JS', 'GL']
这是创建另一个存储函数的代码,该函数接受姓氏作为参数:
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
"""
我们可以使用占位符语法来指定参数的位置,并注意特殊的占位符位置来指示结果。
def result = sql.firstRow("{? = call FULL_NAME(?)}", ['Koenig'])
assert result[0] == 'Dierk Koenig'
最后,这是一个带有输入和输出参数的存储过程:
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
"""
要使用 CONCAT_NAME
存储过程参数,我们使用一个特殊的 call
方法。任何输入参数都直接作为方法调用的参数提供。对于输出参数,必须指定结果类型,如下所示:
sql.call("{call CONCAT_NAME(?, ?, ?)}", [Sql.VARCHAR, 'Dierk', 'Koenig']) {
fullname -> assert fullname == 'Dierk Koenig'
}
sql.execute """
CREATE PROCEDURE CHECK_ID_POSITIVE_IN_OUT ( INOUT p_err VARCHAR(64), IN pparam INTEGER, OUT re VARCHAR(15))
BEGIN ATOMIC
IF pparam > 0 THEN
set p_err = p_err || '_OK';
set re = 'RET_OK';
ELSE
set p_err = p_err || '_ERROR';
set re = 'RET_ERROR';
END IF;
END;
"""
def scall = "{call CHECK_ID_POSITIVE_IN_OUT(?, ?, ?)}"
sql.call scall, [Sql.inout(Sql.VARCHAR("MESSAGE")), 1, Sql.VARCHAR], {
res, p_err -> assert res == 'MESSAGE_OK' && p_err == 'RET_OK'
}
3.9.5. 使用 DataSets
Groovy 提供了一个 groovy.sql.DataSet 类,它增强了 groovy.sql.Sql 类,提供了可以认为是迷你 ORM 功能。数据库的访问和查询使用 POGO 字段和操作符,而不是 JDBC 级别的 API 调用和 RDBMS 列名。
所以,不是像这样的查询:
def qry = """SELECT * FROM Author
WHERE (firstname > ?)
AND (lastname < ?)
ORDER BY lastname DESC"""
def params = ['Dierk', 'Pragt']
def result = sql.rows(qry, params)
assert result*.firstname == ['Eric', 'Guillaume', 'Paul']
您可以编写如下代码:
def authorDS = sql.dataSet('Author')
def result = authorDS.findAll{ it.firstname > 'Dierk' }
.findAll{ it.lastname < 'Pragt' }
.sort{ it.lastname }
.reverse()
assert result.rows()*.firstname == ['Eric', 'Guillaume', 'Paul']
这里我们有一个辅助的“领域”类:
class Author {
String firstname
String lastname
}
数据库访问和操作涉及创建或使用领域类实例。
3.10. 以 SQL 样式查询集合
Groovy 的 groovy-ginq
模块提供了对集合更高层次的抽象。它可以以类似 SQL 的方式对内存中的对象集合执行查询。此外,查询 XML、JSON、YAML 等也可以得到支持,因为它们可以被解析成集合。由于 GORM 和 jOOQ 足以支持查询数据库,我们将首先介绍集合。
3.10.1. GINQ 又名 Groovy 集成查询
GINQ 是一种用于以类似 SQL 语法进行查询的 DSL,它由以下结构组成:
GQ, i.e. abbreviation for GINQ
|__ from
| |__ <data_source_alias> in <data_source>
|__ [join/innerjoin/leftjoin/rightjoin/fulljoin/crossjoin]*
| |__ <data_source_alias> in <data_source>
| |__ on <condition> ((&& | ||) <condition>)* (NOTE: `crossjoin` does not need `on` clause)
|__ [where]
| |__ <condition> ((&& | ||) <condition>)*
|__ [groupby]
| |__ <expression> [as <alias>] (, <expression> [as <alias>])*
| |__ [having]
| |__ <condition> ((&& | ||) <condition>)*
|__ [orderby]
| |__ <expression> [in (asc|desc)] (, <expression> [in (asc|desc)])*
|__ [limit]
| |__ [<offset>,] <size>
|__ select
|__ <expression> [as <alias>] (, <expression> [as <alias>])*
[] 表示相关子句是可选的,* 表示零次或多次,+ 表示一次或多次。此外,GINQ 的子句是顺序敏感的,因此子句的顺序应保持如上结构。 |
如我们所见,最简单的 GINQ 由一个 from
子句和一个 select
子句组成,看起来像这样:
from n in [0, 1, 2]
select n
GINQ 中只需要一个 from 子句。此外,GINQ 通过 from 和相关的 join 支持多个数据源。 |
作为一种 DSL,GINQ 应该被以下代码块包裹才能执行:
GQ { /* GINQ CODE */ }
例如,
def numbers = [0, 1, 2]
assert [0, 1, 2] == GQ {
from n in numbers
select n
}.toList()
import java.util.stream.Collectors
def numbers = [0, 1, 2]
assert '0#1#2' == GQ {
from n in numbers
select n
}.stream()
.map(e -> String.valueOf(e))
.collect(Collectors.joining('#'))
强烈建议使用 def
来定义 GINQ 执行结果的变量,该变量是一个惰性 Queryable
实例。
def result = GQ {
/* GINQ CODE */
}
def stream = result.stream() // get the stream from GINQ result
def list = result.toList() // get the list from GINQ result
目前 GINQ 在启用 STC 时无法正常工作。 |
此外,GINQ 也可以编写在标有 @GQ
的方法中。
@GQ
def someGinqMethod() {
/* GINQ CODE */
}
例如,
-
使用
@GQ
注解将ginq
方法标记为 GINQ 方法。
@groovy.ginq.transform.GQ
def ginq(list, b, e) {
from n in list
where b < n && n < e
select n
}
assert [3, 4] == ginq([1, 2, 3, 4, 5, 6], 2, 5).toList()
-
将结果类型指定为
List
。
import groovy.ginq.transform.GQ
@GQ(List)
def ginq(b, e) {
from n in [1, 2, 3, 4, 5, 6]
where b < n && n < e
select n
}
assert [3, 4] == ginq(2, 5)
GINQ 支持多种结果类型,例如 List 、Set 、Collection 、Iterable 、Iterator 、java.util.stream.Stream 和数组类型。 |
-
启用并行查询
import groovy.ginq.transform.GQ
@GQ(parallel=true)
def ginq(x) {
from n in [1, 2, 3]
where n < x
select n
}
assert [1] == ginq(2).toList()
GINQ 语法
数据源
GINQ 的数据源可以通过 from
子句指定,这相当于 SQL 的 FROM
。目前 GINQ 支持 Iterable
、Stream
、数组和 GINQ 结果集作为其数据源。
Iterable
数据源from n in [1, 2, 3] select n
Stream
数据源from n in [1, 2, 3].stream() select n
from n in new int[] {1, 2, 3} select n
def vt = GQ {from m in [1, 2, 3] select m}
assert [1, 2, 3] == GQ {
from n in vt select n
}.toList()
投影
列名可以使用 as
子句重命名。
def result = GQ {
from n in [1, 2, 3]
select Math.pow(n, 2) as powerOfN
}
assert [[1, 1], [4, 4], [9, 9]] == result.stream().map(r -> [r[0], r.powerOfN]).toList()
重命名后的列可以通过其新名称引用,例如 r.powerOfN 。此外,它还可以通过其索引引用,例如 r[0] 。 |
assert [[1, 1], [2, 4], [3, 9]] == GQ {
from v in (
from n in [1, 2, 3]
select n, Math.pow(n, 2) as powerOfN
)
select v.n, v.powerOfN
}.toList()
select P1, P2, …, Pn 是 select new NamedRecord(P1, P2, …, Pn) 的简化语法,当且仅当 n >= 2 时。此外,如果使用 as 子句,将创建 NamedRecord 实例。存储在 NamedRecord 中的值可以通过其名称引用。 |
构造新对象作为列值
@groovy.transform.EqualsAndHashCode
class Person {
String name
Person(String name) {
this.name = name
}
}
def persons = [new Person('Daniel'), new Person('Paul'), new Person('Eric')]
assert persons == GQ {
from n in ['Daniel', 'Paul', 'Eric']
select new Person(n)
}.toList()
distinct
等同于 SQL 的 DISTINCT
def result = GQ {
from n in [1, 2, 2, 3, 3, 3]
select distinct(n)
}
assert [1, 2, 3] == result.toList()
def result = GQ {
from n in [1, 2, 2, 3, 3, 3]
select distinct(n, n + 1)
}
assert [[1, 2], [2, 3], [3, 4]] == result.toList()
过滤
where
等同于 SQL 的 WHERE
from n in [0, 1, 2, 3, 4, 5]
where n > 0 && n <= 3
select n * 2
from n in [0, 1, 2]
where n in [1, 2]
select n
from n in [0, 1, 2]
where n in (
from m in [1, 2]
select m
)
select n
import static groovy.lang.Tuple.tuple
assert [0, 1] == GQ {
from n in [0, 1, 2]
where tuple(n, n + 1) in (
from m in [1, 2]
select m - 1, m
)
select n
}.toList()
from n in [0, 1, 2]
where n !in [1, 2]
select n
from n in [0, 1, 2]
where n !in (
from m in [1, 2]
select m
)
select n
import static groovy.lang.Tuple.tuple
assert [2] == GQ {
from n in [0, 1, 2]
where tuple(n, n + 1) !in (
from m in [1, 2]
select m - 1, m
)
select n
}.toList()
from n in [1, 2, 3]
where (
from m in [2, 3]
where m == n
select m
).exists()
select n
from n in [1, 2, 3]
where !(
from m in [2, 3]
where m == n
select m
).exists()
select n
连接
GINQ 的更多数据源可以通过 join 子句指定。
from n1 in [1, 2, 3]
join n2 in [1, 3] on n1 == n2
select n1, n2
join 优于 innerjoin 和 innerhashjoin ,因为它具有更好的可读性,并且它足够智能,可以根据其 on 子句选择正确的具体连接(即 innerjoin 或 innerhashjoin )。 |
from n1 in [1, 2, 3]
innerjoin n2 in [1, 3] on n1 == n2
select n1, n2
from n1 in [1, 2, 3]
leftjoin n2 in [2, 3, 4] on n1 == n2
select n1, n2
from n1 in [2, 3, 4]
rightjoin n2 in [1, 2, 3] on n1 == n2
select n1, n2
from n1 in [1, 2, 3]
fulljoin n2 in [2, 3, 4] on n1 == n2
select n1, n2
from n1 in [1, 2, 3]
crossjoin n2 in [3, 4, 5]
select n1, n2
当数据源包含大量对象时,哈希连接特别高效。
from n1 in [1, 2, 3]
innerhashjoin n2 in [1, 3] on n1 == n2
select n1, n2
from n1 in [1, 2, 3]
lefthashjoin n2 in [2, 3, 4] on n1 == n2
select n1, n2
from n1 in [2, 3, 4]
righthashjoin n2 in [1, 2, 3] on n1 == n2
select n1, n2
from n1 in [1, 2, 3]
fullhashjoin n2 in [2, 3, 4] on n1 == n2
select n1, n2
哈希连接的 on 子句中只允许二元表达式(== ,&& )。 |
分组
groupby
等同于 SQL 的 GROUP BY
,而 having
等同于 SQL 的 HAVING
。
from n in [1, 1, 3, 3, 6, 6, 6]
groupby n
select n, count(n)
from n in [1, 1, 3, 3, 6, 6, 6]
groupby n
having n >= 3
select n, count(n)
from n in [1, 1, 3, 3, 6, 6, 6]
groupby n
having count() < 3
select n, count()
分组列可以使用 as
子句重命名。
from s in ['ab', 'ac', 'bd', 'acd', 'bcd', 'bef']
groupby s.size() as length, s[0] as firstChar
select length, firstChar, max(s)
from s in ['ab', 'ac', 'bd', 'acd', 'bcd', 'bef']
groupby s.size() as length, s[0] as firstChar
having length == 3 && firstChar == 'b'
select length, firstChar, max(s)
GINQ 提供了一些内置聚合函数
函数 | 参数类型 | 返回类型 | 描述 |
---|---|---|---|
count() |
java.lang.Long |
行数,类似于 SQL 中的 |
|
count(表达式) |
任意 |
java.lang.Long |
表达式 值不为 |
min(表达式) |
java.lang.Comparable |
与参数类型相同 |
所有非空值的表达式最小值 |
max(表达式) |
java.lang.Comparable |
与参数类型相同 |
所有非空值的表达式最大值 |
sum(表达式) |
java.lang.Number |
java.math.BigDecimal |
所有非空值的表达式之和 |
avg(表达式) |
java.lang.Number |
java.math.BigDecimal |
所有非空值的平均值(算术平均值) |
list(表达式) |
任意 |
java.util.List |
所有非空值的聚合列表 |
median(表达式) |
java.lang.Number |
java.math.BigDecimal |
值,使得其上方和下方的非空值数量相同(“中间”值,不一定与平均值相同) |
stdev(表达式) |
java.lang.Number |
java.math.BigDecimal |
所有非空值的统计标准差 |
stdevp(表达式) |
java.lang.Number |
java.math.BigDecimal |
所有非空值的总体统计标准差 |
var(表达式) |
java.lang.Number |
java.math.BigDecimal |
所有非空值的统计方差 |
varp(表达式) |
java.lang.Number |
java.math.BigDecimal |
所有非空值的总体统计方差 |
agg(表达式) |
任意 |
任意 |
自定义表达式 中的聚合逻辑并返回单个值 |
from n in [1, 1, 3, 3, 6, 6, 6]
groupby n
select n, count()
from s in ['a', 'b', 'cd', 'ef']
groupby s.size() as length
select length, min(s)
from s in ['a', 'b', 'cd', 'ef']
groupby s.size() as length
select length, max(s)
from n in [1, 1, 3, 3, 6, 6, 6]
groupby n
select n, sum(n)
from n in [1, 1, 3, 3, 6, 6, 6]
groupby n
select n, avg(n)
from n in [1, 1, 3, 3, 6, 6, 6]
groupby n
select n, median(n)
assert [['A', ['APPLE', 'APRICOT']],
['B', ['BANANA']],
['C', ['CANTALOUPE']]] == GQL {
from fruit in ['Apple', 'Apricot', 'Banana', 'Cantaloupe']
groupby fruit[0] as firstChar
select firstChar, list(fruit.toUpperCase()) as fruit_list
}
def persons = [new Person('Linda', 100, 'Female'),
new Person('Daniel', 135, 'Male'),
new Person('David', 122, 'Male')]
assert [['Male', ['Daniel', 'David']], ['Female', ['Linda']]] == GQL {
from p in persons
groupby p.gender
select p.gender, list(p.name)
}
from n in [1, 1, 3, 3, 6, 6, 6]
groupby n
select n, agg(_g.stream().map(r -> r.n).reduce(BigDecimal.ZERO, BigDecimal::add))
_g 是 agg 聚合函数的隐式变量,它表示分组的 Queryable 对象,其记录(例如 r )可以通过别名(例如 n )引用数据源 |
from fruit in ['Apple', 'Apricot', 'Banana', 'Cantaloupe']
groupby fruit.substring(0, 1) as firstChar
select firstChar, agg(_g.stream().map(r -> r.fruit).toList()) as fruit_list
此外,我们还可以将聚合函数应用于整个 GINQ 结果,即无需 groupby
子句
assert [3] == GQ {
from n in [1, 2, 3]
select max(n)
}.toList()
assert [[1, 3, 2, 2, 6, 3, 3, 6]] == GQ {
from n in [1, 2, 3]
select min(n), max(n), avg(n), median(n), sum(n), count(n), count(),
agg(_g.stream().map(r -> r.n).reduce(BigDecimal.ZERO, BigDecimal::add))
}.toList()
assert [0.816496580927726] == GQ {
from n in [1, 2, 3]
select stdev(n)
}.toList()
assert [1] == GQ {
from n in [1, 2, 3]
select stdevp(n)
}.toList()
assert [0.6666666666666667] == GQ {
from n in [1, 2, 3]
select var(n)
}.toList()
assert [1] == GQ {
from n in [1, 2, 3]
select varp(n)
}.toList()
排序
orderby
等效于 SQL 的 ORDER BY
from n in [1, 5, 2, 6]
orderby n
select n
按升序排序时,in asc 是可选的 |
from n in [1, 5, 2, 6]
orderby n in asc
select n
from n in [1, 5, 2, 6]
orderby n in desc
select n
from s in ['a', 'b', 'ef', 'cd']
orderby s.length() in desc, s in asc
select s
from s in ['a', 'b', 'ef', 'cd']
orderby s.length() in desc, s
select s
from n in [1, null, 5, null, 2, 6]
orderby n in asc(nullslast)
select n
nullslast 等效于 SQL 的 NULLS LAST 并默认应用。nullsfirst 等效于 SQL 的 NULLS FIRST 。 |
from n in [1, null, 5, null, 2, 6]
orderby n in asc(nullsfirst)
select n
from n in [1, null, 5, null, 2, 6]
orderby n in desc(nullslast)
select n
from n in [1, null, 5, null, 2, 6]
orderby n in desc(nullsfirst)
select n
分页
limit
类似于 MySQL 的 limit
子句,可以指定分页的 offset
(第一个参数)和 size
(第二个参数),或者只将唯一的参数指定为 size
from n in [1, 2, 3, 4, 5]
limit 3
select n
from n in [1, 2, 3, 4, 5]
limit 1, 3
select n
嵌套 GINQ
from
子句中的嵌套 GINQfrom v in (
from n in [1, 2, 3]
select n
)
select v
where
子句中的嵌套 GINQfrom n in [0, 1, 2]
where n in (
from m in [1, 2]
select m
)
select n
from n in [0, 1, 2]
where (
from m in [1, 2]
where m == n
select m
).exists()
select n
select
子句中的嵌套 GINQassert [null, 2, 3] == GQ {
from n in [1, 2, 3]
select (
from m in [2, 3, 4]
where m == n
limit 1
select m
)
}.toList()
建议使用 limit 1 限制子查询结果的数量,因为如果返回多个值,将抛出 TooManyValuesException |
我们可以使用 as
子句命名子查询结果
assert [[1, null], [2, 2], [3, 3]] == GQ {
from n in [1, 2, 3]
select n, (
from m in [2, 3, 4]
where m == n
select m
) as sqr
}.toList()
窗口函数
窗口可以通过 partitionby
、orderby
、rows
和 range
定义
over(
[partitionby <expression> (, <expression>)*]
[orderby <expression> (, <expression>)*
[rows <lower>, <upper> | range <lower>, <upper>]]
)
-
0
用作rows
和range
子句的边界,等效于 SQL 的CURRENT ROW
,负数表示PRECEDING
,正数表示FOLLOWING
-
null
用作rows
和range
子句的下限,等效于 SQL 的UNBOUNDED PRECEDING
-
null
用作rows
和range
子句的上限,等效于 SQL 的UNBOUNDED FOLLOWING
此外,GINQ 还提供了一些内置的窗口函数
函数 | 参数类型 | 返回类型 | 描述 |
---|---|---|---|
rowNumber() |
java.lang.Long |
当前行在其分区中的行号,从 |
|
rank() |
java.lang.Long |
当前行的排名,有间隙 |
|
denseRank() |
java.lang.Long |
当前行的排名,无间隙 |
|
percentRank() |
java.math.BigDecimal |
当前行的相对排名:(排名 - 1)/(总行数 - 1) |
|
cumeDist() |
java.math.BigDecimal |
当前行的相对排名:(当前行之前或与当前行相同等级的行数)/(总行数) |
|
ntile(表达式) |
java.lang.Long |
java.lang.Long |
存储桶索引范围从 |
lead(表达式 [, offset [, default]]) |
任意 [, java.lang.Long [, 与 expression 类型相同]] |
与 表达式 类型相同 |
返回在分区内当前行之后 offset 行处评估的 表达式;如果没有此类行,则返回 default(其类型必须与 表达式 相同)。offset 和 default 都相对于当前行进行评估。如果省略,offset 默认为 |
lag(表达式 [, offset [, default]]) |
任意 [, java.lang.Long [, 与 expression 类型相同]] |
与 表达式 类型相同 |
返回在分区内当前行之前 offset 行处评估的 表达式;如果没有此类行,则返回 default(其类型必须与 表达式 相同)。offset 和 default 都相对于当前行进行评估。如果省略,offset 默认为 |
firstValue(表达式) |
任意 |
与 表达式 类型相同 |
返回在窗口帧第一行处评估的 表达式 |
lastValue(表达式) |
任意 |
与 表达式 类型相同 |
返回在窗口帧最后一行处评估的 表达式 |
nthValue(表达式, n) |
任意, java.lang.Long |
与 表达式 类型相同 |
返回在窗口帧的第 n 行处评估的 表达式 |
count() |
java.lang.Long |
行数,类似于 SQL 中的 |
|
count(表达式) |
任意 |
java.lang.Long |
表达式 值不为 |
min(表达式) |
java.lang.Comparable |
与参数类型相同 |
所有非空值的表达式最小值 |
max(表达式) |
java.lang.Comparable |
与参数类型相同 |
所有非空值的表达式最大值 |
sum(表达式) |
java.lang.Number |
java.math.BigDecimal |
所有非空值的表达式之和 |
avg(表达式) |
java.lang.Number |
java.math.BigDecimal |
所有非空值的平均值(算术平均值) |
median(表达式) |
java.lang.Number |
java.math.BigDecimal |
值,使得其上方和下方的非空值数量相同(“中间”值,不一定与平均值相同) |
stdev(表达式) |
java.lang.Number |
java.math.BigDecimal |
所有非空值的统计标准差 |
stdevp(表达式) |
java.lang.Number |
java.math.BigDecimal |
所有非空值的总体统计标准差 |
var(表达式) |
java.lang.Number |
java.math.BigDecimal |
所有非空值的统计方差 |
varp(表达式) |
java.lang.Number |
java.math.BigDecimal |
所有非空值的总体统计方差 |
agg(表达式) |
任意 |
任意 |
孵化中:自定义 表达式 中的聚合逻辑并返回单个值 |
rowNumber
assert [[2, 1, 1, 1], [1, 0, 0, 2], [null, 3, 3, 3], [3, 2, 2, 0]] == GQ {
from n in [2, 1, null, 3]
select n, (rowNumber() over(orderby n)),
(rowNumber() over(orderby n in asc)),
(rowNumber() over(orderby n in desc))
}.toList()
assert [[1, 0, 1, 2, 3], [2, 1, 2, 1, 2], [null, 3, 0, 3, 0], [3, 2, 3, 0, 1]] == GQ {
from n in [1, 2, null, 3]
select n, (rowNumber() over(orderby n in asc(nullslast))),
(rowNumber() over(orderby n in asc(nullsfirst))),
(rowNumber() over(orderby n in desc(nullslast))),
(rowNumber() over(orderby n in desc(nullsfirst)))
}.toList()
窗口函数周围的括号是必需的。 |
rank
, denseRank
, percentRank
, cumeDist
和 ntile
assert [['a', 1, 1], ['b', 2, 2], ['b', 2, 2],
['c', 4, 3], ['c', 4, 3], ['d', 6, 4],
['e', 7, 5]] == GQ {
from s in ['a', 'b', 'b', 'c', 'c', 'd', 'e']
select s,
(rank() over(orderby s)),
(denseRank() over(orderby s))
}.toList()
assert [[60, 0, 0.4], [60, 0, 0.4], [80, 0.5, 0.8], [80, 0.5, 0.8], [100, 1, 1]] == GQ {
from n in [60, 60, 80, 80, 100]
select n,
(percentRank() over(orderby n)),
(cumeDist() over(orderby n))
}.toList()
assert [[1, 0], [2, 0], [3, 0],
[4, 1], [5, 1],
[6, 2], [7, 2],[8, 2],
[9, 3], [10, 3]] == GQ {
from n in 1..10
select n, (ntile(4) over(orderby n))
}.toList()
lead
和 lag
assert [[2, 3], [1, 2], [3, null]] == GQ {
from n in [2, 1, 3]
select n, (lead(n) over(orderby n))
}.toList()
assert [[2, 3], [1, 2], [3, null]] == GQ {
from n in [2, 1, 3]
select n, (lead(n) over(orderby n in asc))
}.toList()
assert [['a', 'bc'], ['ab', null], ['b', 'a'], ['bc', 'ab']] == GQ {
from s in ['a', 'ab', 'b', 'bc']
select s, (lead(s) over(orderby s.length(), s in desc))
}.toList()
assert [['a', null], ['ab', null], ['b', 'a'], ['bc', 'ab']] == GQ {
from s in ['a', 'ab', 'b', 'bc']
select s, (lead(s) over(partitionby s.length() orderby s.length(), s in desc))
}.toList()
assert [[2, 1], [1, null], [3, 2]] == GQ {
from n in [2, 1, 3]
select n, (lag(n) over(orderby n))
}.toList()
assert [[2, 3], [1, 2], [3, null]] == GQ {
from n in [2, 1, 3]
select n, (lag(n) over(orderby n in desc))
}.toList()
assert [['a', null], ['b', 'a'], ['aa', null], ['bb', 'aa']] == GQ {
from s in ['a', 'b', 'aa', 'bb']
select s, (lag(s) over(partitionby s.length() orderby s))
}.toList()
assert [[2, 3, 1], [1, 2, null], [3, null, 2]] == GQ {
from n in [2, 1, 3]
select n, (lead(n) over(orderby n)), (lag(n) over(orderby n))
}.toList()
除了默认偏移量 1
之外,还可以指定偏移量
assert [[2, null, null], [1, 3, null], [3, null, 1]] == GQ {
from n in [2, 1, 3]
select n, (lead(n, 2) over(orderby n)), (lag(n, 2) over(orderby n))
}.toList()
当偏移量指定的索引超出窗口时,可以返回默认值,例如 'NONE'
assert [[2, 'NONE', 'NONE'], [1, 3, 'NONE'], [3, 'NONE', 1]] == GQ {
from n in [2, 1, 3]
select n, (lead(n, 2, 'NONE') over(orderby n)), (lag(n, 2, 'NONE') over(orderby n))
}.toList()
firstValue
, lastValue
和 nthValue
assert [[2, 1], [1, 1], [3, 2]] == GQ {
from n in [2, 1, 3]
select n, (firstValue(n) over(orderby n rows -1, 1))
}.toList()
assert [[2, 3], [1, 2], [3, 3]] == GQ {
from n in [2, 1, 3]
select n, (lastValue(n) over(orderby n rows -1, 1))
}.toList()
assert [[2, 2], [1, 1], [3, 3]] == GQ {
from n in [2, 1, 3]
select n, (firstValue(n) over(orderby n rows 0, 1))
}.toList()
assert [[2, 1], [1, null], [3, 1]] == GQ {
from n in [2, 1, 3]
select n, (firstValue(n) over(orderby n rows -2, -1))
}.toList()
assert [[2, 1], [1, null], [3, 2]] == GQ {
from n in [2, 1, 3]
select n, (lastValue(n) over(orderby n rows -2, -1))
}.toList()
assert [[2, 3], [1, 3], [3, null]] == GQ {
from n in [2, 1, 3]
select n, (lastValue(n) over(orderby n rows 1, 2))
}.toList()
assert [[2, 3], [1, 2], [3, null]] == GQ {
from n in [2, 1, 3]
select n, (firstValue(n) over(orderby n rows 1, 2))
}.toList()
assert [[2, 2], [1, 1], [3, 3]] == GQ {
from n in [2, 1, 3]
select n, (lastValue(n) over(orderby n rows -1, 0))
}.toList()
assert [[2, 1], [1, 1], [3, 1]] == GQ {
from n in [2, 1, 3]
select n, (firstValue(n) over(orderby n rows null, 1))
}.toList()
assert [[2, 3], [1, 3], [3, 3]] == GQ {
from n in [2, 1, 3]
select n, (lastValue(n) over(orderby n rows -1, null))
}.toList()
assert [['a', 'a', 'b'], ['aa', 'aa', 'bb'], ['b', 'a', 'b'], ['bb', 'aa', 'bb']] == GQ {
from s in ['a', 'aa', 'b', 'bb']
select s, (firstValue(s) over(partitionby s.length() orderby s)),
(lastValue(s) over(partitionby s.length() orderby s))
}.toList()
assert [[1, 1, 2, 3, null], [2, 1, 2, 3, null], [3, 1, 2, 3, null]] == GQ {
from n in 1..3
select n, (nthValue(n, 0) over(orderby n)),
(nthValue(n, 1) over(orderby n)),
(nthValue(n, 2) over(orderby n)),
(nthValue(n, 3) over(orderby n))
}.toList()
min
, max
, count
, sum
, avg
, median
, stdev
, stdevp
, var
,varp
和 agg
assert [['a', 'a', 'b'], ['b', 'a', 'b'], ['aa', 'aa', 'bb'], ['bb', 'aa', 'bb']] == GQ {
from s in ['a', 'b', 'aa', 'bb']
select s, (min(s) over(partitionby s.length())), (max(s) over(partitionby s.length()))
}.toList()
assert [[1, 2, 2, 2, 1, 1], [1, 2, 2, 2, 1, 1],
[2, 2, 2, 4, 2, 2], [2, 2, 2, 4, 2, 2],
[3, 2, 2, 6, 3, 3], [3, 2, 2, 6, 3, 3]] == GQ {
from n in [1, 1, 2, 2, 3, 3]
select n, (count() over(partitionby n)),
(count(n) over(partitionby n)),
(sum(n) over(partitionby n)),
(avg(n) over(partitionby n)),
(median(n) over(partitionby n))
}.toList()
assert [[2, 6, 3, 1, 3, 4], [1, 6, 3, 1, 3, 4],
[3, 6, 3, 1, 3, 4], [null, 6, 3, 1, 3, 4]] == GQ {
from n in [2, 1, 3, null]
select n, (sum(n) over()),
(max(n) over()),
(min(n) over()),
(count(n) over()),
(count() over())
}.toList()
assert [[1, 1, 1], [2, 2, 3], [5, 2, 10], [5, 2, 10]] == GQ {
from n in [1, 2, 5, 5]
select n, (count() over(orderby n range -2, 0)),
(sum(n) over(orderby n range -2, 0))
}.toList()
assert [[1, 2, 3], [2, 1, 2], [5, 2, 10], [5, 2, 10]] == GQ {
from n in [1, 2, 5, 5]
select n, (count() over(orderby n range 0, 1)),
(sum(n) over(orderby n range 0, 1))
}.toList()
assert [[1, 2, 3], [2, 2, 3], [5, 2, 10], [5, 2, 10]] == GQ {
from n in [1, 2, 5, 5]
select n, (count() over(orderby n range -1, 1)),
(sum(n) over(orderby n range -1, 1))
}.toList()
assert [[1, 1, 2], [2, 0, 0], [5, 0, 0], [5, 0, 0]] == GQ {
from n in [1, 2, 5, 5]
select n, (count() over(orderby n in desc range 1, 2)),
(sum(n) over(orderby n in desc range 1, 2))
}.toList()
assert [[1, 0, 0], [2, 1, 1], [5, 0, 0], [5, 0, 0]] == GQ {
from n in [1, 2, 5, 5]
select n, (count() over(orderby n in desc range -2, -1)),
(sum(n) over(orderby n in desc range -2, -1))
}.toList()
assert [[1, 3, 12], [2, 2, 10], [5, 0, 0], [5, 0, 0]] == GQ {
from n in [1, 2, 5, 5]
select n, (count() over(orderby n range 1, null)),
(sum(n) over(orderby n range 1, null))
}.toList()
assert [[1, 2, 3], [2, 2, 3], [5, 4, 13], [5, 4, 13]] == GQ {
from n in [1, 2, 5, 5]
select n, (count() over(orderby n range null, 1)),
(sum(n) over(orderby n range null, 1))
}.toList()
assert [[1, 0.816496580927726],
[2, 0.816496580927726],
[3, 0.816496580927726]] == GQ {
from n in [1, 2, 3]
select n, (stdev(n) over())
}.toList()
assert [[1, 1], [2, 1], [3, 1]] == GQ {
from n in [1, 2, 3]
select n, (stdevp(n) over())
}.toList()
assert [[1, 0.6666666666666667],
[2, 0.6666666666666667],
[3, 0.6666666666666667]] == GQ {
from n in [1, 2, 3]
select n, (var(n) over())
}.toList()
assert [[1, 1], [2, 1], [3, 1]] == GQ {
from n in [1, 2, 3]
select n, (varp(n) over())
}.toList()
assert [[1, 4], [2, 2], [3, 4]] == GQ {
from n in [1, 2, 3]
select n,
(agg(_g.stream().map(r -> r.n).reduce(BigDecimal.ZERO, BigDecimal::add)) over(partitionby n % 2))
}.toList()
GINQ 提示
列表推导
列表推导是一种基于现有列表定义和创建列表的优雅方式
assert [4, 16, 36, 64, 100] == GQ {from n in 1..<11 where n % 2 == 0 select n ** 2}.toList()
assert [4, 16, 36, 64, 100] == GQ {from n in 1..<11 where n % 2 == 0 select n ** 2} as List
assert [4, 16, 36, 64, 100] == GQL {from n in 1..<11 where n % 2 == 0 select n ** 2}
GQL {…} 是 GQ {…}.toList() 的缩写 |
GINQ 可以直接用作循环中的列表推导
def result = []
for (def x : GQ {from n in 1..<11 where n % 2 == 0 select n ** 2}) {
result << x
}
assert [4, 16, 36, 64, 100] == result
查询与更新
这类似于 SQL 中的 update
语句
import groovy.transform.*
@TupleConstructor
@EqualsAndHashCode
@ToString
class Person {
String name
String nickname
}
def linda = new Person('Linda', null)
def david = new Person('David', null)
def persons = [new Person('Daniel', 'ShanFengXiaoZi'), linda, david]
def result = GQ {
from p in persons
where p.nickname == null
select p
}.stream()
.peek(p -> { p.nickname = 'Unknown' }) // update `nickname`
.toList()
def expected = [new Person('Linda', 'Unknown'), new Person('David', 'Unknown')]
assert expected == result
assert ['Unknown', 'Unknown'] == [linda, david]*.nickname // ensure the original objects are updated
with
子句的替代方案
GINQ 目前不支持 with
子句,但我们可以通过定义临时变量来解决
def v = GQ { from n in [1, 2, 3] where n < 3 select n }
def result = GQ {
from n in v
where n > 1
select n
}
assert [2] == result.toList()
case-when
的替代方案
SQL 的 case-when
可以替换为 switch 表达式
assert ['a', 'b', 'c', 'c'] == GQ {
from n in [1, 2, 3, 4]
select switch (n) {
case 1 -> 'a'
case 2 -> 'b'
default -> 'c'
}
}.toList()
查询 JSON
import groovy.json.JsonSlurper
def json = new JsonSlurper().parseText('''
{
"fruits": [
{"name": "Orange", "price": 11},
{"name": "Apple", "price": 6},
{"name": "Banana", "price": 4},
{"name": "Mongo", "price": 29},
{"name": "Durian", "price": 32}
]
}
''')
def expected = [['Mongo', 29], ['Orange', 11], ['Apple', 6], ['Banana', 4]]
assert expected == GQ {
from f in json.fruits
where f.price < 32
orderby f.price in desc
select f.name, f.price
}.toList()
并行查询
并行查询在查询大数据源时特别高效。它默认禁用,但我们可以手动启用它
assert [[1, 1], [2, 2], [3, 3]] == GQ(parallel: true) {
from n1 in 1..1000
join n2 in 1..10000 on n2 == n1
where n1 <= 3 && n2 <= 5
select n1, n2
}.toList()
由于并行查询将使用共享线程池,因此以下代码可以在所有 GINQ 语句执行完成后释放资源,并将等待直到所有线程任务完成。
GQ {
shutdown
}
一旦发出 shutdown ,并行查询将不再工作。 |
以下代码等同于上述代码,换句话说,immediate
是可选的
GQ {
shutdown immediate
}
不等待任务完成就关闭
GQ {
shutdown abort
}
自定义 GINQ
对于高级用户,您可以通过指定自己的目标代码生成器来定制 GINQ 行为。例如,我们可以指定合格的类名 org.apache.groovy.ginq.provider.collection.GinqAstWalker
作为目标代码生成器,为查询集合生成 GINQ 方法调用,这是 GINQ 的默认行为
assert [0, 1, 2] == GQ(astWalker: 'org.apache.groovy.ginq.provider.collection.GinqAstWalker') {
from n in [0, 1, 2]
select n
}.toList()
优化 GINQ
GINQ 优化器默认启用,以获得更好的性能。它将转换 GINQ AST 以实现更好的执行计划。我们可以手动禁用它
assert [[2, 2]] == GQ(optimize: false) {
from n1 in [1, 2, 3]
join n2 in [1, 2, 3] on n1 == n2
where n1 > 1 && n2 < 3
select n1, n2
}.toList()
GINQ 示例
生成乘法表
from v in (
from a in 1..9
join b in 1..9 on a <= b
select a as f, b as s, "$a * $b = ${a * b}".toString() as r
)
groupby v.s
select max(v.f == 1 ? v.r : '') as v1,
max(v.f == 2 ? v.r : '') as v2,
max(v.f == 3 ? v.r : '') as v3,
max(v.f == 4 ? v.r : '') as v4,
max(v.f == 5 ? v.r : '') as v5,
max(v.f == 6 ? v.r : '') as v6,
max(v.f == 7 ? v.r : '') as v7,
max(v.f == 8 ? v.r : '') as v8,
max(v.f == 9 ? v.r : '') as v9
3.11. 处理 XML
3.11.1. 解析 XML
XmlParser 和 XmlSlurper
使用 Groovy 解析 XML 最常用的方法是使用以下之一:
-
groovy.xml.XmlParser
-
groovy.xml.XmlSlurper
两者都采用相同的方法解析 XML。两者都附带了一堆重载的解析方法以及一些特殊方法,例如 parseText
、parseFile 等。在下一个示例中,我们将使用 parseText
方法。它解析 XML String
并将其递归转换为对象列表或映射。
def text = '''
<list>
<technology>
<name>Groovy</name>
</technology>
</list>
'''
def list = new XmlSlurper().parseText(text) (1)
assert list instanceof groovy.xml.slurpersupport.GPathResult (2)
assert list.technology.name == 'Groovy' (3)
1 | 解析 XML 并返回根节点作为 GPathResult |
2 | 检查我们正在使用 GPathResult |
3 | 以 GPath 样式遍历树 |
def text = '''
<list>
<technology>
<name>Groovy</name>
</technology>
</list>
'''
def list = new XmlParser().parseText(text) (1)
assert list instanceof groovy.util.Node (2)
assert list.technology.name.text() == 'Groovy' (3)
1 | 解析 XML 并返回根节点作为 Node |
2 | 检查我们正在使用 Node |
3 | 以 GPath 样式遍历树 |
首先,让我们看看 XMLParser
和 XMLSlurper
之间的相似之处
-
两者都基于
SAX
,因此它们的内存占用都很低 -
两者都可以更新/转换 XML
但它们有关键的差异
-
XmlSlurper
惰性评估结构。因此,如果您更新 XML,则必须再次评估整个树。 -
XmlSlurper
在解析 XML 时返回GPathResult
实例 -
XmlParser
在解析 XML 时返回Node
对象
何时使用其中一个?
在 StackOverflow 上有一个讨论。这里写出的结论部分基于此条目。 |
-
如果您想将现有文档转换为另一个文档,那么
XmlSlurper
将是您的选择 -
如果您想同时更新和读取,那么
XmlParser
是您的选择。
其背后的原理是,每次您使用 XmlSlurper
创建节点时,它都不会立即可用,直到您使用另一个 XmlSlurper
实例再次解析文档。如果只需要读取少量节点,那么 XmlSlurper
适合您。"
-
如果您只需要读取少量节点,那么
XmlSlurper
应该是您的选择,因为它不必在内存中创建完整的结构"
总的来说,这两个类执行方式相似。即使是使用 GPath 表达式的方式也相同(两者都使用 breadthFirst()
和 depthFirst()
表达式)。所以我想这取决于写入/读取的频率。
DOMCategory
使用 groovy.xml.dom.DOMCategory
还有另一种解析 XML 文档的方法,它是一个类别类,为 Java 的 DOM 类添加了 GPath 风格的操作。
Java 内置了对使用表示 XML 文档各个部分的类(例如 Document 、Element 、NodeList 、Attr 等)进行 DOM 处理 XML 的支持。有关这些类的更多信息,请参阅相应的 JavaDoc。 |
具有以下 XML:
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>
'''
您可以使用 groovy.xml.DOMBuilder
和 groovy.xml.dom.DOMCategory
解析它。
def reader = new StringReader(CAR_RECORDS)
def doc = DOMBuilder.parse(reader) (1)
def records = doc.documentElement
use(DOMCategory) { (2)
assert records.car.size() == 3
}
1 | 解析 XML |
2 | 创建 DOMCategory 范围以能够使用辅助方法调用 |
3.11.2. GPath
在 Groovy 中查询 XML 最常见的方法是使用 GPath
GPath 是一种集成到 Groovy 中的路径表达式语言,允许识别嵌套结构化数据的部分。从这个意义上说,它与 XPath 用于 XML 的目标和范围相似。使用 GPath 表达式的两个主要地方是处理嵌套的 POJO 或处理 XML 时
它类似于 XPath 表达式,您不仅可以将其与 XML 一起使用,还可以与 POJO 类一起使用。例如,您可以指定到感兴趣的对象或元素的路径
对于 XML,您还可以指定属性,例如
-
a["@href"]
→ 所有 a 元素的 href 属性 -
a.'@href'
→ 另一种表达方式 -
a.@href
→ 使用 XmlSlurper 时另一种表达方式
让我们用一个例子来说明这一点
static final String books = '''
<response version-api="2.0">
<value>
<books>
<book available="20" id="1">
<title>Don Quixote</title>
<author id="1">Miguel 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 Quixote</title>
<author id="4">Miguel de Cervantes</author>
</book>
</books>
</value>
</response>
'''
简单地遍历树
我们能做的第一件事就是使用 POJO 的表示法获取值。让我们获取第一本书的作者姓名
def response = new XmlSlurper().parseText(books)
def authorResult = response.value.books.book[0].author
assert authorResult.text() == 'Miguel de Cervantes'
首先我们使用 XmlSlurper
解析文档,然后我们必须将返回值视为 XML 文档的根,因此在本例中是 "response"。
这就是为什么我们从响应开始遍历文档,然后是 value.books.book[0].author
。请注意,在 XPath
中,节点数组从 [1] 开始而不是 [0],但由于 GPath
基于 Java,因此它从索引 0 开始。
最后,我们将拥有 author
节点的实例,因为我们想要该节点内部的文本,所以我们应该调用 text()
方法。author
节点是 GPathResult
类型的实例,text()
是一个方法,它以 String 的形式提供该节点的内容。
当使用 GPath
处理使用 XmlSlurper
解析的 XML 时,结果将是一个 GPathResult
对象。GPathResult
还有许多其他方便的方法可以将节点内的文本转换为任何其他类型,例如
-
toInteger()
-
toFloat()
-
toBigInteger()
-
…
所有这些方法都尝试将 String
转换为适当的类型。
如果我们使用 XmlParser
解析的 XML,我们可能会处理 Node
类型的实例。但是,在这些示例中应用于 GPathResult
的所有操作也可以应用于 Node。两个解析器的创建者都考虑了 GPath
兼容性。
下一步是从给定节点的属性中获取一些值。在下面的示例中,我们想获取第一本书的作者 ID。我们将使用两种不同的方法。首先让我们看看代码
def response = new XmlSlurper().parseText(books)
def book = response.value.books.book[0] (1)
def bookAuthorId1 = book.@id (2)
def bookAuthorId2 = book['@id'] (3)
assert bookAuthorId1 == '1' (4)
assert bookAuthorId1.toInteger() == 1 (5)
assert bookAuthorId1 == bookAuthorId2
1 | 获取第一本书节点 |
2 | 获取图书的 id 属性 @id |
3 | 使用 map notation ['@id'] 获取图书的 id 属性 |
4 | 将值作为 String 获取 |
5 | 将属性值作为 Integer 获取 |
如您所见,有两种表示法来获取属性:
-
直接表示法,使用
@属性名
-
映射表示法,使用
['@属性名']
两者都同样有效。
使用 children (*)、depthFirst (**) 和 breadthFirst 进行灵活导航
如果您曾经使用过 XPath,您可能使用过以下表达式:
-
/following-sibling::othernode
: 在同一级别查找节点“othernode” -
//
: 查找所有地方
或多或少,我们在 GPath 中有它们的对应项,带有快捷方式 *
(即 children()
)和 **
(即 depthFirst()
)。
第一个示例展示了 *
的简单用法,它只迭代节点的直接子节点。
def response = new XmlSlurper().parseText(books)
// .'*' could be replaced by .children()
def catcherInTheRye = response.value.books.'*'.find { node ->
// node.@id == 2 could be expressed as node['@id'] == 2
node.name() == 'book' && node.@id == '2'
}
assert catcherInTheRye.title.text() == 'Catcher in the Rye'
此测试搜索“books”节点的任何子节点,这些子节点匹配给定条件。更详细地说,该表达式表示:查找“books”节点下直接存在的、标签名为“book”且 ID 值为“2”的任何节点。
此操作大致对应于 breadthFirst()
方法,但它只停止在一个级别,而不是继续到内部级别。
如果我们想查找一个给定值,而无需确切知道它在哪里,该怎么办?假设我们只知道作者“Lewis Carroll”的 ID。我们如何才能找到那本书?使用 **
是解决方案
def response = new XmlSlurper().parseText(books)
// .'**' could be replaced by .depthFirst()
def bookId = response.'**'.find { book ->
book.author.text() == 'Lewis Carroll'
}.@id
assert bookId == 3
**
等同于从当前点向下在树的任何地方查找某个东西。在本例中,我们使用了 find(Closure cl)
方法来查找第一个匹配项。
如果我们想收集所有书名怎么办?这很简单,只需使用 findAll
def response = new XmlSlurper().parseText(books)
def titles = response.'**'.findAll { node -> node.name() == 'title' }*.text()
assert titles.size() == 4
在最后两个示例中,**
用作 depthFirst()
方法的快捷方式。它在从给定节点向下遍历树时,尽可能深入地遍历树。breadthFirst()
方法在遍历到下一个级别之前完成给定级别的所有节点。
以下示例显示了这两种方法之间的区别
def response = new XmlSlurper().parseText(books)
def nodeName = { node -> node.name() }
def withId2or3 = { node -> node.@id in [2, 3] }
assert ['book', 'author', 'book', 'author'] ==
response.value.books.depthFirst().findAll(withId2or3).collect(nodeName)
assert ['book', 'book', 'author', 'author'] ==
response.value.books.breadthFirst().findAll(withId2or3).collect(nodeName)
在此示例中,我们搜索 id 属性值为 2 或 3 的任何节点。同时存在符合该条件的 book
和 author
节点。不同的遍历顺序将找到相同节点,但顺序不同,这取决于树的遍历方式。
值得再次提及的是,有一些有用的方法可以将节点的值转换为整数、浮点数等。当进行如下比较时,这些方法可能会很方便:
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
在这种情况下,数字 2 是硬编码的,但想象一下该值可能来自任何其他来源(数据库等)。
3.11.3. 创建 XML
使用 Groovy 创建 XML 最常用的方法是使用构建器,即以下之一:
-
groovy.xml.MarkupBuilder
-
groovy.xml.StreamingMarkupBuilder
MarkupBuilder
以下是使用 Groovy 的 MarkupBuilder 创建新 XML 文件的示例
def writer = new StringWriter()
def xml = new MarkupBuilder(writer) (1)
xml.records() { (2)
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()) (3)
assert records.car.first().name.text() == 'HSV Maloo'
assert records.car.last().name.text() == 'Royale'
1 | 创建 MarkupBuilder 实例 |
2 | 开始创建 XML 树 |
3 | 创建 XmlSlurper 实例以遍历并测试生成的 XML |
让我们仔细看看
def xmlString = "<movie>the godfather</movie>" (1)
def xmlWriter = new StringWriter() (2)
def xmlMarkup = new MarkupBuilder(xmlWriter)
xmlMarkup.movie("the godfather") (3)
assert xmlString == xmlWriter.toString() (4)
1 | 我们正在创建一个参考字符串以进行比较 |
2 | MarkupBuilder 使用 xmlWriter 实例最终将 xml 表示形式转换为 String 实例 |
3 | xmlMarkup.movie(…) 调用将创建一个名为 movie 且内容为 the godfather 的 XML 节点。 |
def xmlString = "<movie id='2'>the godfather</movie>"
def xmlWriter = new StringWriter()
def xmlMarkup = new MarkupBuilder(xmlWriter)
xmlMarkup.movie(id: "2", "the godfather") (1)
assert xmlString == xmlWriter.toString()
1 | 这次为了同时创建属性和节点内容,您可以创建任意数量的映射条目,最后添加一个值来设置节点的内容 |
该值可以是任何 Object ,该值将被序列化为它的 String 表示形式。 |
def xmlWriter = new StringWriter()
def xmlMarkup = new MarkupBuilder(xmlWriter)
xmlMarkup.movie(id: 2) { (1)
name("the godfather")
}
def movie = new XmlSlurper().parseText(xmlWriter.toString())
assert movie.@id == 2
assert movie.name.text() == 'the godfather'
1 | 闭包表示给定节点的子元素。注意,这次我们没有使用字符串作为属性,而是使用了数字。 |
有时您可能希望在 XML 文档中使用特定命名空间
def xmlWriter = new StringWriter()
def xmlMarkup = new MarkupBuilder(xmlWriter)
xmlMarkup
.'x:movies'('xmlns:x': 'https://groovy-lang.cn') { (1)
'x:movie'(id: 1, 'the godfather')
'x:movie'(id: 2, 'ronin')
}
def movies =
new XmlSlurper() (2)
.parseText(xmlWriter.toString())
.declareNamespace(x: 'https://groovy-lang.cn')
assert movies.'x:movie'.last().@id == 2
assert movies.'x:movie'.last().text() == 'ronin'
1 | 创建具有给定命名空间 xmlns:x 的节点 |
2 | 创建 XmlSlurper ,注册命名空间,以便能够测试我们刚刚创建的 XML |
那么,有没有更具意义的例子呢?我们可能希望生成更多元素,以便在创建 XML 时有一些逻辑
def xmlWriter = new StringWriter()
def xmlMarkup = new MarkupBuilder(xmlWriter)
xmlMarkup
.'x:movies'('xmlns:x': 'https://groovy-lang.cn') {
(1..3).each { n -> (1)
'x:movie'(id: n, "the godfather $n")
if (n % 2 == 0) { (2)
'x:movie'(id: n, "the godfather $n (Extended)")
}
}
}
def movies =
new XmlSlurper()
.parseText(xmlWriter.toString())
.declareNamespace(x: 'https://groovy-lang.cn')
assert movies.'x:movie'.size() == 4
assert movies.'x:movie'*.text().every { name -> name.startsWith('the') }
1 | 从范围生成元素 |
2 | 使用条件创建给定元素 |
当然,构建器实例可以作为参数传递,以便重构/模块化您的代码
def xmlWriter = new StringWriter()
def xmlMarkup = new MarkupBuilder(xmlWriter)
(1)
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': 'https://groovy-lang.cn') {
buildMovieList(xmlMarkup) (2)
}
def movies =
new XmlSlurper()
.parseText(xmlWriter.toString())
.declareNamespace(x: 'https://groovy-lang.cn')
assert movies.'x:movie'.size() == 4
assert movies.'x:movie'*.text().every { name -> name.startsWith('the') }
1 | 在这种情况下,我们创建了一个闭包来处理电影列表的创建 |
2 | 只在需要时使用 buildMovieList 函数 |
StreamingMarkupBuilder
类 groovy.xml.StreamingMarkupBuilder
是用于创建 XML 标记的构建器类。此实现使用 groovy.xml.streamingmarkupsupport.StreamingMarkupWriter
处理输出。
def xml = new StreamingMarkupBuilder().bind { (1)
records {
car(name: 'HSV Maloo', make: 'Holden', year: 2006) { (2)
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()) (3)
assert records.car.size() == 3
assert records.car.find { it.@name == 'P50' }.country.text() == 'Isle of Man'
1 | 请注意,StreamingMarkupBuilder.bind 返回一个 Writable 实例,该实例可用于将标记流式传输到 Writer |
2 | 我们将输出捕获为字符串,以便再次解析它并使用 XmlSlurper 检查生成的 XML 的结构。 |
MarkupBuilderHelper
groovy.xml.MarkupBuilderHelper
,顾名思义,是 groovy.xml.MarkupBuilder
的一个辅助类。
此辅助类通常可以从 groovy.xml.MarkupBuilder
实例或 groovy.xml.StreamingMarkupBuilder
实例中访问。
在以下情况下,此辅助类可能会很有用:
-
在输出中生成注释
-
在输出中生成 XML 处理指令
-
在输出中生成 XML 声明
-
在当前标签的主体中打印数据,转义 XML 实体
-
在当前标签的主体中打印数据
在 MarkupBuilder
和 StreamingMarkupBuilder
中,此辅助类通过 mkp
属性访问
def xmlWriter = new StringWriter()
def xmlMarkup = new MarkupBuilder(xmlWriter).rules {
mkp.comment('THIS IS THE MAIN RULE') (1)
rule(sentence: mkp.yield('3 > n')) (2)
}
(3)
assert xmlWriter.toString().contains('3 > n')
assert xmlWriter.toString().contains('<!-- THIS IS THE MAIN RULE -->')
1 | 使用 mkp 在 XML 中创建注释 |
2 | 使用 mkp 生成转义值 |
3 | 检查两个假设是否都为真 |
这是另一个示例,展示了在使用 StreamingMarkupBuilder
时,从 bind
方法范围内部访问 mkp
属性的用法
def xml = new StreamingMarkupBuilder().bind {
records {
car(name: mkp.yield('3 < 5')) (1)
car(name: mkp.yieldUnescaped('1 < 3')) (2)
}
}
assert xml.toString().contains('3 < 5')
assert xml.toString().contains('1 < 3')
1 | 如果我们想使用 mkp.yield 为 name 属性生成一个转义值 |
2 | 稍后使用 XmlSlurper 检查值 |
DOMToGroovy
假设我们有一个现有的 XML 文档,我们希望自动生成标记而无需手动输入所有内容?我们只需要使用 org.codehaus.groovy.tools.xml.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)) (1)
converter.print(document) (2)
String xmlRecovered =
new GroovyShell()
.evaluate("""
def writer = new StringWriter()
def builder = new groovy.xml.MarkupBuilder(writer)
builder.${output}
return writer.toString()
""") (3)
assert new XmlSlurper().parseText(xmlRecovered).song.title.text() == 'Here I go' (4)
1 | 创建 DOMToGroovy 实例 |
2 | 将 XML 转换为 MarkupBuilder 调用,这些调用在输出 StringWriter 中可用 |
3 | 使用 output 变量创建整个 MarkupBuilder |
4 | 返回 XML 字符串 |
3.11.4. 操作 XML
在本章中,您将看到使用 XmlSlurper
或 XmlParser
添加/修改/删除节点的不同方法。我们将要处理的 xml 如下所示:
def xml = """
<response version-api="2.0">
<value>
<books>
<book id="2">
<title>Don Quixote</title>
<author id="1">Miguel de Cervantes</author>
</book>
</books>
</value>
</response>
"""
添加节点
XmlSlurper
和 XmlParser
的主要区别在于,前者创建节点后,在文档再次评估之前不可用,因此您应该再次解析转换后的文档才能看到新节点。因此,在选择这两种方法时请记住这一点。
如果您需要在创建节点后立即查看它,那么 XmlParser
应该是您的选择,但如果您计划对 XML 进行多次更改并将结果发送到另一个进程,那么 XmlSlurper
可能更高效。
您不能直接使用 XmlSlurper
实例创建新节点,但可以使用 XmlParser
。从 XmlParser 创建新节点的方法是通过其 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"
createNode()
方法接收以下参数:
-
父节点(可以为 null)
-
标签的限定名称(在这种情况下,我们只使用本地部分,不带任何命名空间)。我们使用的是
groovy.namespace.QName
实例 -
包含标签属性的映射(在此特定情况下没有)
无论如何,您通常不会从解析器实例创建节点,而是从已解析的 XML 实例创建节点。也就是说,从 Node
或 GPathResult
实例创建节点。
请看下一个示例。我们使用 XmlParser
解析 XML,然后从解析后的文档实例创建新节点(请注意,此处的方法接收参数的方式略有不同)
def parser = new XmlParser()
def response = parser.parseText(xml)
response.appendNode(
new QName("numberOfResults"),
[:],
"1"
)
response.numberOfResults.text() == "1"
使用 XmlSlurper
时,GPathResult
实例没有 createNode()
方法。
修改/删除节点
我们知道如何解析文档,添加新节点,现在我想更改给定节点的内容。让我们开始使用 XmlParser
和 Node
。此示例将第一本书的信息更改为另一本书。
def response = new XmlParser().parseText(xml)
/* Use the same syntax as groovy.xml.MarkupBuilder */
response.value.books.book[0].replaceNode { (1)
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"
使用 replaceNode()
时,我们作为参数传递的闭包应遵循与使用 groovy.xml.MarkupBuilder
相同的规则
这是使用 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 Quixote"
/* 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"
请注意,使用 XmlSlurper
时,我们必须再次解析转换后的文档才能找到创建的节点。在这个特定的示例中,这可能有点烦人,不是吗?
最后,这两个解析器也使用相同的方法为给定属性添加新属性。这次的区别在于您是否希望新节点立即可用。首先是 XmlParser
def parser = new XmlParser()
def response = parser.parseText(xml)
response.@numberOfResults = "1"
assert response.@numberOfResults == "1"
和 XmlSlurper
def response = new XmlSlurper().parseText(books)
response.@numberOfResults = "2"
assert response.@numberOfResults == "2"
使用 XmlSlurper
时,添加新属性无需执行新评估。
打印 XML
XmlUtil
有时获取给定节点的值以及节点本身(例如将此节点添加到另一个 XML)很有用。
为此,您可以使用 groovy.xml.XmlUtil
类。它有几个静态方法可以将 XML 片段从多种类型的源(Node、GPathResult、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">Miguel de Cervantes</author>')
3.12. 处理 YAML
Groovy 有一个可选的 groovy-yaml
模块,它提供对 Groovy 对象和 YAML 之间转换的支持。用于 YAML 序列化和解析的类位于 groovy.yaml
包中。
3.12.1. YamlSlurper
YamlSlurper
是一个类,它将 YAML 文本或读取器内容解析为 Groovy 数据结构(对象),例如映射、列表和基本类型,如 Integer
、Double
、Boolean
和 String
。
该类附带了许多重载的 parse
方法以及一些特殊方法,例如 parseText
等。在下一个示例中,我们将使用 parseText
方法。它解析 YAML String
并将其递归转换为对象列表或映射。其他 parse*
方法类似,它们返回 YAML String
,但适用于不同的参数类型。
def ys = new YamlSlurper()
def yaml = ys.parseText '''
language: groovy
sudo: required
dist: trusty
matrix:
include:
- jdk: openjdk10
- jdk: oraclejdk9
- jdk: oraclejdk8
before_script:
- |
unset _JAVA_OPTIONS
'''
assert 'groovy' == yaml.language
assert 'required' == yaml.sudo
assert 'trusty' == yaml.dist
assert ['openjdk10', 'oraclejdk9', 'oraclejdk8'] == yaml.matrix.include.jdk
assert ['unset _JAVA_OPTIONS'] == yaml.before_script*.trim()
请注意,结果是普通的映射,可以像普通的 Groovy 对象实例一样处理。YamlSlurper
根据 YAML Ain’t Markup Language (YAML™) 的定义解析给定的 YAML。
由于 YamlSlurper
返回纯 Groovy 对象实例,背后没有任何特殊的 YAML 类,因此其用法是透明的。实际上,YamlSlurper
结果符合 GPath 表达式。GPath 是一种强大的表达式语言,受多种不同数据格式的 slurper 支持(XmlSlurper
用于 XML 是一个示例)。
有关更多详细信息,请参阅 GPath 表达式 一节。 |
下表概述了 YAML 类型和相应的 Groovy 数据类型
YAML | Groovy |
---|---|
字符串 |
|
数字 |
|
对象 |
|
数组 |
|
true |
|
false |
|
null |
|
日期 |
|
当 YAML 中的值为 null 时,YamlSlurper 会将其补充为 Groovy null 值。这与将 null 值表示为库提供的单例对象的其他 YAML 解析器不同。 |
构建器
从 Groovy 创建 YAML 的另一种方法是使用 YamlBuilder
。构建器提供了一个 DSL,允许形成一个对象图,然后将其转换为 YAML。
def builder = new YamlBuilder()
builder.records {
car {
name 'HSV Maloo'
make 'Holden'
year 2006
country 'Australia'
homepage new URL('http://example.org')
record {
type 'speed'
description 'production pickup truck with speed of 271kph'
}
}
}
assert builder.toString() == '''---
records:
car:
name: "HSV Maloo"
make: "Holden"
year: 2006
country: "Australia"
homepage: "http://example.org"
record:
type: "speed"
description: "production pickup truck with speed of 271kph"
'''
3.13. 处理 TOML
Groovy 有一个可选的 groovy-toml
模块,它提供对 Groovy 对象和 TOML 之间转换的支持。用于 TOML 序列化和解析的类位于 groovy.toml
包中。
3.13.1. TomlSlurper
TomlSlurper
是一个类,它将 TOML 文本或读取器内容解析为 Groovy 数据结构(对象),例如映射、列表和基本类型,如 Integer
、Double
、Boolean
和 String
。
该类附带了许多重载的 parse
方法以及一些特殊方法,例如 parseText
等。在下一个示例中,我们将使用 parseText
方法。它解析 TOML String
并将其递归转换为对象列表或映射。其他 parse*
方法类似,它们返回 TOML String
,但适用于不同的参数类型。
def ts = new TomlSlurper()
def toml = ts.parseText '''
language = "groovy"
sudo = "required"
dist = "trusty"
before_script = [ "unset _JAVA_OPTIONS\\n\\n \\n" ]
[[matrix.include]]
jdk = "openjdk10"
[[matrix.include]]
jdk = "oraclejdk9"
[[matrix.include]]
jdk = "oraclejdk8"
'''
assert 'groovy' == toml.language
assert 'required' == toml.sudo
assert 'trusty' == toml.dist
assert ['openjdk10', 'oraclejdk9', 'oraclejdk8'] == toml.matrix.include.jdk
assert ['unset _JAVA_OPTIONS'] == toml.before_script*.trim()
请注意,结果是一个普通的映射,可以像普通的 Groovy 对象实例一样处理。TomlSlurper
根据 Tom’s Obvious, Minimal Language 的定义解析给定的 TOML。
由于 TomlSlurper
返回纯 Groovy 对象实例,背后没有任何特殊的 TOML 类,因此其用法是透明的。实际上,TomlSlurper
结果符合 GPath 表达式。GPath 是一种强大的表达式语言,受多种不同数据格式的 slurper 支持(XmlSlurper
用于 XML 是一个示例)。
有关更多详细信息,请参阅 GPath 表达式 一节。 |
下表概述了 TOML 类型和相应的 Groovy 数据类型
TOML | Groovy |
---|---|
字符串 |
|
数字 |
|
对象 |
|
数组 |
|
true |
|
false |
|
null |
|
日期 |
|
当 TOML 中的值为 null 时,TomlSlurper 会将其补充为 Groovy null 值。这与将 null 值表示为库提供的单例对象的其他 TOML 解析器不同。 |
构建器
从 Groovy 创建 TOML 的另一种方法是使用 TomlBuilder
。该构建器提供了一个 DSL,允许形成一个对象图,然后将其转换为 TOML。
def builder = new TomlBuilder()
builder.records {
car {
name 'HSV Maloo'
make 'Holden'
year 2006
country 'Australia'
homepage new URL('http://example.org')
record {
type 'speed'
description 'production pickup truck with speed of 271kph'
}
}
}
assert builder.toString() == '''\
records.car.name = 'HSV Maloo'
records.car.make = 'Holden'
records.car.year = 2006
records.car.country = 'Australia'
records.car.homepage = 'http://example.org'
records.car.record.type = 'speed'
records.car.record.description = 'production pickup truck with speed of 271kph'
'''
3.14. Groovy Contracts – 对 Groovy 的契约式设计支持
该模块提供了契约注释,支持在 Groovy 类和接口上指定类不变量、前置条件和后置条件。提供了特殊支持,以便后置条件可以引用变量的旧值或与调用方法相关联的结果值。
3.14.1. 应用 @Invariant、@Requires 和 @Ensures
当您的类路径中包含 GContracts 时,可以通过使用 org.gcontracts.annotations 包中找到的断言之一来将契约应用于 Groovy 类或接口。
package acme
import groovy.contracts.*
@Invariant({ speed() >= 0 })
class Rocket {
int speed = 0
boolean started = true
@Requires({ isStarted() })
@Ensures({ old.speed < speed })
def accelerate(inc) { speed += inc }
def isStarted() { started }
def speed() { speed }
}
def r = new Rocket()
r.accelerate(5)
3.14.2. 更多功能
GContracts 支持以下功能集:
-
通过 @Invariant、@Requires 和 @Ensures 定义类不变量、前置条件和后置条件
-
继承具体前身类的类不变量、前置条件和后置条件
-
在已实现的接口中继承类不变量、前置条件和后置条件
-
在后置条件断言中使用 old 和 result 变量
-
在普通 Groovy 对象 (POGOs) 中注入断言
-
基于 Groovy 强大断言的可读断言消息
-
通过 @AssertionsEnabled 在包或类级别启用契约
-
使用 Java 的 -ea 和 -da VM 参数启用或禁用契约检查
-
注释契约:一种在项目领域模型中重用重复契约元素的方式
-
循环断言方法调用检测
3.14.3. 栈示例
目前,Groovy Contracts 支持 3 个注解:@Invariant、@Requires 和 @Ensures —— 它们都作为带有闭包的注解工作,其中闭包允许您将任意代码片段指定为注解参数
@Grab(group='org.apache.groovy', module='groovy-contracts', version='4.0.0')
import groovy.contracts.*
@Invariant({ elements != null })
class Stack<T> {
List<T> elements
@Ensures({ is_empty() })
def Stack() {
elements = []
}
@Requires({ preElements?.size() > 0 })
@Ensures({ !is_empty() })
def Stack(List<T> preElements) {
elements = preElements
}
boolean is_empty() {
elements.isEmpty()
}
@Requires({ !is_empty() })
T last_item() {
elements.get(count() - 1)
}
def count() {
elements.size()
}
@Ensures({ result == true ? count() > 0 : count() >= 0 })
boolean has(T item) {
elements.contains(item)
}
@Ensures({ last_item() == item })
def push(T item) {
elements.add(item)
}
@Requires({ !is_empty() })
@Ensures({ last_item() == item })
def replace(T item) {
remove()
elements.add(item)
}
@Requires({ !is_empty() })
@Ensures({ result != null })
T remove() {
elements.remove(count() - 1)
}
String toString() { elements.toString() }
}
def stack = new Stack<Integer>()
上面的示例指定了一个类不变量和具有前置条件和后置条件的方法。请注意,前置条件可以引用方法参数,后置条件可以通过结果变量访问方法的结果,并通过 old 访问旧的实例变量值。
确实,Groovy AST 转换将这些断言注解更改为 Java 断言语句(可以使用 JVM 参数打开和关闭),并将它们注入到适当的位置,例如,类不变量用于在每次方法调用之前和之后检查对象的状态。
3.15. 脚本 Ant 任务
由于 AntBuilder,Groovy 与 Apache Ant 结合得很好。
3.16. <groovy> Ant 任务
3.16.1. <groovy>
在这里,我们描述一个 Ant 任务,用于在 Ant 构建文件中使用 Groovy。您可能还对 Ant 内置的 script 任务感兴趣,它支持 Groovy 和其他语言,或者 AntBuilder ,它允许您用 Groovy 而不是 XML 编写 Ant 构建脚本。 |
从 Apache Ant 执行一系列 Groovy 语句。语句可以从资源中读取,也可以作为封装 Groovy 标签之间的直接文本。
3.16.2. 必需 taskdef
假设您需要的所有 groovy jar 都位于 my.classpath 中(这将是 groovy-VERSION.jar
、groovy-ant-VERSION.jar
以及您可能正在使用的任何模块和传递依赖项),您需要在调用 groovy
任务之前在 build.xml
中的某个点声明此任务。
<taskdef name="groovy"
classname="org.codehaus.groovy.ant.Groovy"
classpathref="my.classpath"/>
您可以简单地将语句放在 groovy
标签之间,如下所示:
<groovy>
...
</groovy>
或者您可以将 Groovy 源脚本作为资源提供。您可以使用 src
属性指定路径名,如下所示:
<groovy src="/some/path/MyGroovyScript.groovy" otherAttributes="...">
或作为嵌套的 fileset
,如下所示(尽管 fileset 定义预计只选择一个文件)
<groovy>
<fileset file="MyGroovyScript.groovy"/>
</groovy>
或者作为嵌套的单个元素 资源集合,它可能看起来像以下任何一种:
<groovy>
<file file="MyGroovyScript.groovy"/>
</groovy>
<groovy>
<url url="https://some.domain/some/path/to/MyGroovyScript.groovy"/>
</groovy>
<groovy>
<javaconstant name="some.packagename.SomeClass.MY_CODE_FRAGMENT"/>
</groovy>
您还可以提供一个 过滤器链,如下所示:
<groovy>
<fileset file="MyGroovyScript.groovy"/>
<!-- take 5 lines after skipping 18 lines, just as an example -->
<filterchain>
<headfilter lines="5" skip="18"/>
</filterchain>
</groovy>
如果您的任何模块通过 classpath 加载服务,例如 groovy-json
,您可能需要使用 contextClassLoader 属性(请参阅下文)。
3.16.3. <groovy> 属性
属性 | 描述 | 必填项 |
---|---|---|
src |
包含 Groovy 语句的文件。包含该文件的目录将添加到类路径中。 |
是的,除非语句包含在标签内 |
classpath |
要使用的类路径。 |
不 |
classpathref |
要使用的类路径,以对其他地方定义的 PATH 的引用形式给出。 |
不 |
output |
设置输出文件;默认为 Ant 日志。 |
不 |
append |
如果启用且输出到文件,则追加到现有文件而不是覆盖。默认为 false。 |
不 |
fork |
如果启用,脚本将在派生的 JVM 进程中执行(默认禁用)。 |
不 |
scriptBaseClass |
脚本的基类名称。 |
不 |
parameters |
在 JDK 8 及更高版本上为方法参数名称的反射生成元数据。默认为 false。 |
不 |
useGroovyShell |
如果启用,将使用新的 GroovyShell 运行脚本。特殊变量将不可用,但您不需要 Ant 在类路径中。默认为 false。 |
不 |
includeAntRuntime |
如果启用,在分叉时,系统类路径将包含在类路径中。默认为 true。 |
不 |
stacktrace |
如果启用,如果在编译期间发生错误,将报告堆栈跟踪。默认为 false。 |
不 |
configScript |
设置 Groovy 编译器配置的配置文件脚本。 |
不 |
contextClassLoader |
如果启用,contextClassLoader 将设置为用于运行脚本的 shell 的 classLoader。如果 fork 为 true,则不使用。 |
不 |
3.16.4. 指定为嵌套元素的参数
<classpath>
Groovy 的 classpath 属性是类似于 PATH 的结构,也可以通过嵌套的 classpath 元素设置。
3.16.5. 可用绑定
您的 Groovy 语句中可使用多个绑定。
名称 | 描述 |
---|---|
ant |
一个 |
project |
当前 Ant 项目 |
properties |
一个 Ant 属性的 |
target |
调用此 Groovy 脚本的拥有目标 |
task |
包装任务,可以访问 |
args |
命令行参数,如果有的话 |
3.16.6. 示例
Hello world,版本 1
<groovy>
println "Hello World"
</groovy>
Hello world,版本 2
<groovy>
ant.echo "Hello World"
</groovy>
列出当前目录中的所有 xml 文件
<groovy>
xmlfiles = new File(".").listFiles().findAll{ it =~ "\.xml$" }
xmlfiles.sort().each { println it.toString() }
</groovy>
列出 jar 中的所有 xml 文件
<zipfileset id="found" src="foobar.jar"
includes="**/*.xml"/>
<groovy>
project.references.found.each {
println it.name
}
</groovy>
运行脚本
<groovy src="/some/directory/some/file.groovy">
<classpath>
<pathelement location="/my/groovy/classes/directory"/>
</classpath>
</groovy>
查找 jar 目录中所有具有 org.*
包的 Builder
类
<property name="local.target" value="C:/Projects/GroovyExamples"/>
<groovy>
import java.util.jar.JarFile
def classes = []
def resourceNamePattern = /org\/.*\/.*Builder.class/
def jarNamePattern = /.*(beta|commons).*jar$/
def libdir = new File("${properties['local.target']}/lib")
libdir.listFiles().grep(~jarNamePattern).each { candidate ->
new JarFile(candidate).entries().each { entry ->
if (entry.name ==~ resourceNamePattern) classes += entry.name
}
}
properties["builder-classes"] = classes.join(' ')
</groovy>
<echo message='${builder-classes}'/>
这可能会导致以下结果:
org/apache/commons/cli/PatternOptionBuilder.class org/apache/commons/cli/OptionBuilder.class org/codehaus/groovy/tools/groovydoc/GroovyRootDocBuilder.class org/custommonkey/xmlunit/HTMLDocumentBuilder.class org/custommonkey/xmlunit/TolerantSaxDocumentBuilder.class
上述内容的 FileScanner 版本(收集名称略有不同)
<groovy>
import java.util.jar.JarFile
def resourceNamePattern = /org\/.*\/.*Builder.class/
def candidates = ant.fileScanner {
fileset(dir: '${local.target}/lib') {
include(name: '*beta*.jar')
include(name: '*commons*.jar')
}
}
def classes = candidates.collect {
new JarFile(it).entries().collect { it.name }.findAll {
it ==~ resourceNamePattern
}
}.flatten()
properties["builder-classes"] = classes.join(' ')
</groovy>
从您的 Ant 脚本调用 Web 服务
<?xml version="1.0" encoding="UTF-8"?>
<project name="SOAP example" default="main" basedir=".">
<property environment="env"/>
<property name="celsius" value="0"/>
<target name="main">
<taskdef name="groovy" classname="org.codehaus.groovy.ant.Groovy">
<classpath>
<fileset dir="${env.GROOVY_HOME}" includes="lib/groovy-*.jar,lib/ivy*.jar"/>
</classpath>
</taskdef>
<groovy>
@Grab('org.codehaus.groovy.modules:groovyws:0.5.1')
import groovyx.net.ws.WSClient
def url = 'https://w3schools.org.cn/webservices/tempconvert.asmx?WSDL'
def proxy = new WSClient(url, this.class.classLoader)
proxy.initialize()
ant.echo "I'm freezing at ${properties.celsius} degrees Celsius"
properties.result = proxy.CelsiusToFahrenheit(properties.celsius)
</groovy>
<antcall target="results"/>
</target>
<target name="results">
<echo message="I'm freezing at ${result} degrees Fahrenheit"/>
</target>
</project>
这将输出以下内容(以及一些信息消息)
main:
...
[echo] I'm freezing at 0 degrees Celsius
results:
[echo] I'm freezing at 32 degrees Fahrenheit
BUILD SUCCESSFUL
设置参数
<target name="run">
<groovy>
<arg line="1 2 3"/>
<arg value="4 5"/>
println args.size()
println args[2]
args.each{ ant.echo(message:it) }
</groovy>
</target>
输出
Buildfile: build.xml
run:
[groovy] 4
[groovy] 3
[echo] 1
[echo] 2
[echo] 3
[echo] 4 5
BUILD SUCCESSFUL
3.17. <groovyc> Ant 任务
3.17.1. <groovyc>
描述
从 Apache Ant 编译 Groovy 源文件,如果使用联合编译选项,则编译 Java 源文件。
所需 taskdef
假设 groovy jar 位于 groovy.libs 中,您需要在调用 groovyc
任务之前在 build.xml
中的某个点声明此任务。还要考虑添加您可能正在使用的任何额外的 Groovy 模块 jar、库和可能的传递依赖项。
<taskdef name="groovyc" classname="org.codehaus.groovy.ant.Groovyc">
<classpath>
<fileset file="${groovy.libs}/groovy-ant-VERSION.jar"/>
<fileset file="${groovy.libs}/groovy-VERSION.jar"/>
</classpath>
</taskdef>
<groovyc> 属性
属性 | 描述 | 必填项 |
---|---|---|
srcdir |
Groovy(可能还有 Java)源文件的位置。 |
是 |
目标目录 |
存储类文件的位置。 |
是 |
classpath |
要使用的类路径。 |
不 |
classpathref |
要使用的类路径,以路径引用形式给出。 |
不 |
源路径 |
要使用的源路径。 |
不 |
sourcepathref |
要使用的源路径,以路径引用形式给出。 |
不 |
encoding |
源文件的编码。 |
不 |
verbose |
要求编译器输出详细信息;默认为否。 |
不 |
includeAntRuntime |
是否在类路径中包含 Ant 运行时库;默认为是。 |
不 |
includeJavaRuntime |
是否在类路径中包含来自正在执行的 VM 的默认运行时库;默认为否。 |
不 |
includeDestClasses |
此属性控制是否将目标类目录包含在提供给编译器的类路径中。默认值为“true”。 |
不 |
fork |
是否使用派生的 JVM 实例执行 groovyc;默认为否。 |
不 |
memoryInitialSize |
如果使用 fork 模式,则为底层 VM 的初始内存大小;否则忽略。默认为标准 VM 内存设置。(示例:83886080、81920k 或 80m) |
不 |
memoryMaximumSize |
如果使用 fork 模式,则为底层 VM 的最大内存大小;否则忽略。默认为标准 VM 内存设置。(示例:83886080、81920k 或 80m) |
不 |
failonerror |
指示编译错误是否会导致构建失败;默认为 true。 |
不 |
proceed |
failonerror 的反向别名。 |
不 |
listfiles |
指示是否列出要编译的源文件;默认为否。 |
不 |
stacktrace |
如果为 true,则每个编译错误消息都将包含堆栈跟踪 |
不 |
indy |
当使用 Groovy 2.0 及更高版本并在 JDK 7 上运行时,启用对“invoke dynamic”支持的编译 |
不 |
scriptBaseClass |
设置 Groovy 脚本的基类 |
不 |
stubdir |
设置 Java 源存根文件生成的存根目录。该目录不需要存在,也不会自动删除——尽管其内容将被清除,除非 'keepStubs' 为 true。分叉时忽略。 |
不 |
keepStubs |
设置 keepStubs 标志。默认为 false。设置为 true 用于调试。分叉时忽略。 |
不 |
forceLookupUnnamedFiles |
Groovyc Ant 任务经常用于知道要编译的源文件完整列表的构建系统上下文中。在这种情况下,Groovy 编译器在查找源文件时搜索类路径是浪费的,因此 Groovyc Ant 任务默认以关闭此类搜索的特殊模式调用编译器。如果您希望编译器搜索源文件,则需要将此标志设置为 true。默认为 false。 |
不 |
configscript |
设置用于自定义编译配置的配置文件。 |
不 |
parameters |
在 JDK 8 及更高版本上为方法参数名称的反射生成元数据。默认为 false。 |
不 |
previewFeatures |
在 JDK 12 及更高版本上启用 JEP 预览功能。默认为 false。 |
不 |
targetBytecode |
设置字节码兼容级别。 |
不 |
javahome |
设置要使用的 |
不 |
executable |
设置在分叉模式下调用编译器时要使用的 java 可执行文件的名称,否则忽略。 |
不 |
scriptExtension |
设置搜索 Groovy 源文件时使用的扩展名。接受 *.groovy、.groovy 或 groovy 形式的扩展名。 |
不 |
updatedProperty |
编译成功时要设置的属性。如果编译失败或没有要编译的文件,则不会设置此属性。 |
不 |
errorProperty |
编译失败时要设置的属性。如果编译失败,则会设置此属性。 |
不 |
示例
<path id="classpath.main">
<fileset dir="${groovy.libs}" includes="*.jar" excludes="groovy-ant-*.jar"/>
...
</path>
<groovyc srcdir="${dir.sources}" destdir="${dir.classes}" classpathref="classpath.main"
fork="true" includeantruntime="false" configscript="config.groovy" targetBytecode="1.8"/>
<groovyc> 嵌套元素
元素 | 种类 | 必填项 | 替换属性 |
---|---|---|---|
src |
路径结构 |
是(除非使用 srcdir) |
srcdir |
classpath |
路径结构 |
不 |
classpath 或 classpathref |
javac |
javac 任务 |
不 |
不适用 |
注意事项
-
有关路径结构,请参阅例如 https://ant.apache.ac.cn/manual/using.html#path
-
有关
javac
任务的用法,请参阅 https://ant.apache.ac.cn/manual/Tasks/javac.html -
嵌套的
javac
任务的行为或多或少与顶级javac
任务的文档一致。嵌套的javac
任务的srcdir
、destdir
、fork
、memoryInitialSize
和memoryMaximumSize
取自封闭的groovyc
任务。如果指定了这些属性或任何其他未明确支持的属性,则会记录警告,并且它们将被完全忽略。嵌套的javac
任务上指定的classpath
和classpathref
将与从封闭的groovyc
任务获取的值合并,并用于 Groovy 编译。在嵌套的javac
任务中,唯一支持的元素是compilerarg
,并且仅支持value
属性,它被视为顶级javac
任务的line
属性,即它由空格分隔为单独的参数。只有以-W
、-X
或-proc:
开头的参数才会被正确转换。其他任何内容都将原样提供给 groovyc,并且必须手动添加-F
或-J
前缀。
联合编译
联合编译通过使用嵌入式 javac
元素启用,如下例所示:
<groovyc srcdir="${testSourceDirectory}" destdir="${testClassesDirectory}" targetBytecode="1.8">
<classpath>
<pathelement path="${mainClassesDirectory}"/>
<pathelement path="${testClassesDirectory}"/>
<path refid="testPath"/>
</classpath>
<javac debug="true" source="1.8" target="1.8" />
</groovyc>
有关联合编译的更多详细信息,请参阅联合编译部分。
3.18. 模板引擎
3.18.1. 简介
Groovy 支持多种动态生成文本的方式,包括 GStrings
、printf
和 MarkupBuilder 等。此外,还有一个专门的模板框架,非常适合文本生成遵循静态模板形式的应用程序。
3.18.2. 模板框架
Groovy 中的模板框架由 TemplateEngine
抽象基类(引擎必须实现)和 Template
接口(生成的模板必须实现)组成。
Groovy 附带了几个模板引擎:
-
SimpleTemplateEngine
- 用于基本模板 -
StreamingTemplateEngine
- 功能上等同于SimpleTemplateEngine
,但可以处理大于 64k 的字符串 -
GStringTemplateEngine
- 将模板存储为可写闭包(适用于流式场景) -
XmlTemplateEngine
- 在模板和输出均为有效 XML 时工作良好 -
MarkupTemplateEngine
- 一个非常完整、优化的模板引擎
3.18.3. SimpleTemplateEngine
这里展示的是 SimpleTemplateEngine
,它允许您在模板中使用类似 JSP 的 scriptlet(参见下面的示例)、脚本和 EL 表达式,以生成参数化文本。以下是使用该系统的一个示例:
def text = 'Dear "$firstname $lastname",\nSo nice to meet you in <% print city %>.\nSee you in ${month},\n${signed}'
def binding = ["firstname":"Sam", "lastname":"Pullara", "city":"San Francisco", "month":"December", "signed":"Groovy-Dev"]
def engine = new groovy.text.SimpleTemplateEngine()
def template = engine.createTemplate(text).make(binding)
def result = 'Dear "Sam Pullara",\nSo nice to meet you in San Francisco.\nSee you in December,\nGroovy-Dev'
assert result == template.toString()
虽然通常不建议在模板(或视图)中混合处理逻辑,但有时非常简单的逻辑可能会很有用。例如,在上面的示例中,我们可以将此更改为
$firstname
更改为(假设我们已在模板内部设置了 capitalize 的静态导入)
${firstname.capitalize()}
或者这个
<% print city %>
到这个
<% print city == "New York" ? "The Big Apple" : city %>
高级用法说明
如果您碰巧将模板直接嵌入到脚本中(如我们上面所做的那样),您必须小心反斜杠转义。因为模板字符串本身将在传递给模板框架之前由 Groovy 解析,所以您必须转义作为 Groovy 程序的一部分输入的 GString 表达式或 scriptlet“代码”中的任何反斜杠。例如,如果我们想在The Big Apple周围加上引号,我们将使用
<% print city == "New York" ? "\\"The Big Apple\\"" : city %>
同样,如果我们想要换行,我们将使用
\\n
在 Groovy 脚本中出现的任何 GString 表达式或 scriptlet“代码”中。正常的“\n”在静态模板文本本身中或整个模板本身在外部模板文件中时是正常的。同样,要在文本中表示实际的反斜杠,您需要
\\\\
在外部文件或
\\\\
在任何 GString 表达式或 scriptlet“代码”中。(注意:这种额外的斜杠的必要性可能会在 Groovy 的未来版本中消失,如果我们能找到一种简单的方法来支持这种更改。)
3.18.4. StreamingTemplateEngine
StreamingTemplateEngine
引擎在功能上等同于 SimpleTemplateEngine
,但它使用可写闭包创建模板,使其更具可扩展性,适用于大型模板。具体来说,此模板引擎可以处理大于 64k 的字符串。
它使用 JSP 风格的 <% %> 脚本和 <%= %> 表达式语法或 GString 风格的表达式。变量 'out' 绑定到模板正在写入的 writer。
通常,模板源将是一个文件,但这里我们展示了一个简单示例,将模板作为字符串提供
def text = '''\
Dear <% out.print firstname %> ${lastname},
We <% if (accepted) out.print 'are pleased' else out.print 'regret' %> \
to inform you that your paper entitled
'$title' was ${ accepted ? 'accepted' : 'rejected' }.
The conference committee.'''
def template = new groovy.text.StreamingTemplateEngine().createTemplate(text)
def binding = [
firstname : "Grace",
lastname : "Hopper",
accepted : true,
title : 'Groovy for COBOL programmers'
]
String response = template.make(binding)
assert response == '''Dear Grace Hopper,
We are pleased to inform you that your paper entitled
'Groovy for COBOL programmers' was accepted.
The conference committee.'''
3.18.5. GStringTemplateEngine
作为使用 GStringTemplateEngine
的一个示例,这里再次使用上面的示例(进行了一些更改以显示其他选项)。首先,这次我们将模板存储在一个文件中
Dear "$firstname $lastname",
So nice to meet you in <% out << (city == "New York" ? "\\"The Big Apple\\"" : city) %>.
See you in ${month},
${signed}
请注意,我们使用 out
而不是 print
来支持 GStringTemplateEngine
的流式传输特性。由于模板在单独的文件中,因此无需转义反斜杠。以下是我们如何调用它:
def f = new File('test.template')
def engine = new groovy.text.GStringTemplateEngine()
def template = engine.createTemplate(f).make(binding)
println template.toString()
这是输出:
Dear "Sam Pullara", So nice to meet you in "The Big Apple". See you in December, Groovy-Dev
3.18.6. XmlTemplateEngine
XmlTemplateEngine
用于模板场景,其中模板源和预期输出都旨在为 XML。模板可以使用正常的 ${expression}
和 $variable
符号将任意表达式插入到模板中。此外,还支持特殊标签:<gsp:scriptlet>
(用于插入代码片段)和 <gsp:expression>
(用于生成输出的代码片段)。
注释和处理指令将作为处理的一部分被删除,特殊 XML 字符(如 <
、>
、"
和 '
)将使用相应的 XML 符号进行转义。输出也将使用标准 XML 漂亮的打印进行缩进。
gsp: 标签的 xmlns 命名空间定义将被删除,但其他命名空间定义将保留(但可能会更改到 XML 树中的等效位置)。
通常,模板源将在文件中,但这里有一个简单的示例,将 XML 模板作为字符串提供:
def binding = [firstname: 'Jochen', lastname: 'Theodorou', nickname: 'blackdrag', salutation: 'Dear']
def engine = new groovy.text.XmlTemplateEngine()
def text = '''\
<document xmlns:gsp='http://groovy.codehaus.org/2005/gsp' xmlns:foo='baz' type='letter'>
<gsp:scriptlet>def greeting = "${salutation}est"</gsp:scriptlet>
<gsp:expression>greeting</gsp:expression>
<foo:to>$firstname "$nickname" $lastname</foo:to>
How are you today?
</document>
'''
def template = engine.createTemplate(text).make(binding)
println template.toString()
此示例将生成以下输出:
<document type='letter'>
Dearest
<foo:to xmlns:foo='baz'>
Jochen "blackdrag" Theodorou
</foo:to>
How are you today?
</document>
3.18.7. MarkupTemplateEngine
此模板引擎主要用于生成类似 XML 的标记(XML、XHTML、HTML5 等),但也可以用于生成任何基于文本的内容。与传统模板引擎不同,此引擎依赖于使用构建器语法的 DSL。这是一个示例模板:
xmlDeclaration()
cars {
cars.each {
car(make: it.make, model: it.model)
}
}
如果你用以下模型喂它
model = [cars: [new Car(make: 'Peugeot', model: '508'), new Car(make: 'Toyota', model: 'Prius')]]
它将被渲染为:
<?xml version='1.0'?>
<cars><car make='Peugeot' model='508'/><car make='Toyota' model='Prius'/></cars>
此模板引擎的主要功能是:
-
类似标记构建器的语法
-
模板被编译成字节码
-
快速渲染
-
模型的可选类型检查
-
includes
-
国际化支持
-
片段/布局
模板格式
基础知识
模板由 Groovy 代码组成。让我们更彻底地探索第一个示例:
xmlDeclaration() (1)
cars { (2)
cars.each { (3)
car(make: it.make, model: it.model) (4)
} (5)
}
1 | 渲染 XML 声明字符串。 |
2 | 打开 cars 标签 |
3 | cars 是模板模型中的一个变量,它是一个 Car 实例列表 |
4 | 对于每个项目,我们都会创建一个带有 Car 实例属性的 car 标签 |
5 | 关闭 cars 标签 |
如您所见,常规 Groovy 代码可以在模板中使用。在这里,我们正在列表上调用 each
(从模型中检索),允许我们为每个条目渲染一个 car
标签。
以类似的方式,渲染 HTML 代码就这么简单:
yieldUnescaped '<!DOCTYPE html>' (1)
html(lang:'en') { (2)
head { (3)
meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"') (4)
title('My page') (5)
} (6)
body { (7)
p('This is an example of HTML contents') (8)
} (9)
} (10)
1 | 渲染 HTML 文档类型特殊标签 |
2 | 打开带有属性的 html 标签 |
3 | 打开 head 标签 |
4 | 渲染带有 http-equiv 属性的 meta 标签 |
5 | 渲染 title 标签 |
6 | 关闭 head 标签 |
7 | 打开 body 标签 |
8 | 渲染 p 标签 |
9 | 关闭 body 标签 |
10 | 关闭 html 标签 |
输出非常简单:
<!DOCTYPE html><html lang='en'><head><meta http-equiv='"Content-Type" content="text/html; charset=utf-8"'/><title>My page</title></head><body><p>This is an example of HTML contents</p></body></html>
通过一些 配置,您可以让输出漂亮地打印,自动添加换行符和缩进。 |
支持方法
在前面的示例中,文档类型声明使用 yieldUnescaped
方法渲染。我们还看到了 xmlDeclaration
方法。模板引擎提供了几个支持方法,可以帮助您适当地渲染内容:
方法 | 描述 | 示例 |
---|---|---|
yield |
渲染内容,但在渲染前进行转义 |
模板:
输出:
|
yieldUnescaped |
渲染原始内容。参数按原样渲染,不进行转义。 |
模板:
输出:
|
xmlDeclaration |
渲染 XML 声明字符串。如果在配置中指定了编码,则会写入声明中。 |
模板:
输出:
如果 输出:
|
comment |
在 XML 注释中渲染原始内容 |
模板:
输出:
|
newLine |
渲染新行。另请参阅 |
模板:
输出:
|
pi |
渲染 XML 处理指令。 |
模板:
输出:
|
tryEscape |
如果对象是 |
模板:
输出:
|
包含
MarkupTemplateEngine
支持从另一个文件包含内容。包含的内容可以是:
-
另一个模板
-
原始内容
-
需要转义的内容
包含另一个模板可以使用:
include template: 'other_template.tpl'
将文件作为原始内容包含,不转义,可以这样做:
include unescaped: 'raw.txt'
最后,需要转义才能渲染的文本可以这样包含:
include escaped: 'to_be_escaped.txt'
或者,您可以使用以下辅助方法代替:
-
includeGroovy(<name>)
用于包含另一个模板 -
includeEscaped(<name>)
用于包含另一个文件并进行转义 -
includeUnescaped(<name>)
用于包含另一个文件而不进行转义
调用这些方法而不是 include xxx:
语法可能很有用,如果要包含的文件名是动态的(例如存储在变量中)。要包含的文件(无论其类型是模板还是文本)都可以在 classpath 中找到。这是 MarkupTemplateEngine
接受可选 ClassLoader
作为构造函数参数的原因之一(另一个原因是您可以在模板中包含引用其他类的代码)。
如果您不想将模板放在类路径中,MarkupTemplateEngine
提供了一个方便的构造函数,允许您定义模板所在的目录。
片段
片段是嵌套模板。它们可以用于在单个模板中提供改进的组合。片段由一个字符串、内部模板和用于渲染此模板的模型组成。考虑以下模板:
ul {
pages.each {
fragment "li(line)", line:it
}
}
fragment
元素创建了一个嵌套模板,并使用特定于此模板的模型进行渲染。在这里,我们有 li(line)
片段,其中 line
绑定到 it
。由于 it
对应于 pages
的迭代,我们将为模型中的每个页面生成一个 li
元素
<ul><li>Page 1</li><li>Page 2</li></ul>
片段对于模板元素的因子化很有趣。它们的代价是每个模板编译一个片段,并且它们无法外部化。
布局
布局与片段不同,它们引用其他模板。它们可以用于组合模板和共享公共结构。如果您有例如,一个常见的 HTML 页面设置,并且您只希望替换主体,那么这通常很有趣。这可以通过布局轻松完成。首先,您需要创建一个布局模板
html {
head {
title(title) (1)
}
body {
bodyContents() (2)
}
}
1 | title 变量(在 title 标签内)是一个布局变量 |
2 | bodyContents 调用将渲染主体 |
然后您需要一个包含布局的模板
layout 'layout-main.tpl', (1)
title: 'Layout example', (2)
bodyContents: contents { p('This is the body') } (3)
1 | 使用 main-layout.tpl 布局文件 |
2 | 设置 title 变量 |
3 | 设置 bodyContents |
如您所见,由于布局文件中的 bodyContents()
调用,bodyContents
将在布局内部渲染。结果,模板将渲染为:
<html><head><title>Layout example</title></head><body><p>This is the body</p></body></html>
对 contents
方法的调用用于告诉模板引擎,该代码块实际上是模板的规范,而不是要直接渲染的辅助函数。如果您在规范之前不添加 contents
,那么内容将被渲染,但您也会看到一个随机生成的字符串,对应于代码块的结果值。
布局是跨多个模板共享通用元素,而无需重写所有内容或使用包含的强大方式。
默认情况下,布局使用的模型独立于使用它们的页面的模型。但是,也可以使它们继承父模型。假设模型定义如下:
model = new HashMap<String,Object>();
model.put('title','Title from main model');
以及以下模板
layout 'layout-main.tpl', true, (1)
bodyContents: contents { p('This is the body') }
1 | 注意使用 true 来启用模型继承 |
那么就没有必要像上一个示例那样将 title
值传递给布局。结果将是
<html><head><title>Title from main model</title></head><body><p>This is the body</p></body></html>
但也可以覆盖父模型中的值
layout 'layout-main.tpl', true, (1)
title: 'overridden title', (2)
bodyContents: contents { p('This is the body') }
1 | true 表示继承父模型 |
2 | 但 title 被覆盖 |
那么输出将是:
<html><head><title>overridden title</title></head><body><p>This is the body</p></body></html>
渲染内容
创建模板引擎
在服务器端,渲染模板需要 groovy.text.markup.MarkupTemplateEngine
实例和 groovy.text.markup.TemplateConfiguration
TemplateConfiguration config = new TemplateConfiguration(); (1)
MarkupTemplateEngine engine = new MarkupTemplateEngine(config); (2)
Template template = engine.createTemplate("p('test template')"); (3)
Map<String, Object> model = new HashMap<>(); (4)
Writable output = template.make(model); (5)
output.writeTo(writer); (6)
1 | 创建模板配置 |
2 | 使用此配置创建模板引擎 |
3 | 从 String 创建模板实例 |
4 | 创建要在模板中使用的模型 |
5 | 将模型绑定到模板实例 |
6 | 渲染输出 |
解析模板有几种可能的选项
-
从
String
,使用createTemplate(String)
-
从
Reader
,使用createTemplate(Reader)
-
从
URL
,使用createTemplate(URL)
-
给定模板名称,使用
createTemplateByPath(String)
通常应首选最后一个版本
Template template = engine.createTemplateByPath("main.tpl");
Writable output = template.make(model);
output.writeTo(writer);
配置选项
引擎的行为可以通过 TemplateConfiguration
类访问的几个配置选项进行调整
选项 | 默认值 | 描述 | 示例 |
---|---|---|---|
declarationEncoding |
null |
确定调用 |
模板:
输出:
如果 输出:
|
expandEmptyElements |
false |
如果为 true,空标签以其展开形式渲染。 |
模板:
输出:
如果 输出:
|
useDoubleQuotes |
false |
如果为 true,属性使用双引号而不是单引号 |
模板:
输出:
如果 输出:
|
newLineString |
系统默认值(系统属性 |
允许选择渲染新行时使用的字符串 |
模板:
如果 输出:
|
autoEscape |
false |
如果为 true,模型中的变量在渲染前会自动转义。 |
请参阅自动转义部分 |
autoIndent |
false |
如果为 true,在新行后自动缩进 |
请参阅自动格式化部分 |
autoIndentString |
四 (4) 个空格 |
用作缩进的字符串。 |
请参阅自动格式化部分 |
autoNewLine |
false |
如果为 true,根据模板源的原始格式自动插入新行 |
请参阅自动格式化部分 |
baseTemplateClass |
|
设置编译模板的超类。这可用于提供特定于应用程序的模板。 |
请参阅自定义模板部分 |
locale |
默认区域设置 |
设置模板的默认区域设置。 |
请参阅国际化部分 |
模板引擎创建后,更改配置是不安全的。 |
自动格式化
默认情况下,模板引擎将渲染输出而不进行任何特定格式化。一些配置选项可以改善这种情况
-
autoIndent
负责在新行插入后自动缩进 -
autoNewLine
负责根据模板源的原始格式自动插入新行
通常,如果需要可读的、美观的输出,建议将 autoIndent
和 autoNewLine
都设置为 true
config.setAutoNewLine(true);
config.setAutoIndent(true);
使用以下模板
html {
head {
title('Title')
}
}
输出现在将是
<html>
<head>
<title>Title</title>
</head>
</html>
我们可以稍微修改模板,使 title
指令与 head
指令位于同一行
html {
head { title('Title')
}
}
输出将反映这一点
<html>
<head><title>Title</title>
</head>
</html>
新行仅在找到标签的花括号时插入,并且插入位置对应于嵌套内容所在的位置。这意味着另一个标签主体中的标签不会触发新行,除非它们自己使用花括号
html {
head {
meta(attr:'value') (1)
title('Title') (2)
newLine() (3)
meta(attr:'value2') (4)
}
}
1 | 插入新行是因为 meta 与 head 不在同一行 |
2 | 未插入新行,因为我们与前一个标签在同一深度 |
3 | 我们可以通过显式调用 newLine 强制渲染新行 |
4 | 此标签将渲染在新的一行上 |
这次,输出将是
<html>
<head>
<meta attr='value'/><title>Title</title>
<meta attr='value2'/>
</head>
</html>
默认情况下,渲染器使用四个 (4) 个空格作为缩进,但您可以通过设置 TemplateConfiguration#autoIndentString
属性来更改它。
自动转义
默认情况下,从模型读取的内容会原样渲染。如果此内容来自用户输入,则可能很敏感,您可能希望默认将其转义,例如避免 XSS 注入。为此,模板配置提供了一个选项,该选项会自动转义模型中的对象,只要它们继承自 CharSequence
(通常是字符串)。
让我们想象一下以下设置
config.setAutoEscape(false);
model = new HashMap<String,Object>();
model.put("unsafeContents", "I am an <html> hacker.");
以及以下模板
html {
body {
div(unsafeContents)
}
}
那么您不希望 unsafeContents
中的 HTML 原样渲染,因为存在潜在的安全问题
<html><body><div>I am an <html> hacker.</div></body></html>
自动转义将解决此问题
config.setAutoEscape(true);
现在输出已正确转义
<html><body><div>I am an <html> hacker.</div></body></html>
请注意,使用自动转义并不能阻止您包含模型中未转义的内容。为此,您的模板应显式提及不应转义模型变量,方法是在其前面加上 unescaped.
,如本例所示
html {
body {
div(unescaped.unsafeContents)
}
}
常见陷阱
假设您要生成一个包含标记字符串的 <p>
标签
p {
yield "This is a "
a(href:'target.html', "link")
yield " to another page"
}
并生成
<p>This is a <a href='target.html'>link</a> to another page</p>
这不能写得更短吗?一个天真的替代方案是
p {
yield "This is a ${a(href:'target.html', "link")} to another page"
}
但结果不会像预期的那样
<p><a href='target.html'>link</a>This is a to another page</p>
原因是标记模板引擎是一个流式引擎。在原始版本中,第一次 yield
调用生成一个流式输出的字符串,然后生成并流式输出 a
链接,然后流式输出最后一次 yield
调用,导致按顺序执行。但是对于上面的字符串版本,执行顺序不同
-
yield
调用需要一个参数,一个字符串 -
该参数需要在生成yield调用之前进行评估
因此,评估字符串会导致 a(href:…)
调用在 yield
本身被调用之前执行。这不是您想要做的。相反,您想要生成一个包含标记的字符串,然后将其传递给 yield
调用。这可以通过这种方式完成
p("This is a ${stringOf {a(href:'target.html', "link")}} to another page")
请注意 stringOf
调用,它基本上告诉标记模板引擎底层标记需要单独渲染并导出为字符串。请注意,对于简单表达式,stringOf
可以替换为以美元符号开头的替代标签符号
p("This is a ${$a(href:'target.html', "link")} to another page")
值得注意的是,使用 stringOf 或特殊的 $tag 符号会触发创建单独的字符串写入器,然后用于渲染标记。这比使用直接流式传输标记的 yield 调用版本要慢。 |
国际化
模板引擎对国际化有原生支持。为此,当您创建 TemplateConfiguration
时,您可以提供一个 Locale
,它是用于模板的默认区域设置。每个模板可能有不同的版本,每个区域设置一个。模板的名称有所不同
-
file.tpl
:默认模板文件 -
file_fr_FR.tpl
:模板的法文版本 -
file_en_US.tpl
:模板的美式英文版本 -
…
当模板被渲染或包含时,则
-
如果模板名称或包含名称显式设置了区域设置,则包含特定版本,如果找不到则包含默认版本
-
如果模板名称不包含区域设置,则使用
TemplateConfiguration
区域设置的版本,如果找不到则使用默认版本
例如,假设默认区域设置设置为 Locale.ENGLISH
,并且主模板包含
include template: 'locale_include_fr_FR.tpl'
然后使用特定模板渲染模板
Texte en français
使用不指定区域设置的包含将使模板引擎查找具有配置区域设置的模板,如果没有,则回退到默认模板,如下所示
include template: 'locale_include.tpl'
Default text
但是,将模板引擎的默认区域设置更改为 Locale.FRANCE
将更改输出,因为模板引擎现在将查找带有 fr_FR
区域设置的文件
Texte en français
此策略允许您逐个翻译模板,通过依赖默认模板,这些模板的文件名中未设置区域设置。
自定义模板类
默认情况下,创建的模板继承 groovy.text.markup.BaseTemplate
类。为应用程序提供不同的模板类可能很有趣,例如提供了解应用程序的附加帮助方法,或自定义渲染基元(例如 HTML)。
模板引擎通过在 TemplateConfiguration
中设置替代的 baseTemplateClass
来提供此功能
config.setBaseTemplateClass(MyTemplate.class);
自定义基类必须扩展 BaseClass
,如本例所示
public abstract class MyTemplate extends BaseTemplate {
private List<Module> modules
public MyTemplate(
final MarkupTemplateEngine templateEngine,
final Map model,
final Map<String, String> modelTypes,
final TemplateConfiguration configuration) {
super(templateEngine, model, modelTypes, configuration)
}
List<Module> getModules() {
return modules
}
void setModules(final List<Module> modules) {
this.modules = modules
}
boolean hasModule(String name) {
modules?.any { it.name == name }
}
}
此示例显示了一个提供名为 hasModule
的附加方法的类,该方法可以直接在模板中使用
if (hasModule('foo')) {
p 'Found module [foo]'
} else {
p 'Module [foo] not found'
}
类型检查模板
可选类型检查
即使模板未进行类型检查,它们也是静态编译的。这意味着一旦模板编译完成,性能应该非常好。对于某些应用程序,确保模板在实际渲染之前有效可能是一个好主意。这意味着,例如,如果模型变量上的方法不存在,则模板编译失败。
MarkupTemplateEngine
提供了这样的功能。模板可以可选地进行类型检查。为此,开发人员必须在模板创建时提供附加信息,即模型中变量的类型。想象一个公开页面列表的模型,其中页面定义为
public class Page {
Long id
String title
String body
}
然后可以在模型中公开页面列表,如下所示
Page p = new Page();
p.setTitle("Sample page");
p.setBody("Page body");
List<Page> pages = new LinkedList<>();
pages.add(p);
model = new HashMap<String,Object>();
model.put("pages", pages);
模板可以轻松使用它
pages.each { page -> (1)
p("Page title: $page.title") (2)
p(page.text) (3)
}
1 | 迭代模型中的页面 |
2 | page.title 有效 |
3 | page.text 无效(应为 page.body ) |
如果没有类型检查,模板的编译会成功,因为模板引擎在页面实际渲染之前不知道模型。这意味着问题只会在运行时,即页面渲染后才浮现
No such property: text
在某些情况下,这可能很难解决甚至发现。通过向模板引擎声明 pages
的类型,我们现在能够在编译时失败
modelTypes = new HashMap<String,String>(); (1)
modelTypes.put("pages", "List<Page>"); (2)
Template template = engine.createTypeCheckedModelTemplate("main.tpl", modelTypes) (3)
1 | 创建一个将保存模型类型的映射 |
2 | 声明 pages 变量的类型(注意类型使用字符串) |
3 | 使用 createTypeCheckedModelTemplate 而不是 createTemplate |
这次,当模板在最后一行编译时,会发生错误
[Static type checking] - No such property: text for class: Page
这意味着您不需要等到页面渲染后才能看到错误。使用 createTypeCheckedModelTemplate
是强制性的。
类型声明的替代方法
或者,如果开发人员也是模板编写者,则可以直接在模板中声明预期变量的类型。在这种情况下,即使您调用 createTemplate
,它也会进行类型检查
modelTypes = { (1)
List<Page> pages (2)
}
pages.each { page ->
p("Page title: $page.title")
p(page.text)
}
1 | 类型需要在 modelTypes 头部声明 |
2 | 为模型中的每个对象声明一个变量 |
类型检查模板的性能
使用类型检查模型的一个额外好处是性能应该会提高。通过告诉类型检查器预期的类型,您还可以让编译器为其生成优化代码,因此如果您正在寻找最佳性能,请考虑使用类型检查模板。
3.18.8. 其他解决方案
此外,还有其他可与 Groovy 一起使用的模板解决方案,例如 FreeMarker、Velocity、StringTemplate 等。
3.19. Servlet 支持
您可以使用 Groovy 编写 (Java) Servlet(称为 Groovlets)。
还有一个 GroovyServlet
。
此功能将自动编译您的 .groovy 源文件,将其转换为字节码,加载类并将其缓存直到您更改源文件。
这是一个简单的示例,向您展示了 Groovlet 可以执行的操作。
请注意使用隐式变量访问会话、输出和请求。另请注意,这更像一个脚本,因为它没有类包装器。
if (!session) {
session = request.getSession(true)
}
if (!session.counter) {
session.counter = 1
}
println """
<html>
<head>
<title>Groovy Servlet</title>
</head>
<body>
<p>
Hello, ${request.remoteHost}: ${session.counter}! ${new Date()}
</p>
</body>
</html>
"""
session.counter = session.counter + 1
或者,使用 MarkupBuilder 执行相同的操作
if (!session) {
session = request.getSession(true)
}
if (!session.counter) {
session.counter = 1
}
html.html { // html is implicitly bound to new MarkupBuilder(out)
head {
title('Groovy Servlet')
}
body {
p("Hello, ${request.remoteHost}: ${session.counter}! ${new Date()}")
}
}
session.counter = session.counter + 1
3.19.1. 隐式变量
以下变量可在 Groovlet 中使用
变量名 | 绑定到 | 注意 |
---|---|---|
request |
ServletRequest |
- |
response |
ServletResponse |
- |
context |
ServletContext |
- |
application |
ServletContext |
- |
session |
getSession(false) |
可能为 null!参见 <1> |
params |
一个 Map 对象 |
|
headers |
一个 Map 对象 |
|
out |
response.getWriter() |
参见 <2> |
sout |
response.getOutputStream() |
参见 <2> |
html |
new MarkupBuilder(out) |
参见 <2> |
json |
new StreamingJsonBuilder(out) |
参见 <2> |
-
只有在会话对象已经存在的情况下,才会设置会话变量。请参阅上面示例中的
if (session == null)
检查。 -
这些变量不能在
Groovlet
内部重新赋值。它们在首次访问时绑定,允许例如在使用out
之前调用response
对象上的方法。
3.19.2. 设置 Groovlets
将以下内容添加到您的 web.xml
中
<servlet>
<servlet-name>Groovy</servlet-name>
<servlet-class>groovy.servlet.GroovyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Groovy</servlet-name>
<url-pattern>*.groovy</url-pattern>
</servlet-mapping>
然后将所需的 groovy jar 文件放入 WEB-INF/lib
。
现在将 .groovy 文件放入,例如,根目录(即您放置 html 文件的位置)。GroovyServlet
负责编译 .groovy 文件。
因此,例如使用 tomcat,您可以像这样编辑 tomcat/conf/server.xml
<Context path="/groovy" docBase="c:/groovy-servlet"/>
然后通过 https://:8080/groovy/hello.groovy 访问它
3.20. 在 Java 应用程序中集成 Groovy
3.20.1. Groovy 集成机制
Groovy 语言提供了几种在运行时将其集成到应用程序(Java 甚至 Groovy)中的方法,从最基本、简单的代码执行到最完整、集成缓存和编译器定制。
本节中的所有示例都使用 Groovy 编写,但相同的集成机制也可以从 Java 使用。 |
评估
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
多个来源
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 方法 |
脚本与应用程序之间的数据共享
可以使用 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 | 向绑定添加一个日期(您不限于简单类型) |
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
或 explicit
类型会失败,因为那样会创建局部变量
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"
}
在多线程环境中使用共享数据时必须非常小心。您传递给 GroovyShell 的 Binding 实例是非线程安全的,并且由所有脚本共享。 |
可以通过利用 parse
返回的 Script
实例来解决 Binding
的共享实例问题
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 | 会将 x 变量存储在 b1 中 |
2 | 会将 x 变量存储在 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 | 为线程 1 创建脚本实例 |
2 | 为线程 2 创建脚本实例 |
3 | 将第一个绑定分配给脚本 1 |
4 | 将第二个绑定分配给脚本 2 |
5 | 在单独的线程中启动第一个脚本 |
6 | 在单独的线程中启动第二个脚本 |
7 | 等待完成 |
如果像这里这样需要线程安全,则更建议直接使用 GroovyClassLoader。
自定义脚本类
我们已经看到 parse
方法返回一个 groovy.lang.Script
实例,但可以使用自定义类,前提是它本身扩展了 Script
。它可用于为脚本提供额外的行为,如下例所示
abstract class MyScript extends Script {
String name
String greet() {
"Hello, $name!"
}
}
自定义类定义了一个名为 name
的属性和一个名为 greet
的新方法。此可以通过使用自定义配置作为脚本基类
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 | 指示它使用 MyScript 作为脚本的基类 |
3 | 然后在创建 shell 时使用编译器配置 |
4 | 脚本现在可以访问新方法 greet |
您不限于单独的scriptBaseClass配置。您可以使用任何编译器配置调整,包括编译定制器。 |
GroovyClassLoader
在上一节中,我们展示了 GroovyShell
是一个易于执行脚本的工具,但它使编译除了脚本之外的任何东西都变得复杂。在内部,它使用了 groovy.lang.GroovyClassLoader
,它是运行时类编译和加载的核心。
通过利用 GroovyClassLoader
而不是 GroovyShell
,您将能够加载类,而不是脚本实例
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 | 创建新的 GroovyClassLoader |
2 | parseClass 将返回一个 Class 实例 |
3 | 您可以检查返回的类是否确实是脚本中定义的类 |
4 | 您可以创建该类的新实例,该实例不是脚本 |
5 | 然后在其上调用任何方法 |
GroovyClassLoader 维护它创建的所有类的引用,因此很容易造成内存泄漏。特别是,如果您两次执行同一个脚本,如果它是字符串,则会获得两个不同的类! |
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 | 动态创建一个名为“Foo”的类 |
2 | 使用单独的 parseClass 调用创建一个看起来相同的类 |
3 | 确保两个类具有相同的名称 |
4 | 但它们实际上是不同的! |
原因是 GroovyClassLoader
不会跟踪源文本。如果您想要相同的实例,那么源必须是一个文件,如本例所示
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 | 从 File 解析类 |
2 | 从不同的文件实例解析类,但指向同一个物理文件 |
3 | 确保我们的类具有相同的名称 |
4 | 但现在,它们是同一个实例 |
使用 File
作为输入,GroovyClassLoader
能够缓存生成的类文件,从而避免在运行时为同一源创建多个类。
GroovyScriptEngine
groovy.util.GroovyScriptEngine
类为依赖脚本重载和脚本依赖的应用程序提供了灵活的基础。虽然 GroovyShell
专注于独立的 Script
,而 GroovyClassLoader
处理任何 Groovy 类的动态编译和加载,但 GroovyScriptEngine
将在 GroovyClassLoader
之上添加一个层来处理脚本依赖和重载。
为了说明这一点,我们将创建一个脚本引擎并在无限循环中执行代码。首先,您需要在目录中创建以下脚本
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 | 创建一个脚本引擎,它将在我们的源目录中查找源 |
2 | 执行脚本,它将返回 Greeter 的实例 |
3 | 打印问候消息 |
此时,您应该每秒看到一条消息
Hello, world! Hello, world! ...
不中断脚本执行,现在将 ReloadingTest
文件的内容替换为
class Greeter {
String sayHello() {
def greet = "Hello, Groovy!"
greet
}
}
new Greeter()
消息应更改为
Hello, world! ... Hello, Groovy! Hello, Groovy! ...
但是也可以依赖另一个脚本。为了说明这一点,请在同一目录中创建以下文件,而不要中断正在执行的脚本
class Dependency {
String message = 'Hello, dependency 1'
}
并像这样更新 ReloadingTest
脚本
import Dependency
class Greeter {
String sayHello() {
def greet = new Dependency().message
greet
}
}
new Greeter()
这次,消息应更改为
Hello, Groovy! ... Hello, dependency 1! Hello, dependency 1! ...
作为最后一次测试,您可以更新 Dependency.groovy
文件,而无需触及 ReloadingTest
文件
class Dependency {
String message = 'Hello, dependency 2'
}
您应该会观察到依赖文件已重新加载
Hello, dependency 1! ... Hello, dependency 2! Hello, dependency 2!
CompilationUnit
最终,通过直接依赖 org.codehaus.groovy.control.CompilationUnit
类,可以在编译期间执行更多操作。该类负责确定编译的各个步骤,并允许您引入新步骤,甚至在各个阶段停止编译。例如,存根生成就是这样完成的,用于联合编译器。
但是,不建议覆盖 CompilationUnit
,只有在没有其他标准解决方案起作用时才应这样做。
3.20.2. JSR 223 javax.script API
JSR-223 是用于在 Java 中调用脚本框架的标准 API。它自 Java 6 起可用,旨在提供一个通用框架,用于从 Java 调用多种语言。Groovy 提供了自己更丰富的集成机制,如果您不打算在同一个应用程序中使用多种语言,建议您使用 Groovy 集成机制而不是有限的 JSR-223 API。 |
以下是您需要初始化 JSR-223 引擎以从 Java 与 Groovy 通信的方法
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
...
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("groovy");
然后您可以轻松执行 Groovy 脚本
Integer sum = (Integer) engine.eval("(1..10).sum()");
assertEquals(Integer.valueOf(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(Integer.valueOf(120), result);
引擎默认保留对脚本函数的硬引用。要更改此设置,您应该将一个引擎级别作用域属性设置为脚本上下文,名称为 #jsr223.groovy.engine.keep.globals
,值为字符串,phantom
表示使用虚引用,weak
表示使用弱引用,soft
表示使用软引用——大小写不敏感。任何其他字符串都将导致使用硬引用。
3.21. 领域特定语言
3.21.1. 命令链
Groovy 允许您在顶级语句的方法调用参数周围省略括号。“命令链”功能通过允许我们链接此类无括号方法调用来扩展此功能,既不需要参数周围的括号,也不需要链接调用之间的点。总的来说,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
如果您的命令链包含奇数个元素,则该链将由方法/参数组成,并以最终属性访问结束
// equivalent to: take(3).cookies
// and also this: take(3).getCookies()
take 3 cookies
这种命令链方法在可以以 Groovy 编写的更广泛的 DSL 方面开辟了有趣的可能性。
以上示例说明了如何使用基于命令链的 DSL,但没有说明如何创建它。您可以使用各种策略,但为了说明如何创建此类 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
,因为它已经有了一个很好的流畅 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 开发人员来说,它读起来相当好,但如果这不是您的目标受众,或者您需要编写许多这样的语句,它可能会被认为有点冗长。同样,编写 DSL 有很多选择。我们将使用 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 = Splitter.on(',').trimResults(CharMatcher.is('_' as char)).split("_a ,_b_ ,c__").iterator().toList()
我们可以这样写
def result = split "_a ,_b_ ,c__" on ',' trimming '_\'
3.21.2. 运算符重载
Groovy 中的各种运算符都映射到对象的常规方法调用。
这允许您提供自己的 Java 或 Groovy 对象,这些对象可以利用运算符重载。下表描述了 Groovy 中支持的运算符及其映射到的方法。
运算符 | 方法 |
---|---|
|
a.plus(b) |
|
a.minus(b) |
|
a.multiply(b) |
|
a.power(b) |
|
a.div(b) |
|
a.mod(b) |
|
a.or(b) |
|
a.and(b) |
|
a.xor(b) |
|
a.next() |
|
a.previous() |
|
a.getAt(b) |
|
a.putAt(b, c) |
|
a.leftShift(b) |
|
a.rightShift(b) |
|
a.rightShiftUnsigned(b) |
|
b.isCase(a) |
|
a.asBoolean() |
|
a.bitwiseNegate() |
|
a.negative() |
|
a.positive() |
|
a.asType(b) |
|
a.equals(b) |
|
! a.equals(b) |
|
a.compareTo(b) |
|
a.compareTo(b) > 0 |
|
a.compareTo(b) >= 0 |
|
a.compareTo(b) < 0 |
|
a.compareTo(b) <= 0 |
3.21.3. 脚本基类
脚本类
Groovy 脚本总是编译成类。例如,一个简单的脚本,例如
println 'Hello from Groovy'
被编译成一个扩展抽象 groovy.lang.Script 类的类。该类包含一个名为 run 的抽象方法。当脚本被编译时,其主体将成为 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 | 绑定用于在脚本和调用类之间共享数据 |
2 | GroovyShell 可以与此绑定一起使用 |
3 | 输入变量从调用类内部设置到绑定中 |
4 | 然后评估脚本 |
5 | 并且 z 变量已“导出”到绑定中 |
这是一种在调用者和脚本之间共享数据的非常实用的方法,但在某些情况下可能不足或不实用。为此,Groovy 允许您设置自己的基本脚本类。基本脚本类必须扩展 groovy.lang.Script 并且是单个抽象方法类型
abstract class MyBaseClass extends Script {
String name
public void greet() { println "Hello, $name!" }
}
然后可以在编译器配置中声明自定义脚本基类,例如
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 | 创建自定义编译器配置 |
2 | 将基脚本类设置为我们的自定义基脚本类 |
3 | 然后使用该配置创建 GroovyShell |
4 | 脚本将扩展基脚本类,直接访问 name 属性和 greet 方法 |
@BaseScript 注解
作为替代,也可以直接在脚本中使用 @BaseScript
注解
import groovy.transform.BaseScript
@BaseScript MyBaseClass baseScript
setName 'Judith'
greet()
其中 @BaseScript
应该注解一个变量,其类型是基脚本的类。或者,您可以将基脚本类设置为 @BaseScript
注解本身的成员
@BaseScript(MyBaseClass)
import groovy.transform.BaseScript
setName 'Judith'
greet()
备用抽象方法
我们已经看到基脚本类是一个需要实现 run
方法的单一抽象方法类型。run
方法由脚本引擎自动执行。在某些情况下,拥有一个实现 run
方法但提供用于脚本主体的替代抽象方法的基类可能很有趣。例如,基脚本 run
方法可能在 run
方法执行之前执行一些初始化。这可以通过以下方式实现
abstract class MyBaseClass extends Script {
int count
abstract void scriptBody() (1)
def run() {
count++ (2)
scriptBody() (3)
count (4)
}
}
1 | 基脚本类应定义一个(且只有一个)抽象方法 |
2 | run 方法可以被重写并在执行脚本主体之前执行任务 |
3 | run 调用抽象的 scriptBody 方法,该方法将委托给用户脚本 |
4 | 然后它可以返回除脚本值之外的其他内容 |
如果您执行此代码
def result = shell.evaluate """
println 'Ok'
"""
assert result == 1
那么您将看到脚本已执行,但评估结果为 1
,由基类的 run
方法返回。如果您使用 parse
而不是 evaluate
,则会更清楚,因为它允许您在同一个脚本实例上多次执行 run
方法
def script = shell.parse("println 'Ok'")
assert script.run() == 1
assert script.run() == 2
3.21.4. 向数字添加属性
在 Groovy 中,数字类型被视为与任何其他类型相等。因此,可以通过向数字添加属性或方法来增强它们。例如,在处理可测量数量时,这非常方便。有关如何在 Groovy 中增强现有类的详细信息,请参阅扩展模块部分或类别部分。
在 Groovy 中使用 TimeCategory
可以找到这方面的示例
use(TimeCategory) {
println 1.minute.from.now (1)
println 10.hours.ago
def someDate = new Date() (2)
println someDate - 3.months
}
1 | 使用 TimeCategory ,向 Integer 类添加了一个属性 minute |
2 | 类似地,months 方法返回一个 groovy.time.DatumDependentDuration ,它可以在计算中使用 |
类别是词法绑定的,这使得它们非常适合内部 DSL。
3.21.5. @DelegatesTo
编译时解释委托策略
@groovy.lang.DelegatesTo
是一个文档和编译时注解,旨在
-
文档化使用闭包作为参数的 API
-
为静态类型检查器和编译器提供类型信息
Groovy 语言是构建 DSL 的首选平台。使用闭包,创建自定义控制结构非常容易,同时创建构建器也很简单。想象一下您有以下代码
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
值。在这里设置所有者和 this
对象并不是很重要,因为我们将使用 DELEGATE_ONLY
策略,该策略表示方法调用将仅针对闭包的委托进行解析。
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 中所说的构建器模式。
我们展示的代码的一个问题是,email
方法的用户无法获得关于他可以在闭包中调用的方法的任何信息。唯一可能的信息来自方法文档。这有两个问题:首先,文档不总是编写的,如果编写了,它也不总是可用的(例如,javadoc 未下载)。其次,它对 IDE 没有帮助。这里真正有趣的是,IDE 可以在开发人员进入闭包主体后,通过建议 email
类上存在的方法来帮助开发人员。
此外,如果用户在闭包中调用一个未由 EmailSpec
类定义的方法,IDE 至少应发出警告(因为它很可能在运行时中断)。
上述代码还有一个问题,它与静态类型检查不兼容。类型检查可以让用户在编译时而不是运行时知道方法调用是否被授权,但如果您尝试对此代码执行类型检查
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
那么类型检查器将知道存在一个接受 Closure
的 email
方法,但它将抱怨闭包内部的每个方法调用,因为例如 from
不是类中定义的方法。事实上,它是在 EmailSpec
类中定义的,并且它完全没有任何提示来帮助它知道闭包委托将在运行时是 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!'
}
}
}
编译将失败,并出现如下错误
[Static type checking] - Cannot find matching method MyScript#from(java.lang.String). Please check if the declared type is correct and if the method exists. @ line 31, column 21. from 'dsl-guru@mycompany.com'
@DelegatesTo
由于这些原因,Groovy 2.1 引入了一个名为 @DelegatesTo
的新注解。此注解的目标是解决文档问题(让您的 IDE 了解闭包主体中预期的方法),它还将通过向编译器提供有关闭包主体中方法调用的潜在接收者的提示来解决类型检查问题。
想法是注解 email
方法的 Closure
参数
def email(@DelegatesTo(EmailSpec) Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
我们在这里所做的是告诉编译器(或 IDE),当使用闭包调用该方法时,该闭包的委托将设置为 email
类型的一个对象。但仍然存在一个问题:默认的委托策略不是我们方法中使用的策略。因此,我们将提供更多信息并告诉编译器(或 IDE)委托策略也已更改
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()
}
现在,IDE 和类型检查器(如果您正在使用 @TypeChecked
)都将了解委托和委托策略。这非常好,因为它既允许 IDE 提供智能完成,又可以消除编译时只因为程序行为通常只在运行时才知道而存在的错误!
以下代码现在将通过编译
@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 模式
@DelegatesTo
支持多种模式,我们将在本节中通过示例进行描述。
简单委托
在此模式中,唯一强制的参数是 value,它表示我们将调用委托给哪个类。没有更多了。我们告诉编译器,委托的类型将始终是 @DelegatesTo
文档中记录的类型(请注意,它可以是子类,但如果是子类,子类定义的方法将对类型检查器不可见)。
void body(@DelegatesTo(BodySpec) Closure cl) {
// ...
}
委托策略
在此模式中,您必须同时指定委托类和委托策略。如果闭包不使用默认委托策略 Closure.OWNER_FIRST
,则必须使用此模式。
void body(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=BodySpec) Closure cl) {
// ...
}
委托给参数
在此变体中,我们将告诉编译器我们将委托给方法的另一个参数。请看以下代码
def exec(Object target, Closure code) {
def clone = code.rehydrate(target, this, this)
clone()
}
在这里,将使用的委托不是在 exec
方法内部创建的。事实上,我们接受了方法的一个参数并将其委托。用法可能如下所示
def email = new Email()
exec(email) {
from '...'
to '...'
send()
}
每个方法调用都委托给 email
参数。这是一种广泛使用的模式,@DelegatesTo
也通过伴随注解支持此模式
def exec(@DelegatesTo.Target Object target, @DelegatesTo Closure code) {
def clone = code.rehydrate(target, this, this)
clone()
}
一个闭包使用 @DelegatesTo
进行注解,但这次没有指定任何类。相反,我们用 @DelegatesTo.Target
注解了另一个参数。然后委托的类型在编译时确定。有人可能会认为我们正在使用参数类型,在这种情况下是 Object
,但事实并非如此。看这段代码
class Greeter {
void sayHello() { println 'Hello' }
}
def greeter = new Greeter()
exec(greeter) {
sayHello()
}
请记住,这在开箱即用时无需使用 @DelegatesTo
注解即可工作。但是,为了让 IDE 了解委托类型,或者类型检查器了解它,我们需要添加 @DelegatesTo
。在这种情况下,它将知道 Greeter
变量的类型是 Greeter
,因此即使 exec
方法没有明确将目标定义为 Greeter
类型,它也不会在 sayHello 方法上报告错误。这是一个非常强大的功能,因为它避免了您为不同的接收器类型编写多个版本的相同 exec
方法!
在此模式下,@DelegatesTo
注解也支持我们上面描述的 strategy
参数。
多个闭包
在前面的例子中,exec
方法只接受一个闭包,但您可能有接受多个闭包的方法
void fooBarBaz(Closure foo, Closure bar, Closure baz) {
...
}
那么没有什么能阻止您用 @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) {
...
}
但更重要的是,如果您有多个闭包和多个参数,则可以使用多个目标
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()) }
)
此时,您可能想知道为什么我们不使用参数名称作为引用。原因是信息(参数名称)并非总是可用(它是仅调试信息),因此这是 JVM 的一个限制。 |
委托给泛型类型
在某些情况下,指示 IDE 或编译器委托类型不是参数而是泛型类型会很有趣。想象一个在元素列表上运行的配置器
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()
}
}
然后可以用任何列表调用此方法,如下所示
@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' }
为了让类型检查器和 IDE 知道 configure
方法在列表的每个元素上调用闭包,您需要以不同的方式使用 @DelegatesTo
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
接受一个可选的 genericTypeIndex
参数,该参数指示将用作委托类型的泛型类型的索引。这必须与 @DelegatesTo.Target
结合使用,并且索引从 0 开始。在上面的示例中,这意味着委托类型是针对 List<T>
解析的,并且由于索引 0 处的泛型类型是 T
并推断为 Realm
,因此类型检查器推断委托类型将是 Realm
类型。
我们使用 genericTypeIndex 而不是占位符(T )是由于 JVM 的限制。 |
委托给任意类型
您可能无法通过上述任何选项来表示您想要委托的类型。例如,让我们定义一个用一个对象参数化的映射器类,并定义一个返回另一个类型对象的方法
class Mapper<T,U> { (1)
final T value (2)
Mapper(T value) { this.value = value }
U map(Closure<U> producer) { (3)
producer.delegate = value
producer()
}
}
1 | 映射器类接受两个泛型类型参数:源类型和目标类型 |
2 | 源对象存储在最终字段中 |
3 | map 方法要求将源对象转换为目标对象 |
如您所见,map
方法签名没有提供任何关于闭包将操作哪个对象的信息。阅读方法体,我们知道它将是类型为 T
的 value
,但 T
不在方法签名中,所以我们面临一个 @DelegatesTo
的可用选项都不适合的情况。例如,如果我们尝试静态编译此代码
def mapper = new Mapper<String,Integer>('Hello')
assert mapper.map { length() } == 5
那么编译器将失败,并显示
Static type checking] - Cannot find matching method TestScript0#length()
在这种情况下,您可以使用 @DelegatesTo
注解的 type
成员将 T
引用为类型标记
class Mapper<T,U> {
final T value
Mapper(T value) { this.value = value }
U map(@DelegatesTo(type="T") Closure<U> producer) { (1)
producer.delegate = value
producer()
}
}
1 | @DelegatesTo 注解引用了一个不在方法签名中的泛型类型 |
请注意,您不限于泛型类型标记。type
成员可用于表示复杂类型,例如 List<T>
或 Map<T,List<U>>
。您应该将其作为最后手段的原因是,类型仅在类型检查器找到 @DelegatesTo
的用法时进行检查,而不是在注解方法本身编译时进行检查。这意味着类型安全仅在调用站点得到保证。此外,编译速度会较慢(尽管在大多数情况下可能不明显)。
3.21.6. 编译定制器
介绍
无论您是使用 groovyc
编译类还是使用 GroovyShell
(例如执行脚本),其底层都使用编译器配置。此配置包含诸如源编码或类路径之类的信息,但它也可以用于执行更多操作,例如默认添加导入、透明应用 AST 转换或禁用全局 AST 转换。
编译定制器的目标是使这些常见任务易于实现。为此,CompilerConfiguration
类是入口点。通用模式将始终基于以下代码
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)
编译定制器必须扩展 org.codehaus.groovy.control.customizers.CompilationCustomizer 类。定制器工作
-
在特定的编译阶段
-
在编译的每个类节点上
您可以实现自己的编译定制器,但 Groovy 包含一些最常见的操作。
导入定制器
使用此编译定制器,您的代码将透明地添加导入。这对于实现 DSL 的脚本特别有用,您希望避免用户必须编写导入。导入定制器将允许您添加 Groovy 语言允许的所有导入变体,即
-
类导入,可选别名
-
星号导入
-
静态导入,可选别名
-
静态星号导入
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.*
所有快捷方式的详细描述可在 org.codehaus.groovy.control.customizers.ImportCustomizer 中找到
AST 转换定制器
AST 转换定制器旨在透明地应用 AST 转换。与全局 AST 转换不同,后者只要在类路径上找到转换就会应用于每个编译的类(这有缺点,例如增加编译时间或由于在不应应用转换的地方应用转换而产生副作用),定制器将允许您仅针对特定脚本或类选择性地应用转换。
举个例子,假设您想在脚本中使用 @Log
。问题是 @Log
通常应用于类节点,而脚本从定义上来说不需要类节点。但在实现方面,脚本是类,只是您不能用 @Log
注解这个隐式类节点。使用 AST 定制器,您可以有一个变通方法来实现它
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import groovy.util.logging.Log
def acz = new ASTTransformationCustomizer(Log)
config.addCompilationCustomizers(acz)
就是这样!在内部,@Log
AST 转换将应用于编译单元中的每个类节点。这意味着它将应用于脚本,也将应用于脚本中定义的类。
如果您使用的 AST 转换接受参数,您也可以在构造函数中使用参数
def acz = new ASTTransformationCustomizer(Log, value: 'LOGGER')
// use name 'LOGGER' instead of the default 'log'
config.addCompilationCustomizers(acz)
由于 AST 转换定制器使用对象而不是 AST 节点,因此并非所有值都可以转换为 AST 转换参数。例如,基本类型转换为 ConstantExpression
(即 LOGGER
转换为 new ConstantExpression('LOGGER')
),但如果您的 AST 转换接受闭包作为参数,则必须为其提供一个 ClosureExpression
,如以下示例所示
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()
""")
}
安全 AST 定制器
此定制器将允许 DSL 的开发人员限制语言的语法,例如,防止用户使用特定构造。它只在这一方面是“安全”的,即限制 DSL 中允许的构造。它不取代可能作为整体安全的正交方面额外需要的安全管理器。它存在的唯一原因是限制语言的表达能力。此定制器仅在 AST(抽象语法树)级别工作,而不是在运行时工作!乍一看可能很奇怪,但如果您将 Groovy 视为构建 DSL 的平台,则会更有意义。您可能不希望用户拥有完整的语言。在下面的示例中,我们将通过一个只允许算术运算的语言示例来演示它,但此定制器允许您
-
允许/禁止创建闭包
-
允许/禁止导入
-
允许/禁止包定义
-
允许/禁止方法定义
-
限制方法调用的接收者
-
限制用户可以使用的 AST 表达式类型
-
限制用户可以使用的令牌(语法上)
-
限制代码中可以使用的常量的类型
对于所有这些功能,安全 AST 定制器使用允许列表(允许的元素列表)或禁止列表(不允许的元素列表)。对于每种类型的功能(导入、令牌等),您可以选择使用允许列表或禁止列表,但您可以将禁止/允许列表混合用于不同的功能。通常,您会选择允许列表(只允许列出的构造,禁止所有其他构造)。
import org.codehaus.groovy.control.customizers.SecureASTCustomizer
import static org.codehaus.groovy.syntax.Types.* (1)
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
allowedImports = [] // empty allowed list means imports are disallowed
allowedStaticImports = [] // same for static imports
allowedStaticStarImports = ['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
allowedTokens = [ (1)
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
allowedConstantTypesClasses = [ (2)
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!
allowedReceiversClasses = [ (2)
Math,
Integer,
Float,
Double,
Long,
BigDecimal
].asImmutable()
}
1 | 用于 org.codehaus.groovy.syntax.Types 中的令牌类型 |
2 | 您可以在这里使用类字面量 |
如果安全 AST 定制器开箱即用的功能不足以满足您的需求,那么在创建自己的编译定制器之前,您可能对 AST 定制器支持的表达式和语句检查器感兴趣。基本上,它允许您在 AST 树上添加自定义检查,对表达式(表达式检查器)或语句(语句检查器)进行检查。为此,您必须实现 org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementChecker
或 org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker
。
这些接口定义了一个名为 isAuthorized
的单一方法,返回一个布尔值,并接受一个 Statement
(或 Expression
)作为参数。它允许您对表达式或语句执行复杂逻辑,以判断用户是否被允许执行此操作。
例如,定制器中没有预定义的配置标志可以阻止人们使用属性表达式。使用自定义检查器,这很简单
def scz = new SecureASTCustomizer()
def checker = { expr ->
!(expr instanceof AttributeExpression)
} as SecureASTCustomizer.ExpressionChecker
scz.addExpressionCheckers(checker)
然后我们可以通过评估一个简单的脚本来确保它有效
new GroovyShell(config).evaluate '''
class A {
int val
}
def a = new A(val: 123)
a.@val (1)
'''
1 | 编译将失败 |
源感知定制器
此定制器可用作其他定制器上的过滤器。在这种情况下,过滤器是 org.codehaus.groovy.control.SourceUnit
。为此,源感知定制器将另一个定制器作为委托,并且只有当源单元上的谓词匹配时,它才会应用该委托的定制。
SourceUnit
允许您访问多种内容,尤其是正在编译的文件(当然,如果是从文件编译)。它为您提供了根据文件名执行操作的潜力,例如。以下是创建源感知定制器的方法
import org.codehaus.groovy.control.customizers.SourceAwareCustomizer
import org.codehaus.groovy.control.customizers.ImportCustomizer
def delegate = new ImportCustomizer()
def sac = new SourceAwareCustomizer(delegate)
然后您可以在源感知定制器上使用谓词
// 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') }
定制器构建器
如果您在 Groovy 代码中使用编译定制器(如上例所示),则可以使用替代语法来自定义编译。一个构建器(org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder
)通过分层 DSL 简化了定制器的创建。
import org.codehaus.groovy.control.CompilerConfiguration
import static org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder.withConfig (1)
def conf = new CompilerConfiguration()
withConfig(conf) {
// ... (2)
}
1 | 构建器方法的静态导入 |
2 | 配置在此处 |
上面的代码示例展示了如何使用构建器。一个静态方法 withConfig 接受一个对应于构建器代码的闭包,并自动将编译定制器注册到配置中。分发中可用的每个编译定制器都可以通过这种方式进行配置
导入定制器
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
}
}
AST 转换定制器
withConfig(conf) {
ast(Log) (1)
}
withConfig(conf) {
ast(Log, value: 'LOGGER') (2)
}
1 | 透明地应用 @Log |
2 | 使用不同的记录器名称应用 @Log |
安全 AST 定制器
withConfig(conf) {
secureAst {
closuresAllowed = false
methodDefinitionAllowed = false
}
}
源感知定制器
withConfig(configuration){
source(extension: 'sgroovy') {
ast(CompileStatic) (1)
}
}
withConfig(configuration){
source(extensions: ['sgroovy','sg']) {
ast(CompileStatic) (2)
}
}
withConfig(configuration) {
source(extensionValidator: { it.name in ['sgroovy','sg']}) {
ast(CompileStatic) (2)
}
}
withConfig(configuration) {
source(basename: 'foo') {
ast(CompileStatic) (3)
}
}
withConfig(configuration) {
source(basenames: ['foo', 'bar']) {
ast(CompileStatic) (4)
}
}
withConfig(configuration) {
source(basenameValidator: { it in ['foo', 'bar'] }) {
ast(CompileStatic) (4)
}
}
withConfig(configuration) {
source(unitValidator: { unit -> !unit.AST.classes.any { it.name == 'Baz' } }) {
ast(CompileStatic) (5)
}
}
1 | 将 CompileStatic AST 注解应用于 .sgroovy 文件 |
2 | 将 CompileStatic AST 注解应用于 .sgroovy 或 .sg 文件 |
3 | 将 CompileStatic AST 注解应用于名称为“foo”的文件 |
4 | 将 CompileStatic AST 注解应用于名称为“foo”或“bar”的文件 |
5 | 将 CompileStatic AST 注解应用于不包含名为“Baz”的类的文件 |
内联定制器
内联定制器允许您直接编写编译定制器,而无需为其创建类。
withConfig(configuration) {
inline(phase:'CONVERSION') { source, context, classNode -> (1)
println "visiting $classNode" (2)
}
}
1 | 定义一个内联定制器,它将在 CONVERSION 阶段执行 |
2 | 打印正在编译的类节点的名称 |
configscript
命令行参数
到目前为止,我们已经描述了如何使用 CompilationConfiguration
类自定义编译,但这只有在您嵌入 Groovy 并创建自己的 CompilerConfiguration
实例(然后使用它创建 GroovyShell
、GroovyScriptEngine
等)时才有可能。
如果您希望将其应用于使用普通 Groovy 编译器(即使用 groovyc
、ant
或 gradle
等)编译的类,则可以使用名为 configscript
的命令行参数,该参数接受一个 Groovy 配置文件作为参数。
此脚本允许您在文件编译之前访问 CompilerConfiguration
实例(在配置文件中公开为名为 configuration
的变量),以便您可以对其进行调整。
它还透明地集成了上面的编译器配置构建器。例如,让我们看看如何默认激活所有类的静态编译。
Configscript 示例:默认静态编译
通常,Groovy 中的类使用动态运行时编译。您可以通过在任何类上放置名为 @CompileStatic
的注解来激活静态编译。有些人希望默认激活此模式,即不必注解(可能许多)类。使用 configscript
可以实现这一点。首先,您需要在例如 src/conf
中创建一个名为 config.groovy
的文件,其内容如下
withConfig(configuration) { (1)
ast(groovy.transform.CompileStatic)
}
1 | configuration 引用 CompilerConfiguration 实例 |
这就是您所需要的一切。您无需导入构建器,它会自动在脚本中公开。然后,使用以下命令行编译您的文件
groovyc -configscript src/conf/config.groovy src/main/groovy/MyClass.groovy
我们强烈建议您将配置文件与类文件分开,这就是为什么我们建议使用上面的 src/main
和 src/conf
目录。
Configscript 示例:设置系统属性
在配置文件中,您还可以设置系统属性,例如
System.setProperty('spock.iKnowWhatImDoing.disableGroovyVersionCheck', 'true')
如果您有许多系统属性要设置,那么使用配置文件将减少使用长命令行或适当定义的环境变量来设置一堆系统属性的需要。您还可以通过简单地共享配置文件来共享所有设置。
3.21.7. 自定义类型检查扩展
在某些情况下,尽快向用户提供有关错误代码的反馈可能会很有趣,也就是说,在编译 DSL 脚本时,而不是必须等待脚本的执行。然而,这对于动态代码来说通常是不可能的。Groovy 实际上为这个众所周知的问题提供了一个实用的答案,即类型检查扩展。
3.21.8. 构建器
许多任务需要构建事物,而构建器模式是开发人员用来使构建事物变得更容易的一种技术,特别是构建具有分层性质的结构。这种模式非常普遍,以至于 Groovy 提供了特殊的内置支持。首先,有许多内置构建器。其次,有一些类可以更容易地编写自己的构建器。
现有构建器
Groovy 附带了许多内置构建器。让我们看看其中的一些。
SaxBuilder
用于生成XML 简单 API (SAX)事件的构建器。
如果您有以下 SAX 处理器
class LogHandler extends org.xml.sax.helpers.DefaultHandler {
String log = ''
void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) {
log += "Start Element: $localName, "
}
void endElement(String uri, String localName, String qName) {
log += "End Element: $localName, "
}
}
您可以使用 SaxBuilder
为处理器生成 SAX 事件,如下所示
def handler = new LogHandler()
def builder = new groovy.xml.SAXBuilder(handler)
builder.root() {
helloWorld()
}
然后检查一切是否按预期工作
assert handler.log == 'Start Element: root, Start Element: helloWorld, End Element: helloWorld, End Element: root, '
StaxBuilder
一个 Groovy 构建器,可与XML 流 API (StAX)处理器一起使用。
下面是一个使用 Java 的 StAX 实现生成 XML 的简单示例
def factory = javax.xml.stream.XMLOutputFactory.newInstance()
def writer = new StringWriter()
def builder = new groovy.xml.StaxBuilder(factory.createXMLStreamWriter(writer))
builder.root(attribute:1) {
elem1('hello')
elem2('world')
}
assert writer.toString() == '<?xml version="1.0" ?><root attribute="1"><elem1>hello</elem1><elem2>world</elem2></root>'
外部库,例如Jettison,可以按如下方式使用
@Grab('org.codehaus.jettison:jettison:1.3.3')
@GrabExclude('stax:stax-api') // part of Java 6 and later
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"}}'
DOMBuilder
用于将 HTML、XHTML 和 XML 解析为W3C DOM树的构建器。
例如,这个 XML String
String recordsXML = '''
<records>
<car name='HSV Maloo' make='Holden' year='2006'>
<country>Australia</country>
<record type='speed'>Production Pickup Truck with speed of 271kph</record>
</car>
<car name='P50' make='Peel' year='1962'>
<country>Isle of Man</country>
<record type='size'>Smallest Street-Legal Car at 99cm wide and 59 kg in weight</record>
</car>
<car name='Royale' make='Bugatti' year='1931'>
<country>France</country>
<record type='price'>Most Valuable Car at $15 million</record>
</car>
</records>'''
可以使用 DOMBuilder
解析为 DOM 树,如下所示
def reader = new StringReader(recordsXML)
def doc = groovy.xml.DOMBuilder.parse(reader)
然后进一步处理,例如使用DOMCategory
def records = doc.documentElement
use(groovy.xml.dom.DOMCategory) {
assert records.car.size() == 3
}
NodeBuilder
NodeBuilder
用于创建 groovy.util.Node 对象的嵌套树,以处理任意数据。要创建一个简单的用户列表,您可以使用 NodeBuilder
,如下所示
def nodeBuilder = new NodeBuilder()
def userlist = nodeBuilder.userlist {
user(id: '1', firstname: 'John', lastname: 'Smith') {
address(type: 'home', street: '1 Main St.', city: 'Springfield', state: 'MA', zip: '12345')
address(type: 'work', street: '2 South St.', city: 'Boston', state: 'MA', zip: '98765')
}
user(id: '2', firstname: 'Alice', lastname: 'Doe')
}
现在您可以进一步处理数据,例如,使用GPath 表达式
assert userlist.user.@firstname.join(', ') == 'John, Alice'
assert userlist.user.find { it.@lastname == 'Smith' }.address.size() == 2
JsonBuilder
Groovy 的 JsonBuilder
使创建 Json 变得容易。例如,要创建这个 Json 字符串
String carRecords = '''
{
"records": {
"car": {
"name": "HSV Maloo",
"make": "Holden",
"year": 2006,
"country": "Australia",
"record": {
"type": "speed",
"description": "production pickup truck with speed of 271kph"
}
}
}
}
'''
你可以使用 JsonBuilder
如下所示
JsonBuilder builder = new JsonBuilder()
builder.records {
car {
name 'HSV Maloo'
make 'Holden'
year 2006
country 'Australia'
record {
type 'speed'
description 'production pickup truck with speed of 271kph'
}
}
}
String json = JsonOutput.prettyPrint(builder.toString())
我们使用 JsonUnit 来检查构建器是否产生了预期结果
JsonAssert.assertJsonEquals(json, carRecords)
如果需要自定义生成输出,可以在创建 JsonBuilder
时传入 JsonGenerator
实例
import groovy.json.*
def generator = new JsonGenerator.Options()
.excludeNulls()
.excludeFieldsByName('make', 'country', 'record')
.excludeFieldsByType(Number)
.addConverter(URL) { url -> "https://groovy-lang.cn" }
.build()
JsonBuilder builder = new JsonBuilder(generator)
builder.records {
car {
name 'HSV Maloo'
make 'Holden'
year 2006
country 'Australia'
homepage new URL('http://example.org')
record {
type 'speed'
description 'production pickup truck with speed of 271kph'
}
}
}
assert builder.toString() == '{"records":{"car":{"name":"HSV Maloo","homepage":"https://groovy-lang.cn"}}}'
StreamingJsonBuilder
与 JsonBuilder
不同,后者在内存中创建数据结构,这在您希望在输出之前以编程方式更改结构的情况下很方便,StreamingJsonBuilder
直接流式传输到写入器,而无需任何中间内存数据结构。如果您不需要修改结构并希望采用更节省内存的方法,请使用 StreamingJsonBuilder
。
StreamingJsonBuilder
的用法与 JsonBuilder
类似。为了创建这个 Json 字符串
String carRecords = """
{
"records": {
"car": {
"name": "HSV Maloo",
"make": "Holden",
"year": 2006,
"country": "Australia",
"record": {
"type": "speed",
"description": "production pickup truck with speed of 271kph"
}
}
}
}
"""
您可以使用 StreamingJsonBuilder
,如下所示
StringWriter writer = new StringWriter()
StreamingJsonBuilder builder = new StreamingJsonBuilder(writer)
builder.records {
car {
name 'HSV Maloo'
make 'Holden'
year 2006
country 'Australia'
record {
type 'speed'
description 'production pickup truck with speed of 271kph'
}
}
}
String json = JsonOutput.prettyPrint(writer.toString())
我们使用 JsonUnit 来检查预期结果
JsonAssert.assertJsonEquals(json, carRecords)
如果需要自定义生成的输出,可以在创建 StreamingJsonBuilder
时传入 JsonGenerator
实例
def generator = new JsonGenerator.Options()
.excludeNulls()
.excludeFieldsByName('make', 'country', 'record')
.excludeFieldsByType(Number)
.addConverter(URL) { url -> "https://groovy-lang.cn" }
.build()
StringWriter writer = new StringWriter()
StreamingJsonBuilder builder = new StreamingJsonBuilder(writer, generator)
builder.records {
car {
name 'HSV Maloo'
make 'Holden'
year 2006
country 'Australia'
homepage new URL('http://example.org')
record {
type 'speed'
description 'production pickup truck with speed of 271kph'
}
}
}
assert writer.toString() == '{"records":{"car":{"name":"HSV Maloo","homepage":"https://groovy-lang.cn"}}}'
SwingBuilder
SwingBuilder
允许您以声明和简洁的方式创建功能齐全的 Swing GUI。它通过使用 Groovy 中的常见习语——构建器来实现这一点。构建器为您处理创建复杂对象的繁重工作,例如实例化子级、调用 Swing 方法以及将这些子级附加到其父级。因此,您的代码更具可读性和可维护性,同时仍允许您访问完整的 Swing 组件范围。
以下是使用 SwingBuilder
的一个简单示例
import groovy.swing.SwingBuilder
import java.awt.BorderLayout as BL
count = 0
new SwingBuilder().edt {
frame(title: 'Frame', size: [250, 75], 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)
}
}
它将看起来像这样
这种组件层次结构通常通过一系列重复的实例化、设置器以及最终将子级附加到其各自的父级来创建。然而,使用 SwingBuilder
允许您以其原生形式定义此层次结构,这使得界面设计仅通过阅读代码即可理解。
这里展示的灵活性是通过利用 Groovy 中内置的许多编程特性实现的,例如闭包、隐式构造函数调用、导入别名和字符串插值。当然,要使用 SwingBuilder
,不必完全理解这些特性;从上面的代码可以看出,它们的用途是直观的。
这是一个稍微复杂一点的示例,其中包含通过闭包重用 SwingBuilder
代码的示例。
import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*
def swing = new SwingBuilder()
def sharedPanel = {
swing.panel() {
label("Shared Panel")
}
}
count = 0
swing.edt {
frame(title: 'Frame', defaultCloseOperation: JFrame.EXIT_ON_CLOSE, pack: true, show: true) {
vbox {
textlabel = label('Click the button!')
button(
text: 'Click Me',
actionPerformed: {
count++
textlabel.text = "Clicked ${count} time(s)."
println "Clicked!"
}
)
widget(sharedPanel())
widget(sharedPanel())
}
}
}
这是另一个依赖可观察 bean 和绑定的变体
import groovy.swing.SwingBuilder
import groovy.beans.Bindable
class MyModel {
@Bindable int count = 0
}
def model = new MyModel()
new SwingBuilder().edt {
frame(title: 'Java Frame', size: [100, 100], locationRelativeTo: null, show: true) {
gridLayout(cols: 1, rows: 2)
label(text: bind(source: model, sourceProperty: 'count', converter: { v -> v? "Clicked $v times": ''}))
button('Click me!', actionPerformed: { model.count++ })
}
}
@Bindable 是核心 AST 转换之一。它生成所有必需的样板代码,将一个简单的 bean 转换为可观察的 bean。bind()
节点创建适当的 PropertyChangeListeners
,每当触发 PropertyChangeEvent
时,这些侦听器都会更新感兴趣的各方。
AntBuilder
这里我们描述 AntBuilder ,它允许您用 Groovy 而不是 XML 编写 Ant 构建脚本。您可能还有兴趣使用 Groovy Ant 任务从 Ant 使用 Groovy。 |
尽管主要是一个构建工具,Apache Ant 是一个非常实用的文件操作工具,包括 zip 文件、复制、资源处理等。但是,如果您曾使用过 build.xml
文件或某些 Jelly 脚本,并发现自己受限于所有那些尖括号,或者觉得将 XML 用作脚本语言有点奇怪,并想要一些更清晰、更直接的东西,那么也许使用 Groovy 的 Ant 脚本正是您所需要的。
Groovy 有一个名为 AntBuilder
的辅助类,它使得 Ant 任务的脚本编写变得非常容易;允许使用真正的脚本语言进行编程构造(变量、方法、循环、逻辑分支、类等)。它仍然看起来像 Ant XML 的简洁版本,没有所有那些尖括号;尽管您可以在脚本中混合和匹配此标记。Ant 本身是 jar 文件的集合。通过将它们添加到您的类路径中,您可以轻松地在 Groovy 中使用它们。我们相信使用 AntBuilder
会带来更简洁易懂的语法。
AntBuilder
使用我们在 Groovy 中习惯的便捷构建器表示法直接公开 Ant 任务。这是最基本的示例,它在标准输出上打印一条消息
def ant = new groovy.ant.AntBuilder() (1)
ant.echo('hello from Ant!') (2)
1 | 创建 AntBuilder 实例 |
2 | 执行带有参数消息的 echo 任务 |
想象一下您需要创建一个 ZIP 文件。它可以像这样简单
def ant = new AntBuilder()
ant.zip(destfile: 'sources.zip', basedir: 'src')
在下一个示例中,我们演示了如何使用 AntBuilder
在 Groovy 中直接使用经典的 Ant 模式复制文件列表
// let's just call one task
ant.echo("hello")
// here is an example of a block of Ant inside GroovyMarkup
ant.sequential {
echo("inside sequential")
def myDir = "build/AntTest/"
mkdir(dir: myDir)
copy(todir: myDir) {
fileset(dir: "src/test") {
include(name: "**/*.groovy")
}
}
echo("done")
}
// now let's do some normal Groovy again
def file = new File(ant.project.baseDir,"build/AntTest/some/pkg/MyTest.groovy")
assert file.exists()
另一个例子是遍历匹配特定模式的文件列表
// let's create a scanner of filesets
def scanner = ant.fileScanner {
fileset(dir:"src/test") {
include(name:"**/My*.groovy")
}
}
// now let's 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
或者执行 JUnit 测试
ant.junit {
classpath { pathelement(path: '.') }
test(name:'some.pkg.MyTest')
}
我们甚至可以更进一步,直接从 Groovy 编译和执行 Java 文件
ant.echo(file:'Temp.java', '''
class Temp {
public static void main(String[] args) {
System.out.println("Hello");
}
}
''')
ant.javac(srcdir:'.', includes:'Temp.java', fork:'true')
ant.java(classpath:'.', classname:'Temp', fork:'true')
ant.echo('Done')
CliBuilder
CliBuilder
提供了一种紧凑的方式来指定命令行应用程序的可用选项,然后根据该规范自动解析应用程序的命令行参数。按照惯例,选项命令行参数与作为应用程序参数传递给应用程序的任何剩余参数之间存在区别。通常,可能会支持几种类型的选项,例如 -V
或 --tabsize=4
。CliBuilder
消除了为命令行处理开发大量代码的负担。相反,它支持一种声明式的方法来声明您的选项,然后提供一个单独的调用来解析命令行参数,并提供一种简单的机制来查询选项(您可以将其视为一个简单的选项模型)。
尽管您创建的每个命令行的细节可能大相径庭,但每次都遵循相同的主要步骤。首先,创建 CliBuilder
实例。然后,定义允许的命令行选项。这可以使用动态 API样式或注解样式完成。然后根据选项规范解析命令行参数,从而生成选项集合,然后对这些选项进行查询。
这是一个简单的 Greeter.groovy
脚本示例,说明了用法
// import of CliBuilder not shown (1)
// specify parameters
def cli = new CliBuilder(usage: 'groovy Greeter [option]') (2)
cli.a(longOpt: 'audience', args: 1, 'greeting audience') (3)
cli.h(longOpt: 'help', 'display usage') (4)
// parse and process parameters
def options = cli.parse(args) (5)
if (options.h) cli.usage() (6)
else println "Hello ${options.a ? options.a : 'World'}" (7)
1 | Groovy 的早期版本在 groovy.util 包中包含 CliBuilder,无需导入。在 Groovy 2.5 中,这种方法被弃用:应用程序应该选择 groovy.cli.picocli 或 groovy.cli.commons 版本。Groovy 2.5 中的 groovy.util 版本指向 commons-cli 版本以实现向后兼容性,但在 Groovy 3.0 中已删除。 |
2 | 定义一个新的 CliBuilder 实例,指定可选的用法字符串 |
3 | 指定一个带单个参数的 -a 选项,以及一个可选的长变体 --audience |
4 | 指定一个不带参数的 -h 选项,以及一个可选的长变体 --help |
5 | 解析提供给脚本的命令行参数 |
6 | 如果找到 h 选项,则显示用法消息 |
7 | 显示标准问候语,如果找到 a 选项,则显示自定义问候语 |
在没有命令行参数的情况下运行此脚本,即
> groovy Greeter
产生以下输出
Hello World
以 -h
作为单个命令行参数运行此脚本,即
> groovy Greeter -h
产生以下输出
usage: groovy Greeter [option] -a,--audience <arg> greeting audience -h,--help display usage
使用 --audience Groovologist
作为命令行参数运行此脚本,即
> groovy Greeter --audience Groovologist
产生以下输出
Hello Groovologist
在上面的示例中创建 CliBuilder
实例时,我们在构造函数调用中设置了可选的 usage
属性。这遵循了 Groovy 在构造期间设置实例其他属性的正常能力。还有许多其他属性可以设置,例如 header
和 footer
。有关可用属性的完整集合,请参阅 groovy.util.CliBuilder 类的可用属性。
在定义允许的命令行选项时,必须同时提供一个短名称(例如,前面所示的 help
选项的“h”)和一个简短描述(例如,help
选项的“display usage”)。在上面的示例中,我们还设置了一些附加属性,例如 longOpt
和 args
。在指定允许的命令行选项时,支持以下附加属性
名称 | 描述 | 类型 |
---|---|---|
argName |
此选项的参数名称,用于输出 |
|
longOpt |
选项的冗长表示或冗长名称 |
|
args |
参数值的数量 |
|
optionalArg |
参数值是否可选 |
|
required |
选项是否强制 |
|
type |
此选项的类型 |
|
valueSeparator |
值分隔符字符 |
|
defaultValue |
默认值 |
|
convert |
将输入的字符串转换为所需类型 |
|
(1) 稍后会有更多细节
(2) 在 Groovy 的特殊情况下,单字符字符串会被强制转换为字符
如果您的选项只有一个 longOpt
变体,您可以使用特殊的短名称 '_' 来指定该选项,例如:cli._(longOpt: 'verbose', 'enable verbose logging')
。一些剩余的命名参数应该相当一目了然,而另一些则需要更多解释。但在进一步解释之前,让我们看看使用注解使用 CliBuilder
的方法。
与其进行一系列方法调用(尽管以非常声明式的迷你 DSL 形式)来指定允许的选项,不如提供允许选项的接口规范,其中注解用于指示和提供这些选项以及如何处理未处理参数的详细信息。使用了两个注解:groovy.cli.Option 和 groovy.cli.Unparsed。
下面是如何定义这样的规范
interface GreeterI {
@Option(shortName='h', description='display usage') Boolean help() (1)
@Option(shortName='a', description='greeting audience') String audience() (2)
@Unparsed(description = "positional parameters") List remaining() (3)
}
1 | 指定使用 -h 或 --help 设置的布尔选项 |
2 | 指定使用 -a 或 --audience 设置的字符串选项 |
3 | 指定任何剩余参数的存储位置 |
请注意长名称是如何从接口方法名称自动确定的。您可以使用 longName
注解属性来覆盖此行为,并根据需要指定自定义长名称,或者使用 '_' 的长名称来指示不提供长名称。在这种情况下,您需要指定一个短名称。
下面是您可以使用接口规范的方式
// import CliBuilder not shown
def cli = new CliBuilder(usage: 'groovy Greeter') (1)
def argz = '--audience Groovologist'.split()
def options = cli.parseFromSpec(GreeterI, argz) (2)
assert options.audience() == 'Groovologist' (3)
argz = '-h Some Other Args'.split()
options = cli.parseFromSpec(GreeterI, argz) (4)
assert options.help()
assert options.remaining() == ['Some', 'Other', 'Args'] (5)
1 | 像以前一样创建带可选属性的 CliBuilder 实例 |
2 | 使用接口规范解析参数 |
3 | 使用接口中的方法查询选项 |
4 | 解析不同的参数集 |
5 | 查询剩余参数 |
当调用 parseFromSpec
时,CliBuilder
会自动创建一个实现接口的实例并填充它。您只需调用接口方法即可查询选项值。
或者,也许您已经有一个包含选项信息的领域类。您可以简单地注释该类中的属性或设置器,以使 CliBuilder
能够适当地填充您的领域对象。每个注释都通过注释属性描述该选项的属性,并指示 CliBuilder
将用于在您的领域对象中填充该选项的设置器。
下面是如何定义这样的规范
class GreeterC {
@Option(shortName='h', description='display usage')
Boolean help (1)
private String audience
@Option(shortName='a', description='greeting audience')
void setAudience(String audience) { (2)
this.audience = audience
}
String getAudience() { audience }
@Unparsed(description = "positional parameters")
List remaining (3)
}
1 | 指示布尔属性是一个选项 |
2 | 指示字符串属性(带显式 setter)是一个选项 |
3 | 指定任何剩余参数的存储位置 |
以下是您可以使用此规范的方式
// import CliBuilder not shown
def cli = new CliBuilder(usage: 'groovy Greeter [option]') (1)
def options = new GreeterC() (2)
def argz = '--audience Groovologist foo'.split()
cli.parseFromInstance(options, argz) (3)
assert options.audience == 'Groovologist' (4)
assert options.remaining == ['foo'] (5)
1 | 像以前一样创建带有可选参数的 CliBuilder 实例 |
2 | 创建 CliBuilder 要填充的实例 |
3 | 解析参数并填充提供的实例 |
4 | 查询字符串选项属性 |
5 | 查询剩余参数属性 |
当调用 parseFromInstance
时,CliBuilder
会自动填充您的实例。您只需查询实例属性(或您在领域对象中提供的任何访问器方法)即可访问选项值。
最后,还有两个额外的方便注解别名专门用于脚本。它们只是结合了前面提到的注解和 groovy.transform.Field。这些注解的 groovydoc 揭示了详细信息:groovy.cli.OptionField 和 groovy.cli.UnparsedField。
这是一个在自包含脚本中使用这些注解的示例,该脚本将使用与前面实例示例中所示的相同参数进行调用
// import CliBuilder not shown
import groovy.cli.OptionField
import groovy.cli.UnparsedField
@OptionField String audience
@OptionField Boolean help
@UnparsedField List remaining
new CliBuilder().parseFromInstance(this, args)
assert audience == 'Groovologist'
assert remaining == ['foo']
我们在最初的示例中看到,一些选项充当标志,例如 Greeter -h
,但其他选项带有参数,例如 Greeter --audience Groovologist
。最简单的情况涉及充当标志或具有单个(可能可选)参数的选项。以下是涉及这些情况的示例
// import CliBuilder not shown
def cli = new CliBuilder()
cli.a(args: 0, 'a arg') (1)
cli.b(args: 1, 'b arg') (2)
cli.c(args: 1, optionalArg: true, 'c arg') (3)
def options = cli.parse('-a -b foo -c bar baz'.split()) (4)
assert options.a == true
assert options.b == 'foo'
assert options.c == 'bar'
assert options.arguments() == ['baz']
options = cli.parse('-a -c -b foo bar baz'.split()) (5)
assert options.a == true
assert options.c == true
assert options.b == 'foo'
assert options.arguments() == ['bar', 'baz']
1 | 一个只作标志的选项——默认值;允许将 args 设置为 0,但不需要。 |
2 | 具有一个参数的选项 |
3 | 具有可选参数的选项;如果选项被省略,则它充当标志 |
4 | 使用此规范的示例,其中为“c”选项提供了参数 |
5 | 一个使用此规范的例子,其中没有为 'c' 选项提供参数;它只是一个标志 |
注意:当遇到带有可选参数的选项时,它会(某种程度上)贪婪地消耗提供给命令行的下一个参数。但是,如果下一个参数与已知的长或短选项(带有前导单或双连字符)匹配,则该选项将优先,例如上面示例中的 -b
。
选项参数也可以使用注解样式指定。这是一个说明此类定义的接口选项规范
interface WithArgsI {
@Option boolean a()
@Option String b()
@Option(optionalArg=true) String[] c()
@Unparsed List remaining()
}
使用方法如下:
def cli = new CliBuilder()
def options = cli.parseFromSpec(WithArgsI, '-a -b foo -c bar baz'.split())
assert options.a()
assert options.b() == 'foo'
assert options.c() == ['bar']
assert options.remaining() == ['baz']
options = cli.parseFromSpec(WithArgsI, '-a -c -b foo bar baz'.split())
assert options.a()
assert options.c() == []
assert options.b() == 'foo'
assert options.remaining() == ['bar', 'baz']
此示例使用了数组类型的选项规范。我们将在稍后讨论多个参数时更详细地介绍这一点。
命令行上的参数本质上是字符串(或者可以说可以视为标志的布尔值),但可以通过提供额外的类型信息自动转换为更丰富的类型。对于基于注解的参数定义样式,这些类型通过注解属性的字段类型或注解方法的返回类型(或设置器方法的设置器参数类型)提供。对于动态方法样式的参数定义,支持一个特殊的“type”属性,允许您指定类名。
当定义了显式类型时,命名参数 args
假定为 1(布尔类型选项除外,默认情况下为 0)。如果需要,仍然可以提供显式 args
参数。以下是使用动态 API 参数定义样式使用类型的示例
def argz = '''-a John -b -d 21 -e 1980 -f 3.5 -g 3.14159
-h cv.txt -i DOWN and some more'''.split()
def cli = new CliBuilder()
cli.a(type: String, 'a-arg')
cli.b(type: boolean, 'b-arg')
cli.c(type: Boolean, 'c-arg')
cli.d(type: int, 'd-arg')
cli.e(type: Long, 'e-arg')
cli.f(type: Float, 'f-arg')
cli.g(type: BigDecimal, 'g-arg')
cli.h(type: File, 'h-arg')
cli.i(type: RoundingMode, 'i-arg')
def options = cli.parse(argz)
assert options.a == 'John'
assert options.b
assert !options.c
assert options.d == 21
assert options.e == 1980L
assert options.f == 3.5f
assert options.g == 3.14159
assert options.h == new File('cv.txt')
assert options.i == RoundingMode.DOWN
assert options.arguments() == ['and', 'some', 'more']
支持基本类型、数字类型、文件、枚举及其数组(它们使用 org.codehaus.groovy.runtime.StringGroovyMethods#asType 进行转换)。
如果支持的类型不够,您可以提供一个闭包来为您处理字符串到富类型的转换。这是一个使用动态 API 样式的示例
def argz = '''-a John -b Mary -d 2016-01-01 and some more'''.split()
def cli = new CliBuilder()
def lower = { it.toLowerCase() }
cli.a(convert: lower, 'a-arg')
cli.b(convert: { it.toUpperCase() }, 'b-arg')
cli.d(convert: { Date.parse('yyyy-MM-dd', it) }, 'd-arg')
def options = cli.parse(argz)
assert options.a == 'john'
assert options.b == 'MARY'
assert options.d.format('dd-MM-yyyy') == '01-01-2016'
assert options.arguments() == ['and', 'some', 'more']
或者,您可以使用注解样式,将转换闭包作为注解参数提供。这是一个示例规范
interface WithConvertI {
@Option(convert={ it.toLowerCase() }) String a()
@Option(convert={ it.toUpperCase() }) String b()
@Option(convert={ Date.parse("yyyy-MM-dd", it) }) Date d()
@Unparsed List remaining()
}
以及一个使用该规范的示例
Date newYears = Date.parse("yyyy-MM-dd", "2016-01-01")
def argz = '''-a John -b Mary -d 2016-01-01 and some more'''.split()
def cli = new CliBuilder()
def options = cli.parseFromSpec(WithConvertI, argz)
assert options.a() == 'john'
assert options.b() == 'MARY'
assert options.d() == newYears
assert options.remaining() == ['and', 'some', 'more']
使用大于 1 的 args
值也支持多个参数。有一个特殊的命名参数 valueSeparator
,在处理多个参数时也可以选择使用。它允许在命令行上提供此类参数列表时,语法具有额外的灵活性。例如,提供一个值分隔符 ',' 允许在命令行上传递逗号分隔的值列表。
args
值通常是整数。它可以选择作为字符串提供。有两个特殊的字符串符号:` 和 `\*
。*
值表示 0 个或更多。 值表示 1 个或更多。
*
值与使用 +
并同时将 optionalArg
值设置为 true 相同。
访问多个参数遵循一个特殊的约定。只需在您通常用于访问参数选项的属性后面添加一个 's',您将检索到所有提供的参数作为列表。因此,对于名为 'a' 的短选项,您可以使用 options.a
访问第一个 'a' 参数,并使用 options.as
访问所有参数的列表。短名称或长名称以 's' 结尾是可以的,只要您没有不带 's' 的单数变体。因此,如果 name
是您的多个参数选项之一,而 guess
是单个参数选项,则使用 options.names
和 options.guess
不会混淆。
以下是突出显示多个参数使用的摘录
// import CliBuilder not shown
def cli = new CliBuilder()
cli.a(args: 2, 'a-arg')
cli.b(args: '2', valueSeparator: ',', 'b-arg') (1)
cli.c(args: '+', valueSeparator: ',', 'c-arg') (2)
def options = cli.parse('-a 1 2 3 4'.split()) (3)
assert options.a == '1' (4)
assert options.as == ['1', '2'] (5)
assert options.arguments() == ['3', '4']
options = cli.parse('-a1 -a2 3'.split()) (6)
assert options.as == ['1', '2']
assert options.arguments() == ['3']
options = cli.parse(['-b1,2']) (7)
assert options.bs == ['1', '2']
options = cli.parse(['-c', '1'])
assert options.cs == ['1']
options = cli.parse(['-c1'])
assert options.cs == ['1']
options = cli.parse(['-c1,2,3'])
assert options.cs == ['1', '2', '3']
1 | Args 值作为字符串提供,并指定逗号值分隔符 |
2 | 允许一个或多个参数 |
3 | 将提供两个命令行参数作为“b”选项的参数列表 |
4 | 访问 'a' 选项的第一个参数 |
5 | 访问“a”选项的参数列表 |
6 | 为“a”选项指定两个参数的另一种语法 |
7 | 作为逗号分隔值提供的 'b' 选项的参数 |
作为使用复数名称方法访问多个参数的替代方法,您可以为选项使用基于数组的类型。在这种情况下,所有选项将始终通过数组返回,该数组通过正常的单数名称访问。我们将在讨论类型时在下一个示例中看到这一点。
通过为带注解的类成员(方法或属性)使用数组类型,注解样式的选项定义也支持多个参数,如下例所示
interface ValSepI {
@Option(numberOfArguments=2) String[] a()
@Option(numberOfArgumentsString='2', valueSeparator=',') String[] b()
@Option(numberOfArgumentsString='+', valueSeparator=',') String[] c()
@Unparsed remaining()
}
使用方法如下:
def cli = new CliBuilder()
def options = cli.parseFromSpec(ValSepI, '-a 1 2 3 4'.split())
assert options.a() == ['1', '2']
assert options.remaining() == ['3', '4']
options = cli.parseFromSpec(ValSepI, '-a1 -a2 3'.split())
assert options.a() == ['1', '2']
assert options.remaining() == ['3']
options = cli.parseFromSpec(ValSepI, ['-b1,2'] as String[])
assert options.b() == ['1', '2']
options = cli.parseFromSpec(ValSepI, ['-c', '1'] as String[])
assert options.c() == ['1']
options = cli.parseFromSpec(ValSepI, ['-c1'] as String[])
assert options.c() == ['1']
options = cli.parseFromSpec(ValSepI, ['-c1,2,3'] as String[])
assert options.c() == ['1', '2', '3']
以下是使用动态 API 参数定义样式时,使用类型和多个参数的示例
def argz = '''-j 3 4 5 -k1.5,2.5,3.5 and some more'''.split()
def cli = new CliBuilder()
cli.j(args: 3, type: int[], 'j-arg')
cli.k(args: '+', valueSeparator: ',', type: BigDecimal[], 'k-arg')
def options = cli.parse(argz)
assert options.js == [3, 4, 5] (1)
assert options.j == [3, 4, 5] (1)
assert options.k == [1.5, 2.5, 3.5]
assert options.arguments() == ['and', 'some', 'more']
1 | 对于数组类型,可以使用尾随的 's',但不是必需的 |
Groovy 使用 Elvis 运算符在变量使用点提供默认值变得容易,例如 String x = someVariable ?: 'some default'
。但有时您希望将此类默认值作为选项规范的一部分,以最大限度地减少稍后阶段查询器的工作。CliBuilder
支持 defaultValue
属性以满足这种情况。
以下是使用动态 API 样式的使用方法
def cli = new CliBuilder()
cli.f longOpt: 'from', type: String, args: 1, defaultValue: 'one', 'f option'
cli.t longOpt: 'to', type: int, defaultValue: '35', 't option'
def options = cli.parse('-f two'.split())
assert options.hasOption('f')
assert options.f == 'two'
assert !options.hasOption('t')
assert options.t == 35
options = cli.parse('-t 45'.split())
assert !options.hasOption('from')
assert options.from == 'one'
assert options.hasOption('to')
assert options.to == 45
同样,您可能希望使用注解样式进行此类规范。这是一个使用接口规范的示例
interface WithDefaultValueI {
@Option(shortName='f', defaultValue='one') String from()
@Option(shortName='t', defaultValue='35') int to()
}
使用方法如下:
def cli = new CliBuilder()
def options = cli.parseFromSpec(WithDefaultValueI, '-f two'.split())
assert options.from() == 'two'
assert options.to() == 35
options = cli.parseFromSpec(WithDefaultValueI, '-t 45'.split())
assert options.from() == 'one'
assert options.to() == 45
当将注解与实例一起使用时,您还可以使用 defaultValue
注解属性,尽管为属性(或支持字段)提供初始值可能同样容易。
TypeChecked
一起使用使用 CliBuilder
的动态 API 风格本质上是动态的,但如果您想利用 Groovy 的静态类型检查功能,则有几个选项。首先,考虑使用注解风格,例如,这里是一个接口选项规范
interface TypeCheckedI{
@Option String name()
@Option int age()
@Unparsed List remaining()
}
它可以与 @TypeChecked
结合使用,如下所示
@TypeChecked
void testTypeCheckedInterface() {
def argz = "--name John --age 21 and some more".split()
def cli = new CliBuilder()
def options = cli.parseFromSpec(TypeCheckedI, argz)
String n = options.name()
int a = options.age()
assert n == 'John' && a == 21
assert options.remaining() == ['and', 'some', 'more']
}
其次,动态 API 样式的一个特性提供了一些支持。定义语句本质上是动态的,但实际上返回了一个我们前面示例中忽略的值。返回的值实际上是一个 TypedOption<Type>
,特殊的 getAt
支持允许使用类型化选项查询选项,例如 options[savedTypeOption]
。因此,如果您的代码中非类型检查部分有类似以下的语句
def cli = new CliBuilder()
TypedOption<Integer> age = cli.a(longOpt: 'age', type: Integer, 'some age option')
那么,以下语句可以位于代码中经过类型检查的另一部分
def args = '--age 21'.split()
def options = cli.parse(args)
int a = options[age]
assert a == 21
最后,CliBuilder
还提供了一个额外的便捷方法,甚至允许对定义部分进行类型检查。这是一个稍微更冗长的方法调用。不是在方法调用中使用短名称(即 opt 名称),而是使用固定的名称 option
,并将 opt
值作为属性提供。您还必须直接指定类型,如下例所示
import groovy.cli.TypedOption
import groovy.transform.TypeChecked
@TypeChecked
void testTypeChecked() {
def cli = new CliBuilder()
TypedOption<String> name = cli.option(String, opt: 'n', longOpt: 'name', 'name option')
TypedOption<Integer> age = cli.option(Integer, longOpt: 'age', 'age option')
def argz = "--name John --age 21 and some more".split()
def options = cli.parse(argz)
String n = options[name]
int a = options[age]
assert n == 'John' && a == 21
assert options.arguments() == ['and', 'some', 'more']
}
注意高级 CLI 功能
|
例如,以下是使用 Apache Commons CLI 分组机制的一些代码
import org.apache.commons.cli.*
def cli = new CliBuilder()
cli.f longOpt: 'from', 'f option'
cli.u longOpt: 'until', 'u option'
def optionGroup = new OptionGroup()
optionGroup.with {
addOption cli.option('o', [longOpt: 'output'], 'o option')
addOption cli.option('d', [longOpt: 'directory'], 'd option')
}
cli.options.addOptionGroup optionGroup
assert !cli.parse('-d -o'.split()) (1)
1 | 解析将失败,因为一个组中一次只能使用一个选项。 |
下面是 CliBuilder
的 picocli 版本中可用的一些功能。
新属性:errorWriter
当您的应用程序用户提供无效的命令行参数时,CliBuilder 会将错误消息和用法帮助消息写入 stderr
输出流。它不使用 stdout
流,以防止在您的程序输出用作另一个进程的输入时解析错误消息。您可以通过将 errorWriter
设置为不同的值来自定义目标。
另一方面,CliBuilder.usage()
将用法帮助消息打印到 stdout
流。这样,当用户请求帮助(例如,使用 --help
参数)时,他们可以将输出通过管道传输到 less
或 grep
等实用程序。
您可以为测试指定不同的写入器。请注意,为了向后兼容,将 writer
属性设置为不同值将同时将 writer
和 errorWriter
设置为指定的写入器。
ANSI 颜色
CliBuilder 的 picocli 版本会在支持的平台上自动以 ANSI 颜色渲染用法帮助消息。如果需要,您可以自定义此功能。(示例如下。)
新属性:name
如前所述,您可以使用 usage
属性设置用法帮助消息的概要。您可能对一个小的改进感兴趣:如果您只设置命令 name
,则会自动生成一个概要,重复元素后面跟着 …
,可选元素用 [
和 ]
包围。(示例如下。)
新属性:usageMessage
此属性公开了底层 picocli 库中的 UsageMessageSpec
对象,该对象可以对用法帮助消息的各个部分进行精细控制。例如
def cli = new CliBuilder()
cli.name = "myapp"
cli.usageMessage.with {
headerHeading("@|bold,underline Header heading:|@%n")
header("Header 1", "Header 2") // before the synopsis
synopsisHeading("%n@|bold,underline Usage:|@ ")
descriptionHeading("%n@|bold,underline Description heading:|@%n")
description("Description 1", "Description 2") // after the synopsis
optionListHeading("%n@|bold,underline Options heading:|@%n")
footerHeading("%n@|bold,underline Footer heading:|@%n")
footer("Footer 1", "Footer 2")
}
cli.a('option a description')
cli.b('option b description')
cli.c(args: '*', 'option c description')
cli.usage()
产生此输出
属性:解析器
parser
属性提供对 picocli ParserSpec
对象的访问,该对象可用于自定义解析器行为。
当 CliBuilder
控制解析器的选项不够精细时,这可能很有用。例如,为了与 CliBuilder
的 Commons CLI 实现向后兼容,默认情况下,CliBuilder
在遇到未知选项时停止查找选项,并且后续命令行参数被视为位置参数。CliBuilder
提供了一个 stopAtNonOption
属性,通过将其设置为 false
,您可以使解析器更严格,因此未知选项会导致 error: Unknown option: '-x'
。
但是,如果您想将未知选项视为位置参数,并且仍然将后续命令行参数作为选项处理,该怎么办?
这可以通过 parser
属性来实现。例如
def cli = new CliBuilder()
cli.parser.stopAtPositional(false)
cli.parser.unmatchedOptionsArePositionalParams(true)
// ...
def opts = cli.parse(args)
// ...
有关详细信息,请参阅文档。
映射选项
最后,如果您的应用程序的选项是键值对,您可能会对 picocli 对映射的支持感兴趣。例如
import java.util.concurrent.TimeUnit
import static java.util.concurrent.TimeUnit.DAYS
import static java.util.concurrent.TimeUnit.HOURS
def cli = new CliBuilder()
cli.D(args: 2, valueSeparator: '=', 'the old way') (1)
cli.X(type: Map, 'the new way') (2)
cli.Z(type: Map, auxiliaryTypes: [TimeUnit, Integer].toArray(), 'typed map') (3)
def options = cli.parse('-Da=b -Dc=d -Xx=y -Xi=j -ZDAYS=2 -ZHOURS=23'.split())(4)
assert options.Ds == ['a', 'b', 'c', 'd'] (5)
assert options.Xs == [ 'x':'y', 'i':'j' ] (6)
assert options.Zs == [ (DAYS as TimeUnit):2, (HOURS as TimeUnit):23 ] (7)
1 | 以前,key=value 对被拆分为多个部分并添加到列表中 |
2 | Picocli 映射支持:只需将 Map 指定为选项的类型 |
3 | 您甚至可以指定地图元素的类型 |
4 | 为了进行比较,让我们为每个选项指定两个键值对 |
5 | 以前,所有键值对都最终成为一个列表,由应用程序处理此列表 |
6 | Picocli 将键值对作为 Map 返回 |
7 | 映射的键和值都可以是强类型 |
控制 Picocli 版本
要使用特定版本的 picocli,请在您的构建配置中添加对该版本的依赖。如果使用预安装的 Groovy 版本运行脚本,请使用 @Grab
注解来控制 CliBuilder
中使用的 picocli 版本。
@GrabConfig(systemClassLoader=true)
@Grab('info.picocli:picocli:4.2.0')
import groovy.cli.picocli.CliBuilder
def cli = new CliBuilder()
ObjectGraphBuilder
ObjectGraphBuilder
是一个用于构建遵循 JavaBean 约定的任意 bean 图的构建器。它特别适用于创建测试数据。
让我们从属于您的域的类列表开始
package com.acme
class Company {
String name
Address address
List employees = []
}
class Address {
String line1
String line2
int zip
String state
}
class Employee {
String name
int employeeId
Address address
Company company
}
然后使用 ObjectGraphBuilder
构建一个拥有三名员工的 Company
就像这样简单
def builder = new ObjectGraphBuilder() (1)
builder.classLoader = this.class.classLoader (2)
builder.classNameResolver = "com.acme" (3)
def acme = builder.company(name: 'ACME') { (4)
3.times {
employee(id: it.toString(), name: "Drone $it") { (5)
address(line1:"Post street") (6)
}
}
}
assert acme != null
assert acme instanceof Company
assert acme.name == 'ACME'
assert acme.employees.size() == 3
def employee = acme.employees[0]
assert employee instanceof Employee
assert employee.name == 'Drone 0'
assert employee.address instanceof Address
1 | 创建一个新的对象图构建器 |
2 | 设置类将被解析的类加载器 |
3 | 设置类将被解析的基本包名 |
4 | 创建 Company 实例 |
5 | 拥有 3 个 Employee 实例 |
6 | 它们每个都有一个不同的 Address |
在幕后,对象图构建器
-
将尝试将节点名称匹配到
Class
,使用需要包名称的默认ClassNameResolver
策略 -
然后将使用调用无参构造函数的默认
NewInstanceResolver
策略创建相应类的实例 -
解决嵌套节点的父/子关系,涉及另外两种策略
-
RelationNameResolver
将产生子项在父项中的属性名称,以及父项在子项中的属性名称(如果有的话,在此例中,Employee
有一个恰当命名的父项属性company
) -
ChildPropertySetter
将把子节点插入到父节点中,同时考虑子节点是否属于Collection
(在此例中,employees
应该是Company
中Employee
实例的列表)。
-
所有 4 种策略都有一个默认实现,如果代码遵循编写 JavaBeans 的常见约定,则它们会按预期工作。如果您的任何 bean 或对象不遵循约定,您可以插入自己的每种策略实现。例如,想象您需要构建一个不可变的类
@Immutable
class Person {
String name
int age
}
那么,如果你尝试用构建器创建一个 Person
def person = builder.person(name:'Jon', age:17)
它将在运行时失败,出现以下错误
Cannot set readonly property: name for class: com.acme.Person
修复此问题可以通过更改新实例策略来完成
builder.newInstanceResolver = { Class klazz, Map attributes ->
if (klazz.getConstructor(Map)) {
def o = klazz.newInstance(attributes)
attributes.clear()
return o
}
klazz.newInstance()
}
ObjectGraphBuilder
支持每个节点的 id,这意味着您可以在构建器中存储对节点的引用。当多个对象引用同一个实例时,这很有用。因为在某些领域模型中,名为 id
的属性可能具有业务含义,所以 ObjectGraphBuilder
有一个名为 IdentifierResolver
的策略,您可以配置它来更改默认名称值。同样的情况也可能发生在用于引用先前保存实例的属性上,一个名为 ReferenceResolver
的策略将产生适当的值(默认值为 'refId')。
def company = builder.company(name: 'ACME') {
address(id: 'a1', line1: '123 Groovy Rd', zip: 12345, state: 'JV') (1)
employee(name: 'Duke', employeeId: 1, address: a1) (2)
employee(name: 'John', employeeId: 2 ){
address( refId: 'a1' ) (3)
}
}
1 | 可以使用 id 创建地址 |
2 | 员工可以直接通过其 ID 引用地址 |
3 | 或使用与相应地址的 id 对应的 refId 属性 |
值得一提的是,您不能修改被引用 bean 的属性。
FileTreeBuilder
groovy.util.FileTreeBuilder 是一个用于从规范生成文件目录结构的构建器。例如,要创建以下树
src/ |--- main | |--- groovy | |--- Foo.groovy |--- test |--- groovy |--- FooTest.groovy
您可以使用 FileTreeBuilder
,如下所示
tmpDir = File.createTempDir()
def fileTreeBuilder = new FileTreeBuilder(tmpDir)
fileTreeBuilder.dir('src') {
dir('main') {
dir('groovy') {
file('Foo.groovy', 'println "Hello"')
}
}
dir('test') {
dir('groovy') {
file('FooTest.groovy', 'class FooTest extends groovy.test.GroovyTestCase {}')
}
}
}
为了检查一切是否按预期工作,我们使用以下 `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 groovy.test.GroovyTestCase {}'
FileTreeBuilder
也支持简写语法
tmpDir = File.createTempDir()
def fileTreeBuilder = new FileTreeBuilder(tmpDir)
fileTreeBuilder.src {
main {
groovy {
'Foo.groovy'('println "Hello"')
}
}
test {
groovy {
'FooTest.groovy'('class FooTest extends groovy.test.GroovyTestCase {}')
}
}
}
这会产生与上面相同的目录结构,如下面的 `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 groovy.test.GroovyTestCase {}'
创建一个构建器
虽然 Groovy 有许多内置构建器,但构建器模式非常常见,您无疑最终会遇到内置构建器未能满足的构建需求。好消息是您可以构建自己的构建器。您可以通过依靠 Groovy 的元编程能力从头开始做所有事情。或者,BuilderSupport
和 FactoryBuilderSupport
类使设计您自己的构建器变得更容易。
BuilderSupport
构建构建器的一种方法是子类化 BuilderSupport
。通过这种方法,通常的想法是覆盖 BuilderSupport
抽象类中的一个或多个生命周期方法,包括 setParent
、nodeCompleted
以及部分或全部 createNode
方法。
例如,假设我们要创建一个追踪体育训练项目的构建器。每个项目都由许多组构成,每个组都有自己的步骤。一个步骤本身可能是一组更小的步骤。对于每个 set
或 step
,我们可能希望记录所需的 distance
(或 time
)、是否需要 repeat
步骤一定的次数、每个步骤之间是否需要 break
等等。
为了简化本示例,我们将使用映射和列表捕获训练编程。一个集合包含一个步骤列表。诸如 repeat
计数或 distance
之类的信息将存储在每个步骤和集合的属性映射中。
构建器实现如下
-
覆盖几个
createNode
方法。我们将创建一个映射,捕获集合名称、一个空步骤列表以及一些潜在属性。 -
每当我们完成一个节点时,我们都会将该节点添加到父节点的步骤列表中(如果有的话)。
代码看起来像这样
class TrainingBuilder1 extends BuilderSupport {
protected createNode(name) {
[name: name, steps: []]
}
protected createNode(name, Map attributes) {
createNode(name) + attributes
}
void nodeCompleted(maybeParent, node) {
if (maybeParent) maybeParent.steps << node
}
// unused lifecycle methods
protected void setParent(parent, child) { }
protected createNode(name, Map attributes, value) { }
protected createNode(name, value) { }
}
接下来,我们将编写一个小的辅助方法,该方法递归地累加所有子步骤的距离,并根据需要考虑重复的步骤。
def total(map) {
if (map.distance) return map.distance
def repeat = map.repeat ?: 1
repeat * map.steps.sum{ total(it) }
}
最后,我们现在可以使用我们的构建器和辅助方法来创建一个游泳训练计划并检查其总距离
def training = new TrainingBuilder1()
def monday = training.swimming {
warmup(repeat: 3) {
freestyle(distance: 50)
breaststroke(distance: 50)
}
endurance(repeat: 20) {
freestyle(distance: 50, break: 15)
}
warmdown {
kick(distance: 100)
choice(distance: 100)
}
}
assert 1500 == total(monday)
FactoryBuilderSupport
构建构建器的第二种方法是子类化 FactoryBuilderSupport
。此构建器具有与 BuilderSupport
类似的目标,但具有额外的功能以简化领域类构建。
通过这种方法,通常的想法是覆盖 FactoryBuilderSupport
抽象类中的一个或多个生命周期方法,包括 resolveFactory
、nodeCompleted
和 postInstantiate
方法。
我们将使用与之前 BuilderSupport
示例相同的示例;一个跟踪体育训练项目的构建器。
在这个例子中,我们不使用映射和列表来捕获训练程序,而是使用一些简单的领域类。
构建器实现如下
-
覆盖
resolveFactory
方法以返回一个特殊的工厂,该工厂通过将迷你 DSL 中使用的名称大写来返回类。 -
每当我们完成一个节点时,我们都会将该节点添加到父节点的步骤列表中(如果有的话)。
代码,包括特殊工厂类的代码,如下所示
import static org.apache.groovy.util.BeanUtils.capitalize
class TrainingBuilder2 extends FactoryBuilderSupport {
def factory = new TrainingFactory(loader: getClass().classLoader)
protected Factory resolveFactory(name, Map attrs, value) {
factory
}
void nodeCompleted(maybeParent, node) {
if (maybeParent) maybeParent.steps << node
}
}
class TrainingFactory extends AbstractFactory {
ClassLoader loader
def newInstance(FactoryBuilderSupport fbs, name, value, Map attrs) {
def clazz = loader.loadClass(capitalize(name))
value ? clazz.newInstance(value: value) : clazz.newInstance()
}
}
我们不使用列表和映射,而是使用一些简单的领域类和相关的特性
trait HasDistance {
int distance
}
trait Container extends HasDistance {
List steps = []
int repeat
}
class Cycling implements Container { }
class Interval implements Container { }
class Sprint implements HasDistance {}
class Tempo implements HasDistance {}
就像 BuilderSupport
示例一样,有一个辅助方法来计算训练期间覆盖的总距离会很有用。实现与我们之前的示例非常相似,但已根据我们新定义的特性进行了调整。
def total(HasDistance c) {
c.distance
}
def total(Container c) {
if (c.distance) return c.distance
def repeat = c.repeat ?: 1
repeat * c.steps.sum{ total(it) }
}
最后,我们现在可以使用新的构建器和辅助方法来创建一个自行车训练计划并检查其总距离
def training = new TrainingBuilder2()
def tuesday = training.cycling {
interval(repeat: 5) {
sprint(distance: 400)
tempo(distance: 3600)
}
}
assert 20000 == total(tuesday)
3.22. 使用 JMX
3.22.1. 简介
Java 管理扩展 (JMX) 技术提供了一种管理 JDK 上应用程序、设备和服务等资源的标准方法。每个要管理的资源都由一个管理 Bean(或MBean)表示。鉴于 Groovy 直接位于 Java 之上,Groovy 可以利用 JMX 为 Java 已经完成的大量工作。此外,Groovy 在 groovy-jmx
模块中提供了一个 GroovyMBean
类,它使 MBean 看起来像一个普通的 Groovy 对象,并简化了 Groovy 代码以与 MBean 交互。例如,以下代码
println server.getAttribute(beanName, 'Age')
server.setAttribute(beanName, new Attribute('Name', 'New name'))
Object[] params = [5, 20]
String[] signature = [Integer.TYPE, Integer.TYPE]
println server.invoke(beanName, 'add', params, signature)
可以简化为
def mbean = new GroovyMBean(server, beanName)
println mbean.Age
mbean.Name = 'New name'
println mbean.add(5, 20)
本页的其余部分将向您展示如何
-
使用 MXBean 监控 JVM
-
监控 Apache Tomcat 并显示统计信息
-
监控 Oracle OC4J 并显示信息
-
监控 BEA WebLogic 并显示信息
-
利用 Spring 的 MBean 注解支持将您的 Groovy bean 导出为 MBean
3.22.2. 监控 JVM
MBeans 不直接由应用程序访问,而是由一个名为MBean 服务器的存储库管理。Java 包含一个特殊的 MBean 服务器,称为平台 MBean 服务器,它内置在 JVM 中。平台 MBeans 在此服务器中使用唯一名称注册。
您可以使用以下代码通过其平台 MBean 监控 JVM
import java.lang.management.*
def os = ManagementFactory.operatingSystemMXBean
println """OPERATING SYSTEM:
\tarchitecture = $os.arch
\tname = $os.name
\tversion = $os.version
\tprocessors = $os.availableProcessors
"""
def rt = ManagementFactory.runtimeMXBean
println """RUNTIME:
\tname = $rt.name
\tspec name = $rt.specName
\tvendor = $rt.specVendor
\tspec version = $rt.specVersion
\tmanagement spec version = $rt.managementSpecVersion
"""
def cl = ManagementFactory.classLoadingMXBean
println """CLASS LOADING SYSTEM:
\tisVerbose = ${cl.isVerbose()}
\tloadedClassCount = $cl.loadedClassCount
\ttotalLoadedClassCount = $cl.totalLoadedClassCount
\tunloadedClassCount = $cl.unloadedClassCount
"""
def comp = ManagementFactory.compilationMXBean
println """COMPILATION:
\ttotalCompilationTime = $comp.totalCompilationTime
"""
def mem = ManagementFactory.memoryMXBean
def heapUsage = mem.heapMemoryUsage
def nonHeapUsage = mem.nonHeapMemoryUsage
println """MEMORY:
HEAP STORAGE:
\tcommitted = $heapUsage.committed
\tinit = $heapUsage.init
\tmax = $heapUsage.max
\tused = $heapUsage.used
NON-HEAP STORAGE:
\tcommitted = $nonHeapUsage.committed
\tinit = $nonHeapUsage.init
\tmax = $nonHeapUsage.max
\tused = $nonHeapUsage.used
"""
ManagementFactory.memoryPoolMXBeans.each { mp ->
println "\tname: " + mp.name
String[] mmnames = mp.memoryManagerNames
mmnames.each{ mmname ->
println "\t\tManager Name: $mmname"
}
println "\t\tmtype = $mp.type"
println "\t\tUsage threshold supported = " + mp.isUsageThresholdSupported()
}
println()
def td = ManagementFactory.threadMXBean
println "THREADS:"
td.allThreadIds.each { tid ->
println "\tThread name = ${td.getThreadInfo(tid).threadName}"
}
println()
println "GARBAGE COLLECTION:"
ManagementFactory.garbageCollectorMXBeans.each { gc ->
println "\tname = $gc.name"
println "\t\tcollection count = $gc.collectionCount"
println "\t\tcollection time = $gc.collectionTime"
String[] mpoolNames = gc.memoryPoolNames
mpoolNames.each { mpoolName ->
println "\t\tmpool name = $mpoolName"
}
}
运行时,您将看到如下内容:
OPERATING SYSTEM: architecture = amd64 name = Windows 10 version = 10.0 processors = 12 RUNTIME: name = 724176@QUOKKA spec name = Java Virtual Machine Specification vendor = Oracle Corporation spec version = 11 management spec version = 2.0 CLASS LOADING SYSTEM: isVerbose = false loadedClassCount = 6962 totalLoadedClassCount = 6969 unloadedClassCount = 0 COMPILATION: totalCompilationTime = 7548 MEMORY: HEAP STORAGE: committed = 645922816 init = 536870912 max = 8560574464 used = 47808352 NON-HEAP STORAGE: committed = 73859072 init = 7667712 max = -1 used = 70599520 name: CodeHeap 'non-nmethods' Manager Name: CodeCacheManager mtype = Non-heap memory Usage threshold supported = true name: Metaspace Manager Name: Metaspace Manager mtype = Non-heap memory Usage threshold supported = true name: CodeHeap 'profiled nmethods' Manager Name: CodeCacheManager mtype = Non-heap memory Usage threshold supported = true name: Compressed Class Space Manager Name: Metaspace Manager mtype = Non-heap memory Usage threshold supported = true name: G1 Eden Space Manager Name: G1 Old Generation Manager Name: G1 Young Generation mtype = Heap memory Usage threshold supported = false name: G1 Old Gen Manager Name: G1 Old Generation Manager Name: G1 Young Generation mtype = Heap memory Usage threshold supported = true name: G1 Survivor Space Manager Name: G1 Old Generation Manager Name: G1 Young Generation mtype = Heap memory Usage threshold supported = false name: CodeHeap 'non-profiled nmethods' Manager Name: CodeCacheManager mtype = Non-heap memory Usage threshold supported = true THREADS: Thread name = Reference Handler Thread name = Finalizer Thread name = Signal Dispatcher Thread name = Attach Listener Thread name = Common-Cleaner Thread name = Java2D Disposer Thread name = AWT-Shutdown Thread name = AWT-Windows Thread name = Image Fetcher 0 Thread name = AWT-EventQueue-0 Thread name = D3D Screen Updater Thread name = DestroyJavaVM Thread name = TimerQueue Thread name = Thread-0 GARBAGE COLLECTION: name = G1 Young Generation collection count = 6 collection time = 69 mpool name = G1 Eden Space mpool name = G1 Survivor Space mpool name = G1 Old Gen name = G1 Old Generation collection count = 0 collection time = 0 mpool name = G1 Eden Space mpool name = G1 Survivor Space mpool name = G1 Old Gen
3.22.3. 监控 Tomcat
首先启动 Tomcat,通过设置以下内容启用 JMX 监控
set JAVA_OPTS=-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9004\
-Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false
您可以在启动脚本中执行此操作,并且可以选择任何可用的端口,我们使用了 9004。
以下代码使用 JMX 发现正在运行的 Tomcat 中可用的 MBean,确定哪些是 Web 模块,提取每个 Web 模块的处理时间,并使用 JFreeChart 以图表形式显示结果
import groovy.swing.SwingBuilder
import groovy.jmx.GroovyMBean
import javax.management.ObjectName
import javax.management.remote.JMXConnectorFactory as JmxFactory
import javax.management.remote.JMXServiceURL as JmxUrl
import javax.swing.WindowConstants as WC
import org.jfree.chart.ChartFactory
import org.jfree.data.category.DefaultCategoryDataset as Dataset
import org.jfree.chart.plot.PlotOrientation as Orientation
def serverUrl = 'service:jmx:rmi:///jndi/rmi://:9004/jmxrmi'
def server = JmxFactory.connect(new JmxUrl(serverUrl)).MBeanServerConnection
def serverInfo = new GroovyMBean(server, 'Catalina:type=Server').serverInfo
println "Connected to: $serverInfo"
def query = new ObjectName('Catalina:*')
String[] allNames = server.queryNames(query, null)
def modules = allNames.findAll { name ->
name.contains('j2eeType=WebModule')
}.collect{ new GroovyMBean(server, it) }
println "Found ${modules.size()} web modules. Processing ..."
def dataset = new Dataset()
modules.each { m ->
println m.name()
dataset.addValue m.processingTime, 0, m.path
}
def labels = ['Time per Module', 'Module', 'Time']
def options = [false, true, true]
def chart = ChartFactory.createBarChart(*labels, dataset,
Orientation.VERTICAL, *options)
def swing = new SwingBuilder()
def frame = swing.frame(title:'Catalina Module Processing Time', defaultCloseOperation:WC.DISPOSE_ON_CLOSE) {
panel(id:'canvas') { rigidArea(width:800, height:350) }
}
frame.pack()
frame.show()
chart.draw(swing.canvas.graphics, swing.canvas.bounds)
运行时,我们将看到进度跟踪
Connected to: Apache Tomcat/9.0.37 Found 5 web modules. Processing ... Catalina:j2eeType=WebModule,name=///docs,J2EEApplication=none,J2EEServer=none Catalina:j2eeType=WebModule,name=///manager,J2EEApplication=none,J2EEServer=none Catalina:j2eeType=WebModule,name=///,J2EEApplication=none,J2EEServer=none Catalina:j2eeType=WebModule,name=///examples,J2EEApplication=none,J2EEServer=none Catalina:j2eeType=WebModule,name=///host-manager,J2EEApplication=none,J2EEServer=none
输出将如下所示:
注意:如果您在运行此脚本时遇到错误,请参阅下面的故障排除部分。
3.22.4. OC4J 示例
这是一个脚本,用于访问 OC4J 并打印出有关服务器、其运行时以及(作为示例)配置的 JMS 目标的一些信息
import javax.management.remote.*
import oracle.oc4j.admin.jmx.remote.api.JMXConnectorConstant
def serverUrl = new JMXServiceURL('service:jmx:rmi://:23791')
def serverPath = 'oc4j:j2eeType=J2EEServer,name=standalone'
def jvmPath = 'oc4j:j2eeType=JVM,name=single,J2EEServer=standalone'
def provider = 'oracle.oc4j.admin.jmx.remote'
def credentials = [
(JMXConnectorConstant.CREDENTIALS_LOGIN_KEY): 'oc4jadmin',
(JMXConnectorConstant.CREDENTIALS_PASSWORD_KEY): 'admin'
]
def env = [
(JMXConnectorFactory.PROTOCOL_PROVIDER_PACKAGES): provider,
(JMXConnector.CREDENTIALS): credentials
]
def server = JmxFactory.connect(serverUrl, env).MBeanServerConnection
def serverInfo = new GroovyMBean(server, serverPath)
def jvmInfo = new GroovyMBean(server, jvmPath)
println """Connected to $serverInfo.node. \
Server started ${new Date(serverInfo.startTime)}.
OC4J version: $serverInfo.serverVersion from $serverInfo.serverVendor
JVM version: $jvmInfo.javaVersion from $jvmInfo.javaVendor
Memory usage: $jvmInfo.freeMemory bytes free, \
$jvmInfo.totalMemory bytes total
"""
def query = new javax.management.ObjectName('oc4j:*')
String[] allNames = server.queryNames(query, null)
def dests = allNames.findAll { name ->
name.contains('j2eeType=JMSDestinationResource')
}.collect { new GroovyMBean(server, it) }
println "Found ${dests.size()} JMS destinations. Listing ..."
dests.each { d -> println "$d.name: $d.location" }
以下是运行此脚本的结果
Connected to LYREBIRD. Server started Thu May 31 21:04:54 EST 2007. OC4J version: 11.1.1.0.0 from Oracle Corp. JVM version: 1.6.0_01 from Sun Microsystems Inc. Memory usage: 8709976 bytes free, 25153536 bytes total Found 5 JMS destinations. Listing ... Demo Queue: jms/demoQueue Demo Topic: jms/demoTopic jms/Oc4jJmsExceptionQueue: jms/Oc4jJmsExceptionQueue jms/RAExceptionQueue: jms/RAExceptionQueue OracleASRouter_store: OracleASRouter_store
作为一个小变体,此脚本使用 JFreeChart 显示内存使用情况的饼图
import org.jfree.chart.ChartFactory
import javax.swing.WindowConstants as WC
import javax.management.remote.*
import oracle.oc4j.admin.jmx.remote.api.JMXConnectorConstant
def url = 'service:jmx:rmi://:23791'
def credentials = [:]
credentials[JMXConnectorConstant.CREDENTIALS_LOGIN_KEY] = "oc4jadmin"
credentials[JMXConnectorConstant.CREDENTIALS_PASSWORD_KEY] = "password"
def env = [:]
env[JMXConnectorFactory.PROTOCOL_PROVIDER_PACKAGES] = "oracle.oc4j.admin.jmx.remote"
env[JMXConnector.CREDENTIALS] = credentials
def server = JMXConnectorFactory.connect(new JMXServiceURL(url), env).MBeanServerConnection
def jvmInfo = new GroovyMBean(server, 'oc4j:j2eeType=JVM,name=single,J2EEServer=standalone')
def piedata = new org.jfree.data.general.DefaultPieDataset()
piedata.setValue "Free", jvmInfo.freeMemory
piedata.setValue "Used", jvmInfo.totalMemory - jvmInfo.freeMemory
def options = [true, true, true]
def chart = ChartFactory.createPieChart('OC4J Memory Usage', piedata, *options)
chart.backgroundPaint = java.awt.Color.white
def swing = new groovy.swing.SwingBuilder()
def frame = swing.frame(title:'OC4J Memory Usage', defaultCloseOperation:WC.EXIT_ON_CLOSE) {
panel(id:'canvas') { rigidArea(width:350, height:250) }
}
frame.pack()
frame.show()
chart.draw(swing.canvas.graphics, swing.canvas.bounds)
看起来像
3.22.5. WebLogic 示例
此脚本打印出有关服务器的信息,然后是有关 JMS 目标的信息(作为示例)。还有许多其他 MBean 可用。
import javax.management.remote.*
import javax.management.*
import javax.naming.Context
import groovy.jmx.GroovyMBean
def urlRuntime = '/jndi/weblogic.management.mbeanservers.runtime'
def urlBase = 'service:jmx:t3://:7001'
def serviceURL = new JMXServiceURL(urlBase + urlRuntime)
def h = new Hashtable()
h.put(Context.SECURITY_PRINCIPAL, 'weblogic')
h.put(Context.SECURITY_CREDENTIALS, 'weblogic')
h.put(JMXConnectorFactory.PROTOCOL_PROVIDER_PACKAGES, 'weblogic.management.remote')
def server = JMXConnectorFactory.connect(serviceURL, h).MBeanServerConnection
def domainName = new ObjectName('com.bea:Name=RuntimeService,Type=weblogic.management.mbeanservers.runtime.RuntimeServiceMBean')
def rtName = server.getAttribute(domainName, 'ServerRuntime')
def rt = new GroovyMBean(server, rtName)
println "Server: name=$rt.Name, state=$rt.State, version=$rt.WeblogicVersion"
def destFilter = Query.match(Query.attr('Type'), Query.value('JMSDestinationRuntime'))
server.queryNames(new ObjectName('com.bea:*'), destFilter).each { name ->
def jms = new GroovyMBean(server, name)
println "JMS Destination: name=$jms.Name, type=$jms.DestinationType, messages=$jms.MessagesReceivedCount"
}
这是输出
Server: name=examplesServer, state=RUNNING, version=WebLogic Server 10.0 Wed May 9 18:10:27 EDT 2007 933139 JMS Destination: name=examples-jms!exampleTopic, type=Topic, messages=0 JMS Destination: name=examples-jms!exampleQueue, type=Queue, messages=0 JMS Destination: name=examples-jms!jms/MULTIDATASOURCE_MDB_QUEUE, type=Queue, messages=0 JMS Destination: name=examplesJMSServer!examplesJMSServer.TemporaryQueue0, type=Queue, messages=68 JMS Destination: name=examples-jms!quotes, type=Topic, messages=0 JMS Destination: name=examples-jms!weblogic.wsee.wseeExamplesDestinationQueue, type=Queue, messages=0 JMS Destination: name=examples-jms!weblogic.examples.ejb30.ExampleQueue, type=Queue, messages=0
3.22.6. Spring 示例
您还可以使用 Spring 自动将 bean 注册为 JMX 感知。
这是一个示例类 (Calculator.groovy)
import org.springframework.jmx.export.annotation.*
@ManagedResource(objectName="bean:name=calcMBean", description="Calculator MBean")
public class Calculator {
private int invocations
@ManagedAttribute(description="The Invocation Attribute")
public int getInvocations() {
return invocations
}
private int base = 10
@ManagedAttribute(description="The Base to use when adding strings")
public int getBase() {
return base
}
@ManagedAttribute(description="The Base to use when adding strings")
public void setBase(int base) {
this.base = base
}
@ManagedOperation(description="Add two numbers")
@ManagedOperationParameters([
@ManagedOperationParameter(name="x", description="The first number"),
@ManagedOperationParameter(name="y", description="The second number")])
public int add(int x, int y) {
invocations++
return x + y
}
@ManagedOperation(description="Add two strings representing numbers of a particular base")
@ManagedOperationParameters([
@ManagedOperationParameter(name="x", description="The first number"),
@ManagedOperationParameter(name="y", description="The second number")])
public String addStrings(String x, String y) {
invocations++
def result = Integer.valueOf(x, base) + Integer.valueOf(y, base)
return Integer.toString(result, base)
}
}
这是 Spring 配置文件 (beans.xml)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="mbeanServer"
class="org.springframework.jmx.support.MBeanServerFactoryBean">
<property name="locateExistingServerIfPossible" value="true"/>
</bean>
<bean id="exporter"
class="org.springframework.jmx.export.MBeanExporter">
<property name="assembler" ref="assembler"/>
<property name="namingStrategy" ref="namingStrategy"/>
<property name="beans">
<map>
<entry key="bean:name=defaultCalcName" value-ref="calcBean"/>
</map>
</property>
<property name="server" ref="mbeanServer"/>
<property name="autodetect" value="true"/>
</bean>
<bean id="jmxAttributeSource"
class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/>
<!-- will create management interface using annotation metadata -->
<bean id="assembler"
class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler">
<property name="attributeSource" ref="jmxAttributeSource"/>
</bean>
<!-- will pick up the ObjectName from the annotation -->
<bean id="namingStrategy"
class="org.springframework.jmx.export.naming.MetadataNamingStrategy">
<property name="attributeSource" ref="jmxAttributeSource"/>
</bean>
<bean id="calcBean"
class="Calculator">
<property name="base" value="10"/>
</bean>
</beans>
这是一个使用此 bean 和配置的脚本
import org.springframework.context.support.ClassPathXmlApplicationContext
import java.lang.management.ManagementFactory
import javax.management.ObjectName
import javax.management.Attribute
import groovy.jmx.GroovyMBean
// get normal bean
def ctx = new ClassPathXmlApplicationContext("beans.xml")
def calc = ctx.getBean("calcBean")
Thread.start {
// access bean via JMX, use a separate thread just to
// show that we could access remotely if we wanted
def server = ManagementFactory.platformMBeanServer
def mbean = new GroovyMBean(server, 'bean:name=calcMBean')
sleep 1000
assert 8 == mbean.add(7, 1)
mbean.Base = 8
assert '10' == mbean.addStrings('7', '1')
mbean.Base = 16
sleep 2000
println "Number of invocations: $mbean.Invocations"
println mbean
}
assert 15 == calc.add(9, 6)
assert '11' == calc.addStrings('10', '1')
sleep 2000
assert '20' == calc.addStrings('1f', '1')
以下是生成的输出
Number of invocations: 5 MBean Name: bean:name=calcMBean Attributes: (rw) int Base (r) int Invocations Operations: int add(int x, int y) java.lang.String addStrings(java.lang.String x, java.lang.String y) int getInvocations() int getBase() void setBase(int p1)
您甚至可以在进程运行时使用 jconsole 附加到进程。它看起来会像这样
我们用 -Dcom.sun.management.jmxremote
JVM 参数启动了 Groovy 应用程序。
另请参阅
3.22.7. 故障排除
java.lang.SecurityException
如果您收到以下错误,则您的容器的 JMX 访问受到密码保护
java.lang.SecurityException: Authentication failed! Credentials required
要解决此问题,请在连接时添加一个带有凭据的环境,如下所示(密码必须在此之前设置)
def jmxEnv = null
if (password != null) {
jmxEnv = [(JMXConnector.CREDENTIALS): (String[])["monitor", password]]
}
def connector = JMXConnectorFactory.connect(new JMXServiceURL(serverUrl), jmxEnv)
您尝试监视/管理的软件的详细信息可能略有不同。如果适用,请查看上面使用凭据的其他示例(例如 OC4J 和 WebLogic)。如果您仍然遇到问题,您需要查阅您尝试监视/管理的软件的文档,了解如何提供凭据的详细信息。
3.22.8. JmxBuilder
JmxBuilder 是一个基于 Groovy 的 Java 管理扩展 (JMX) API 领域特定语言。它使用构建器模式 (FactoryBuilder) 创建一个内部 DSL,便于通过 MBean 服务器将 POJO 和 Groovy bean 公开为管理组件。JmxBuilder 隐藏了通过 JMX API 创建和导出管理 bean 的复杂性,并提供了一组自然的 Groovy 构造来与 JMX 基础设施交互。
实例化 JmxBuilder
要开始使用 JmxBuilder,只需确保 jar 文件在您的类路径中。然后您可以在您的代码中执行以下操作
def jmx = new JmxBuilder()
就是这样!您现在可以使用 JmxBuilder 了。
注意
-
您可以将您自己的 MBeanServer实例传递给构建器(JmxBuilder(MBeanServer))
-
如果未指定 MBeanServer,则构建器实例将默认为底层平台 MBeanServer。
一旦您有了 JmxBuilder 的实例,您就可以调用它的任何构建器节点了。
JMX 连接器
远程连接是 JMX 架构的关键部分。JmxBuilder 通过最少量的编码即可方便地创建连接器服务器和连接器客户端。
连接器服务器
JmxBuilder.connectorServer() 支持完整的连接器 API 语法,允许您指定属性、覆盖 URL、指定自己的主机等。
语法
jmx.connectorServer( protocol:"rmi", host:"...", port:1099, url:"...", properties:[ "authenticate":true|false, "passwordFile":"...", "accessFile":"...", "sslEnabled" : true | false // any valid connector property ] )
请注意,serverConnector 节点将接受四个 ServerConnector 属性别名(authenticate、passwordFile、accessFile 和 sslEnabled)。您可以使用这些别名或提供任何 RMI 支持的属性。
示例 - 连接器服务器(请参阅下面的更正)
jmx.connectorServer(port: 9000).start()
上面的代码片段返回一个 RMI 连接器,它将开始监听端口 9000。默认情况下,构建器将内部生成 URL "service:jmx:rmi:///jndi/rmi://:9000/jmxrmi"。
注意:遗憾的是,当尝试运行前面的代码片段时(示例不完整,请参阅下文),您很可能会遇到以下情况:
Caught: java.io.IOException: Cannot bind to URL [rmi://:9000/jmxrmi]: javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: localhost; nested exception is: ?????? java.net.ConnectException: Connection refused] ??
这发生在 Mac 和 Linux (CentOS 5) 上,并安装了 Groovy 1.6。也许对 /etc/hosts 文件的配置做了假设?
正确的例子如下所示。 |
连接器示例(已更正) - 连接器服务器
上述示例未创建 RMI 注册表。因此,为了导出,您必须首先导出 RMI 对象注册表(确保导入 java.rmi.registry.LocateRegistry
)。
import java.rmi.registry.LocateRegistry
//...
LocateRegistry.createRegistry(9000)
jmx.connectorServer(port: 9000).start()
连接器客户端
JmxBuilder.connectorClient() 节点允许您创建 JMX 连接器客户端对象以连接到 JMX MBean 服务器。
语法
jmx.connectorClient ( protocol:"rmi", host:"...", port:1099, url:"...", )
示例 - 客户端连接器
创建连接器客户端同样简单。只需一行代码,您就可以创建一个 JMX 连接器客户端实例,如下所示。
def client = jmx.connectorClient(port: 9000)
client.connect()
然后,您可以使用以下方法访问与连接器关联的 MBeanServerConnection
client.getMBeanServerConnection()
JmxBuilder MBean 导出
您可以通过最少的编码导出 Java 对象或 Groovy 对象。JmxBuilder 甚至会查找并导出在运行时注入的动态 Groovy 方法。
隐式与显式描述符
使用构建器时,您可以让 JmxBuilder 隐式生成所有 MBean 描述符信息。当您想编写最少的代码以快速导出 bean 时,这很有用。您还可以显式声明 bean 的所有描述符信息。这使您可以完全控制如何描述要为底层 bean 导出的每条信息。
JmxBuilder.export() 节点
JmxBuilder.export() 节点提供了一个容器,所有要导出到 MBeanServer 的管理实体都放置在此容器中。您可以将一个或多个 bean() 或 timer() 节点作为 export() 节点的子节点放置。JmxBuilder 将自动批量导出节点描述的实体到 MBean 服务器进行管理(请参见下面的示例)。
def beans = jmx.export {
bean(new Foo())
bean(new Bar())
bean(new SomeBar())
}
在上面的代码片段中,JmxBuilder.export() 将导出三个管理 bean 到 MBean 服务器。
JmxBuilder.export() 语法
JmxBuilder.export() 节点支持 registrationPolicy 参数,用于指定 JmxBuilder 在 MBean 注册期间如何处理 bean 名称冲突
jmx.export(policy:"replace|ignore|error") or jmx.export(regPolicy:"replace|ignore|error")
-
替换 - JmxBuilder.export() 将在导出期间替换任何已注册的 bean。
-
忽略 - 如果已注册相同的 bean,则将忽略正在导出的 bean。
-
错误 - JmxBuilder.export() 在注册期间发生 bean 名称冲突时抛出错误。
与 GroovyMBean 类集成
当您将 MBean 导出到 MBeanServer 时,JmxBuilder 将返回一个 GroovyMBean 实例,表示已由构建器导出的管理 bean。诸如 bean() 和 timer() 等节点在被调用时将返回 GroovyMBean 实例。export() 节点返回一个 GroovyMBean[] 数组,表示所有导出到 MBean 服务器的托管对象。
使用 JmxBuilder.bean() 注册 MBean
本参考资料的这一部分使用类 RequestController 来演示如何使用 JmxBuilder 导出运行时管理 bean。该类仅用于说明目的,可以是 POJO 或 Groovy bean。
RequestController
class RequestController {
// constructors
RequestController() { super() }
RequestController(Map resource) { }
// attributes
boolean isStarted() { true }
int getRequestCount() { 0 }
int getResourceCount() { 0 }
void setRequestLimit(int limit) { }
int getRequestLimit() { 0 }
// operations
void start() { }
void stop() { }
void putResource(String name, Object resource) { }
void makeRequest(String res) { }
void makeRequest() { }
}
如前所述,您可以使用 JmxBuilder 灵活的语法导出任何没有描述符的 POJO/POGO。构建器可以自动使用隐式默认值描述管理 bean 的所有方面。这些默认值可以很容易地被覆盖,我们将在下一节中看到这一点。
导出 POJO 或 POGO 的最简单方法如下所示。
jmx.export {
bean(new RequestController(resource: "Hello World"))
}
这是什么意思
-
首先,JmxBuilder.export() 节点将导出一个 MBean 到 MBeanServer,表示声明的 POJO 实例。
-
构建器将为 MBean 和所有其他 MBean 描述符信息生成默认的 ObjectName。
-
JmxBuilder 将自动导出实例上所有声明的属性(MBean getter/setter)、构造函数和操作。
-
导出的属性将具有只读可见性。
请记住,JmxBuilder.export() 返回所有导出实例的 GroovyMBean[] 对象数组。因此,一旦您调用 JmxBuilder.export(),您就可以立即访问底层 MBean 代理(通过 GroovyMBean)。
JmxBuilder.bean() 语法
JmxBuilder.bean() 节点支持一组广泛的描述符,用于描述您的 bean 以进行管理。JMX MBeanServer 使用这些描述符来公开有关用于管理的 bean 的元数据。
jmx.export { bean( target:bean instance, name:ObjectName, desc:"...", attributes:"*", attributes:[] attributes:[ "AttrubuteName1","AttributeName2",...,"AttributeName_n" ] attributes:[ "AttributeName":"*", "AttributeName":[ desc:"...", defaultValue:value, writable:true|false, editable:true|false, onChange:{event-> // event handler} ] ], constructors:"*", constructors:[ "Constructor Name":[], "Constructor Name":[ "ParamType1","ParamType2,...,ParamType_n" ], "Constructor Name":[ desc:"...", params:[ "ParamType1":"*", "ParamType2":[desc:"...", name:"..."],..., "ParamType_n":[desc:"...", name:"..."] ] ] ], operations:"*", operations:[ "OperationName1", "OperationName2",...,"OperationNameN" ], operations:[ "OperationName1":"*", "OperationName2":[ "type1","type2,"type3" ] "OperationName3":[ desc:"...", params:[ "ParamType1":"*" "ParamType2":[desc:"...", name:"..."],..., "ParamType_n":[desc:"...", name:"..."] ], onInvoked:{event-> JmxBuilder.send(event:"", to:"")} ] ], listeners:[ "ListenerName1":[event: "...", from:ObjectName, call:{event->}], "ListenerName2":[event: "...", from:ObjectName, call:&methodPointer] ] ) }
我们将不描述整个节点,以下部分将分别探讨每个属性。
Bean() 节点 - 指定 MBean ObjectName
使用 bean() 节点描述符,您可以指定自己的 MBean ObjectName。
def ctrl = new RequestController(resource:"Hello World")
def beans = jmx.export {
bean(target: ctrl, name: "jmx.tutorial:type=Object")
}
ObjectName 可以指定为 String 或 ObjectName 实例。
Bean() 节点 - 属性导出
JMX 属性是底层 bean 上的 setter 和 getter。JmxBuilder.bean() 节点提供了多种灵活描述和导出 MBean 属性的方法。您可以随意组合它们,以实现任何级别的属性可见性。让我们看看。
使用通配符 "*" 导出所有属性
以下代码片段将描述并导出 bean 上的所有属性为只读。JmxBuilder 将使用默认值来描述导出的管理属性。
def objName = new ObjectName("jmx.tutorial:type=Object")
def beans = jmx.export {
bean(target: new RequestController(),
name: objName,
attributes: "*")
}
导出属性列表
JmxBuilder 将允许您指定要导出的属性列表。
def objName = new ObjectName("jmx.tutorial:type=Object")
def beans = jmx.export {
bean(
target: new RequestController(),
name: objName,
attributes: ["Resource", "RequestCount"]
)
}
在上面的代码片段中,只会导出“Resource”和“RequestCount”属性。同样,由于未提供描述符,JmxBuilder 将使用合理的默认值来描述导出的属性。
使用显式描述符导出属性
JmxBuilder 的优势之一是其在描述 MBean 方面的灵活性。使用构建器,您可以描述要导出到 MBeanServer 的 MBean 属性的所有方面(请参阅上面的语法)。
def objName = new ObjectName("jmx.tutorial:type=Object")
def beans = jmx.export {
bean(
target: new RequestController(),
name: objName,
attributes: [
"Resource": [desc: "The resource to request.", readable: true, writable: true, defaultValue: "Hello"],
"RequestCount": "*"
]
)
}
在上面的代码片段中,属性“Resource”使用所有支持的 JMX 属性描述符(即 desc、readable、writable、defaultValue)进行完全描述。但是,我们使用通配符描述属性RequestCount,它将使用默认值导出和描述。
Bean() 节点 - 构造函数导出
JmxBuilder 支持显式描述和导出底层 bean 中定义的构造函数。导出构造函数时有几个选项可用。您可以随意组合它们以实现所需的管理级别。
使用 "*" 导出所有构造函数
您可以使用构建器的特殊“*”表示法来导出底层 bean 上声明的所有构造函数。构建器将使用默认值来描述 MBean 构造函数。
def objName = new ObjectName("jmx.tutorial:type=Object")
def beans = jmx.export {
bean(
target: new RequestController(),
name: objName,
constructors: "*"
)
}
使用参数描述符导出构造函数
JmxBuilder 允许您通过描述参数签名来指定要导出的特定构造函数。当您有多个具有不同参数签名并想要导出特定构造函数时,这很有用。
def objName = new ObjectName("jmx.tutorial:type=Object")
def beans = jmx.export {
bean(
target: new RequestController(),
name: objName,
constructors: [
"RequestController": ["Object"]
]
)
}
在这里,JmxBuilder 将导出接受一个“Object”类型参数的构造函数。同样,JmxBuilder 将使用默认值来填充构造函数和参数的描述。
使用显式描述符导出构造函数
JmxBuilder 允许您完全描述要导出的构造函数(请参阅上面的语法)。
def objName = new ObjectName("jmx.tutorial:type=Object")
def beans = jmx.export {
bean(target: new RequestController(), name: objName,
constructors: [
"RequestController": [
desc: "Constructor takes param",
params: ["Object" : [name: "Resource", desc: "Resource for controller"]]
]
]
)
}
在上面的代码中,JmxBuilder 将把接受一个参数的构造函数导出到 MBeanServer。请注意,构造函数可以使用所有可选描述符键(包括参数描述符)进行完全描述。
Bean() 节点 - 操作导出
与构造函数类似,JmxBuilder 支持使用灵活的符号描述和导出 MBean 操作(请参阅上面的语法)。您可以随意组合这些符号,以实现所需的操作管理级别。
使用 "*" 导出所有操作
您可以使用构建器的特殊“*”表示法来导出 bean 上定义的所有操作,以便进行管理。构建器将使用操作的默认描述符值进行导出。
def objName = new ObjectName("jmx.tutorial:type=Object")
def beans = jmx.export {
bean(
target: new RequestController(),
name: objName,
operations: "*"
)
}
在此代码片段中,JmxBuilder 将导出所有 bean 操作,并使用默认值在 MBeanServer 中描述它们。
导出操作列表
JmxBuilder 具有一种简写表示法,允许您通过提供要导出的方法列表来快速定位要导出的操作。
def objName = new ObjectName("jmx.tutorial:type=Object")
def beans = jmx.export {
bean(
target: new RequestController(),
name: objName,
operations: ["start", "stop"]
)
}
在上面的代码片段中,构建器将只导出 start() 和 stop() 方法。所有其他方法将被忽略。JmxBuilder 将使用默认描述符值来描述导出的操作。
按签名导出操作
使用 JmxBuilder,您可以使用方法的参数签名来定位要导出进行管理的方法。当您想要区分具有相同名称但您想要导出的方法时(即 stop() 而不是 stop(boolean)),这很有用。
def objName = new ObjectName("jmx.tutorial:type=Object")
def beans = jmx.export {
bean(
target: new RequestController(),
name: objName,
operations: [
"makeRequest": ["String"]
]
)
}
在上面的代码片段中,JmxBuilder 将选择 makeRequest(String) 方法进行导出,而不是不带参数的另一个版本 makeRequest()。在此简写上下文中,签名指定为类型列表(即“String”)。
带有显式描述符的导出操作
JmxBuilder 支持 Bean 操作的详细描述符。您可以为 Bean 上的任何操作提供详细的描述符信息,包括名称、描述、方法参数、参数类型和参数描述。
def objName = new ObjectName("jmx.tutorial:type=Object")
def beans = jmx.export {
bean(target: new RequestController(), name: objName,
operations: [
"start": [desc: "Starts request controller"],
"stop": [desc: "Stops the request controller"],
"setResource": [params: ["Object"]],
"makeRequest": [
desc: "Executes the request.",
params: [
"String": [name: "Resource", desc: "The resource to request"]
]
]
]
)
}
上面的代码片段展示了 JmxBuilder 允许您描述目标进行管理的各种操作方式。
-
操作 start() 和 stop() 由“desc”键描述(因为没有参数,所以这已经足够了)。
-
在操作 setResource() 中,使用了 params 的简写版本来描述方法的参数。
-
makeRequest() 使用扩展描述符语法来描述操作的所有方面。
嵌入描述符
JmxBuilder 支持将描述符直接嵌入到您的 Groovy 类中。因此,您无需将描述包裹在声明的对象周围(如我们在此处所见),而是可以直接将 JMX 描述符嵌入到您的类中。
RequestControllerGroovy
class RequestControllerGroovy {
// attributes
boolean started
int requestCount
int resourceCount
int requestLimit
Map resources
// operations
void start() { }
void stop(){ }
void putResource(String name, Object resource) { }
void makeRequest(String res) { }
void makeRequest() { }
static descriptor = [
name: "jmx.builder:type=EmbeddedObject",
operations: ["start", "stop", "putResource"],
attributes: "*"
]
}
// export
jmx.export(
bean(new RequestControllerGroovy())
)
上面的代码中发生了两件事:
-
定义了 Groovy 类 RequestControllerGroovy,并包含一个 static descriptor 成员。该成员用于声明 JmxBuilder 描述符,以描述目标用于 JMX 导出的类成员。
-
代码的第二部分展示了如何使用 JmxBuilder 导出该类进行管理。
定时器导出
JMX 标准规定 API 的实现必须提供定时器服务。由于 JMX 是一个基于组件的架构,定时器提供了出色的信号机制,用于与 MBeanServer 中注册的监听器组件进行通信。JmxBuilder 支持使用我们目前所见的相同简单语法创建和导出定时器。
定时器节点语法
timer( name:ObjectName, event:"...", message:"...", data:dataValue startDate:"now"|dateValue period:"99d"|"99h"|"99m"|"99s"|99 occurrences:long )
timer() 节点支持多个属性:
-
name:- 必需。定时器的合格 JMX ObjectName 实例(或字符串)。
-
event:- 每次定时信号广播时将发送的 JMX 事件类型字符串(默认为 "jmx.builder.event")。
-
message:- 可选的字符串值,可以发送给监听器。
-
data:- 可选对象,可以发送给定时信号的监听器。
-
startDate:- 何时启动定时器。有效值集 ["now", 日期对象]。默认为 "now"。
-
period:- 定时器的周期,表示为毫秒数或时间单位(天、小时、分钟、秒)。请参阅下面的描述。
-
occurrences:- 指示定时器重复次数的数字。默认为永远。
导出定时器
def timer = jmx.timer(name: "jmx.builder:type=Timer", event: "heartbeat", period: "1s")
timer.start()
上面的代码片段描述、创建并导出了一个标准 JMX 定时器组件。在这里,timer() 节点返回一个 GroovyMBean,它表示 MBeanServer 中注册的定时器 MBean。
另一种导出定时器的方式是在 JmxBuilder.export() 节点内。
def beans = jmx.export {
timer(name: "jmx.builder:type=Timer1", event: "event.signal", period: "1s")
timer(name: "jmx.builder:type=Timer2", event: "event.log", period: "1s")
}
beans[0].start()
beans[1].start()
定时器周期
timer() 节点支持灵活的符号来指定定时器周期值。您可以以秒、分钟、小时和天为单位指定时间。默认是毫秒。
-
timer(period: 100) = 100 毫秒
-
timer(period: "1s") = 1 秒
-
timer(period: "1m") = 1 分钟
-
timer(period: "1h") = 1 小时
-
timer(period: "1d") = 1 天
该节点将自动翻译。
JmxBuilder 和事件
JMX 的一个组成部分是它的事件模型。注册的管理 bean 可以通过在 MBeanServer 的事件总线上广播事件来相互通信。JmxBuilder 提供了几种简单的方法来监听和响应在 MBeanServer 事件总线上广播的事件。开发人员可以捕获总线上的任何事件,或抛出自己的事件,供 MBeanServer 上注册的其他组件使用。
事件处理闭包
JmxBuilder 利用 Groovy 的闭包来提供简单而优雅的 JMX 事件响应方式。JmxBuilder 支持两种闭包签名:
callback = { event ->
// event handling code
}
JmxBuilder 将使用此格式将一个 "event" 对象传递给闭包。事件对象包含有关被拦截事件的信息,以便处理程序可以处理它。参数将根据捕获的事件包含不同组的信息。
处理属性 onChange 事件
在描述属性时(参见上面的 bean() 节点部分),您可以提供一个闭包(或方法指针)作为回调,以便在导出 MBean 上的属性值更新时执行。这使开发人员有机会监听和响应 MBean 上的状态更改。
jmx.export {
bean(
target: new RequestController(), name: "jmx.tutorial:type=Object",
attributes: [
"Resource": [
readable: true, writable: true,
onChange: { e ->
println e.oldValue
println e.newValue
}
]
]
)
}
上面的示例代码片段展示了如何在描述 MBean 属性时指定一个 "onChange" 回调闭包。在这个示例代码中,每当通过导出的 MBean 更新属性“Resource”时,都会执行 onChange 事件。
属性 onChange 事件对象
处理属性 onChange 事件时,处理程序闭包将收到一个包含以下信息的事件对象:
-
event.oldValue - 更改事件发生前的旧属性值。
-
event.newValue - 更改后的新属性值。
-
event.attribute - 发生事件的属性名称。
-
event.attributeType - 导致事件的属性数据类型。
-
event.sequenceNumber - 表示事件序列号的数值。
-
event.timeStamp - 事件发生的时间戳。
处理操作 onCall 事件
与 MBean 属性类似,JmxBuilder 允许开发人员监听 MBeanServer 中注册的 MBean 上的操作调用。JmxBuilder 接受一个回调闭包,该闭包将在 MBean 方法调用后执行。
class EventHandler {
void handleStart(e){
println e
}
}
def handler = new EventHandler()
def beans = jmx.export {
bean(target: new RequestController(), name: "jmx.tutorial:type=Object",
operations: [
"start": [
desc:"Starts request controller",
onCall:handler.&handleStart
]
]
)
}
上面的代码片段展示了如何声明一个用作监听器的 "onCall" 闭包,当 MBean 上的“start()”操作被调用时。此示例使用方法指针语法来展示 JmxBuilder 的多功能性。
操作 onCall 事件对象
处理操作 onCall 事件时,回调闭包将收到一个包含以下信息的事件对象:
-
event.event - 已广播的事件类型字符串。
-
event.source - 调用方法的对象。
-
event.data - 导致事件的属性数据类型。
-
event.sequenceNumber - 表示事件序列号的数值。
-
event.timeStamp - 事件发生的时间戳。
监听器 MBean
当您使用 bean() 节点导出 MBean 时,您可以定义 MBean 可以监听和响应的事件。bean() 节点提供了一个“listeners:”属性,允许您定义您的 bean 可以响应的事件监听器。
def beans = jmx.export {
timer(name: "jmx.builder:type=Timer", event: "heartbeat", period: "1s").start()
bean(target: new RequestController(), name: "jmx.tutorial:type=Object",
operations: "*",
listeners: [
heartbeat: [
from: "jmx.builder:type=Timer",
call: { e ->
println e
}
]
]
)
}
在上面的示例中,我们看到了向导出 MBean 添加监听器的语法。
-
首先,定时器被导出并启动。
-
然后,声明一个 MBean,它将监听定时器事件并执行有意义的操作。
-
"heartbeat:" 名称是任意的,与上面声明的定时器没有关联。
-
事件的源通过 "from:" 属性指定。
您还可以指定您感兴趣从广播器接收的事件类型(因为广播器可以发出多个事件)。
监听 JMX 事件
在某些情况下,您会希望创建独立的事件监听器(不附加到导出的 MBean)。JmxBuilder 提供了 Listener() 节点,允许您创建可以监听 MBeanServer 事件的 JMX 监听器。这对于创建 JMX 客户端应用程序以监视/管理远程 JMX MBeanServer 上的 JMX 代理非常有用。
监听器节点语法
jmx.listener( event: "...", from: "object name" | ObjectName, call: { event-> } )
以下是 listener() 节点属性的描述:
-
event:一个可选字符串,用于标识要监听的 JMX 事件类型。
-
from(必需):要监听的组件的 JMX ObjectName。可以指定为字符串或 ObjectName 实例。
-
call:捕获到事件时要执行的闭包。也可以指定为 Groovy 方法指针。
以下是 JmxBuilder 监听器节点的示例:
jmx.timer(name: "jmx.builder:type=Timer", period: "1s").start()
jmx.listener(
from: "jmx.builder:type=Timer",
call: { e ->
println "beep..."
}
)
此示例展示了如何使用独立的监听器(在 MBean 导出之外)。在这里,我们导出了一个分辨率为 1 秒的定时器。然后,我们为该定时器指定了一个监听器,它将每秒打印“beep”。
发出 JMX 事件
JmxBuilder 提供了在 MBeanServer 的事件总线上广播您自己的事件所需的工具。对可以广播的事件类型没有限制。您只需声明您的发射器以及您要发送的事件类型,然后随时广播您的事件。MBeanServer 中注册的任何组件都可以注册自己来监听您的事件。
发射器语法
jmx.emitter(name:"Object:Name", event:"type")
Emitter() 节点的属性可以概括如下:
-
name:用于在 MBeanServer 中注册您的发射器的可选 JMX ObjectName。默认为 jmx.builder:type=Emitter,name=Emitter@OBJECT_HASH_VALUE
-
event:描述 JMX 事件类型的可选字符串值。默认为 "jmx.builder.event.emitter"。
声明发射器
def emitter = jmx.emitter()
该代码片段使用隐式描述符语法声明了发射器。JmxBuilder 将执行以下操作:
-
创建一个并注册一个具有默认 ObjectName 的发射器 MBean。
-
设置一个默认事件类型,其值为“jmx.builder.event.emitter”。
-
返回一个表示发射器的 GroovyMBean。
与构建器中的其他节点一样,您可以覆盖 emitter() 节点中的所有键。您可以指定 ObjectName 和事件类型。
广播事件
一旦您声明了发射器,您就可以广播您的事件。
emitter.send()
上面的示例展示了发射器在声明后发送事件。MBeanServer 中注册的任何 JMX 组件都可以注册接收来自此发射器的消息。
3.23. 创建 Swing 用户界面
由于使用了 SwingBuilder,创建 Swing 用户界面变得非常简单。
3.24. 安全
安全是一个复杂且多方面的问题,需要以整体的方式来解决。Groovy 提供了一些功能来提高安全性,但关注安全性的组织应该已经解决了其他必要的方面,例如网络安全、文件系统安全、操作系统安全、数据库安全、密码和可能的加密。
此外,由于 Groovy 运行在 JDK 上并可选地使用其他库依赖项,用户应确保其 JDK 和所有依赖项都已更新到最新的安全修复程序。
关于可能影响 Groovy 项目本身的安全问题,该项目遵循 Apache 处理安全漏洞的通用指南。另请参阅项目的安全策略和过去的漏洞列表。
由于运行在 JVM 上并遵循各种 Java 约定,Groovy 程序提供了一些与 Java 程序相同的安全功能,包括:
-
程序无法访问任意内存位置
-
最终变量无法更改
-
数组边界被检查
-
类加载器在加载类时执行字节码验证
-
不能强制转换为不兼容的类
-
可访问用于加密和认证的 API
通过以下方式提供特殊安全支持:
-
groovy.lang.GroovyShell、groovy.lang.GroovyClassLoader 和 Groovy 运行时的其他部分完全支持 Java 安全管理器,它允许您使用安全策略沙盒脚本执行。(注意:此功能可能会在未来的 Groovy 版本中或在特定 JDK 版本上运行时根据 JEP 411 进行缩减)
-
org.codehaus.groovy.control.customizers.SecureASTCustomizer 通过控制代码库(或代码库的一部分)中允许或禁止的代码构造来保护源代码。
-
默认的 XML 处理已启用安全处理并禁用 doctype 定义。
-
Groovy 的 SQL 处理功能提供支持以防止 SQL 注入。
-
临时目录创建可防止已知的安全漏洞,例如脚本存储在操作系统临时目录中时的权限提升。
3.25. Groovy 中的设计模式
在 Java 中使用 设计模式 是一个成熟的话题。设计模式也适用于 Groovy:
-
某些模式可以直接沿用(并且可以利用普通的 Groovy 语法改进以提高可读性)。
-
某些模式不再需要,因为它们已内置在语言中,或者 Groovy 支持更好的方式来实现模式的意图。
-
某些在其他语言中必须在设计级别表达的模式可以直接在 Groovy 中实现(因为 Groovy 可以模糊设计与实现之间的区别)。
3.25.1. 模式
抽象工厂模式
抽象工厂模式提供了一种封装具有共同主题的单个工厂组的方法。它体现了普通工厂的意图,即消除使用接口的代码需要了解接口背后具体实现的需要,但它适用于一组接口并选择实现这些接口的整个具体类族。
例如,我可能有 Button、TextField 和 Scrollbar 接口。我可能有 WindowsButton、MacButton、FlashButton 作为 Button 的具体类。我可能有 WindowsScrollBar、MacScrollBar 和 FlashScrollBar 作为 ScrollBar 的具体实现。使用抽象工厂模式应该允许我一次性选择要使用的窗口系统(即 Windows、Mac、Flash),然后就可以编写引用接口但始终在幕后使用适当的具体类(都来自同一窗口系统)的代码。
示例
假设我们想编写一个游戏系统。我们可能会注意到许多游戏具有非常相似的功能和控制。
我们决定尝试将通用代码和游戏特定代码拆分为不同的类。
首先,我们来看看 Two-up 游戏的特定代码
class TwoupMessages {
def welcome = 'Welcome to the twoup game, you start with $1000'
def done = 'Sorry, you have no money left, goodbye'
}
class TwoupInputConverter {
def convert(input) { input.toInteger() }
}
class TwoupControl {
private money = 1000
private random = new Random()
private tossWasHead() {
def next = random.nextInt()
return next % 2 == 0
}
def moreTurns() {
if (money > 0) {
println "You have $money, how much would you like to bet?"
return true
}
false
}
def play(amount) {
def coin1 = tossWasHead()
def coin2 = tossWasHead()
if (coin1 && coin2) {
money += amount
println 'You win'
} else if (!coin1 && !coin2) {
money -= amount
println 'You lose'
} else {
println 'Draw'
}
}
}
现在,让我们看看数字猜谜游戏的特定代码:
class GuessGameMessages {
def welcome = 'Welcome to the guessing game, my secret number is between 1 and 100'
def done = 'Correct'
}
class GuessGameInputConverter {
def convert(input) { input.toInteger() }
}
class GuessGameControl {
private lower = 1
private upper = 100
private guess = new Random().nextInt(upper - lower) + lower
def moreTurns() {
def done = (lower == guess || upper == guess)
if (!done) {
println "Enter a number between $lower and $upper"
}
!done
}
def play(nextGuess) {
if (nextGuess <= guess) {
lower = [lower, nextGuess].max()
}
if (nextGuess >= guess) {
upper = [upper, nextGuess].min()
}
}
}
现在,让我们编写工厂代码:
def guessFactory = [messages: GuessGameMessages, control: GuessGameControl, converter: GuessGameInputConverter]
def twoupFactory = [messages: TwoupMessages, control: TwoupControl, converter: TwoupInputConverter]
class GameFactory {
def static factory
def static getMessages() { return factory.messages.newInstance() }
def static getControl() { return factory.control.newInstance() }
def static getConverter() { return factory.converter.newInstance() }
}
这个工厂的重要方面是它允许选择整个具体的类族。
以下是我们如何使用工厂:
GameFactory.factory = twoupFactory
def messages = GameFactory.messages
def control = GameFactory.control
def converter = GameFactory.converter
println messages.welcome
def reader = new BufferedReader(new InputStreamReader(System.in))
while (control.moreTurns()) {
def input = reader.readLine().trim()
control.play(converter.convert(input))
}
println messages.done
请注意,第一行配置了我们将使用的具体游戏类族。通过使用工厂属性(如第一行所示)来选择要使用的家族并不重要。其他方式同样是该模式的有效示例。例如,我们可能已经询问用户他们想玩什么游戏,或者从环境设置中确定了是哪个游戏。
如代码所示,运行时游戏可能看起来像这样:
Welcome to the twoup game, you start with $1000 You have 1000, how much would you like to bet? 300 Draw You have 1000, how much would you like to bet? 700 You win You have 1700, how much would you like to bet? 1700 You lose Sorry, you have no money left, goodbye
如果我们把脚本的第一行改成 `GameFactory.factory = guessFactory`,那么运行结果可能如下所示:
Welcome to the guessing game, my secret number is between 1 and 100 Enter a number between 1 and 100 75 Enter a number between 1 and 75 35 Enter a number between 1 and 35 15 Enter a number between 1 and 15 5 Enter a number between 5 and 15 10 Correct
适配器模式
适配器模式(有时也称为包装器模式)允许在需要另一种接口类型的地方使用满足一种接口的对象。该模式有两种典型的变体:委托变体和继承变体。
委托示例
假设我们有以下类:
class SquarePeg {
def width
}
class RoundPeg {
def radius
}
class RoundHole {
def radius
def pegFits(peg) {
peg.radius <= radius
}
String toString() { "RoundHole with radius $radius" }
}
我们可以询问 `RoundHole` 类一个 `RoundPeg` 是否适合它,但是如果我们问 `SquarePeg` 同样的问题,它会失败,因为 `SquarePeg` 类没有 `radius` 属性(即不满足所需的接口)。
为了解决这个问题,我们可以创建一个适配器,使其看起来具有正确的接口。它会是这样:
class SquarePegAdapter {
def peg
def getRadius() {
Math.sqrt(((peg.width / 2) ** 2) * 2)
}
String toString() {
"SquarePegAdapter with peg width $peg.width (and notional radius $radius)"
}
}
我们可以这样使用适配器:
def hole = new RoundHole(radius: 4.0)
(4..7).each { w ->
def peg = new SquarePegAdapter(peg: new SquarePeg(width: w))
if (hole.pegFits(peg)) {
println "peg $peg fits in hole $hole"
} else {
println "peg $peg does not fit in hole $hole"
}
}
其结果输出如下:
peg SquarePegAdapter with peg width 4 (and notional radius 2.8284271247461903) fits in hole RoundHole with radius 4.0 peg SquarePegAdapter with peg width 5 (and notional radius 3.5355339059327378) fits in hole RoundHole with radius 4.0 peg SquarePegAdapter with peg width 6 (and notional radius 4.242640687119285) does not fit in hole RoundHole with radius 4.0 peg SquarePegAdapter with peg width 7 (and notional radius 4.949747468305833) does not fit in hole RoundHole with radius 4.0
继承示例
让我们再次考虑相同的例子,使用继承。首先,这是原始类(未更改):
class SquarePeg {
def width
}
class RoundPeg {
def radius
}
class RoundHole {
def radius
def pegFits(peg) {
peg.radius <= radius
}
String toString() { "RoundHole with radius $radius" }
}
使用继承的适配器:
class SquarePegAdapter extends SquarePeg {
def getRadius() {
Math.sqrt(((width / 2) ** 2) * 2)
}
String toString() {
"SquarePegAdapter with width $width (and notional radius $radius)"
}
}
使用适配器:
def hole = new RoundHole(radius: 4.0)
(4..7).each { w ->
def peg = new SquarePegAdapter(width: w)
if (hole.pegFits(peg)) {
println "peg $peg fits in hole $hole"
} else {
println "peg $peg does not fit in hole $hole"
}
}
输出:
peg SquarePegAdapter with width 4 (and notional radius 2.8284271247461903) fits in hole RoundHole with radius 4.0 peg SquarePegAdapter with width 5 (and notional radius 3.5355339059327378) fits in hole RoundHole with radius 4.0 peg SquarePegAdapter with width 6 (and notional radius 4.242640687119285) does not fit in hole RoundHole with radius 4.0 peg SquarePegAdapter with width 7 (and notional radius 4.949747468305833) does not fit in hole RoundHole with radius 4.0
使用闭包进行适配
作为前面示例的变体,我们可以改为定义以下接口:
interface RoundThing {
def getRadius()
}
然后,我们可以将适配器定义为一个闭包,如下所示:
def adapter = {
p -> [getRadius: { Math.sqrt(((p.width / 2) ** 2) * 2) }] as RoundThing
}
并这样使用它:
def peg = new SquarePeg(width: 4)
if (hole.pegFits(adapter(peg))) {
// ... as before
}
使用 ExpandoMetaClass 进行适配
自 Groovy 1.1 起,内置了一个 MetaClass,它可以自动动态添加属性和方法。
以下是使用该功能时的示例:
def peg = new SquarePeg(width: 4)
peg.metaClass.radius = Math.sqrt(((peg.width / 2) ** 2) * 2)
创建 peg 对象后,您只需动态地为其添加一个属性。无需更改原始类,也无需适配器类。
弹跳器模式
弹跳器模式描述了一种方法的用法,该方法的唯一目的是抛出异常(当满足特定条件时)或不执行任何操作。此类方法通常用于防御性地保护方法的先决条件。
编写实用方法时,应始终防止错误的输入参数。编写内部方法时,您可以通过设置足够的单元测试来确保某些前置条件始终成立。在这种情况下,您可以降低对方法设置保护的必要性。
Groovy 与其他语言不同之处在于,您经常在方法中使用 `assert` 方法,而不是拥有大量实用检查器方法或类。
空值检查示例
我们可能有一个像这样的实用方法:
class NullChecker {
static check(name, arg) {
if (arg == null) {
throw new IllegalArgumentException(name + ' is null')
}
}
}
我们会像这样使用它:
void doStuff(String name, Object value) {
NullChecker.check('name', name)
NullChecker.check('value', value)
// do stuff
}
但更 Groovy 的做法是这样:
void doStuff(String name, Object value) {
assert name != null, 'name should not be null'
assert value != null, 'value should not be null'
// do stuff
}
验证示例
作为另一个例子,我们可能有这个实用方法:
class NumberChecker {
static final String NUMBER_PATTERN = "\\\\d+(\\\\.\\\\d+(E-?\\\\d+)?)?"
static isNumber(str) {
if (!str ==~ NUMBER_PATTERN) {
throw new IllegalArgumentException("Argument '$str' must be a number")
}
}
static isNotZero(number) {
if (number == 0) {
throw new IllegalArgumentException('Argument must not be 0')
}
}
}
我们会像这样使用它:
def stringDivide(String dividendStr, String divisorStr) {
NumberChecker.isNumber(dividendStr)
NumberChecker.isNumber(divisorStr)
def dividend = dividendStr.toDouble()
def divisor = divisorStr.toDouble()
NumberChecker.isNotZero(divisor)
dividend / divisor
}
println stringDivide('1.2E2', '3.0')
// => 40.0
但使用 Groovy,我们可以轻松地使用:
def stringDivide(String dividendStr, String divisorStr) {
assert dividendStr =~ NumberChecker.NUMBER_PATTERN
assert divisorStr =~ NumberChecker.NUMBER_PATTERN
def dividend = dividendStr.toDouble()
def divisor = divisorStr.toDouble()
assert divisor != 0, 'Divisor must not be 0'
dividend / divisor
}
责任链模式
在责任链模式中,使用和实现接口(一个或多个方法)的对象有意地松散耦合。一组实现该接口的对象被组织成一个列表(在极少数情况下是树)。使用该接口的对象向第一个实现者对象发出请求。它将决定是否执行任何操作,以及是否将请求进一步传递到列表(或树)中。有时,如果没有任何实现者响应请求,模式中也会编写一些请求的默认实现。
使用传统类的示例
在这个例子中,脚本向 `lister` 对象发送请求。`lister` 指向一个 `UnixLister` 对象。如果它无法处理请求,它会将请求发送到 `WindowsLister`。如果它也无法处理请求,它会将请求发送到 `DefaultLister`。
class UnixLister {
private nextInLine
UnixLister(next) { nextInLine = next }
def listFiles(dir) {
if (System.getProperty('os.name') == 'Linux') {
println "ls $dir".execute().text
} else {
nextInLine.listFiles(dir)
}
}
}
class WindowsLister {
private nextInLine
WindowsLister(next) { nextInLine = next }
def listFiles(dir) {
if (System.getProperty('os.name').startsWith('Windows')) {
println "cmd.exe /c dir $dir".execute().text
} else {
nextInLine.listFiles(dir)
}
}
}
class DefaultLister {
def listFiles(dir) {
new File(dir).eachFile { f -> println f }
}
}
def lister = new UnixLister(new WindowsLister(new DefaultLister()))
lister.listFiles('Downloads')
输出将是一个文件列表(格式略有不同,具体取决于操作系统)。
这是一个 UML 表示:

使用简化策略的示例
对于简单情况,考虑通过不要求类链来简化代码。相反,使用 Groovy 的真值和 Elvis 运算符,如下所示:
String unixListFiles(dir) {
if (System.getProperty('os.name') == 'Linux') {
"ls $dir".execute().text
}
}
String windowsListFiles(dir) {
if (System.getProperty('os.name').startsWith('Windows')) {
"cmd.exe /c dir $dir".execute().text
}
}
String defaultListFiles(dir) {
new File(dir).listFiles().collect{ f -> f.name }.join('\\n')
}
def dir = 'Downloads'
println unixListFiles(dir) ?: windowsListFiles(dir) ?: defaultListFiles(dir)
或者 Groovy 的 switch,如下所示:
String listFiles(dir) {
switch(dir) {
case { System.getProperty('os.name') == 'Linux' }:
return "ls $dir".execute().text
case { System.getProperty('os.name').startsWith('Windows') }:
return "cmd.exe /c dir $dir".execute().text
default:
new File(dir).listFiles().collect{ f -> f.name }.join('\\n')
}
}
println listFiles('Downloads')
或者,对于 Groovy 3+,考虑使用 lambda 流,如下所示:
Optional<String> unixListFiles(String dir) {
Optional.ofNullable(dir)
.filter(d -> System.getProperty('os.name') == 'Linux')
.map(d -> "ls $d".execute().text)
}
Optional<String> windowsListFiles(String dir) {
Optional.ofNullable(dir)
.filter(d -> System.getProperty('os.name').startsWith('Windows'))
.map(d -> "cmd.exe /c dir $d".execute().text)
}
Optional<String> defaultListFiles(String dir) {
Optional.ofNullable(dir)
.map(d -> new File(d).listFiles().collect{ f -> f.name }.join('\\n'))
}
def dir = 'Downloads'
def handlers = [this::unixListFiles, this::windowsListFiles, this::defaultListFiles]
println handlers.stream()
.map(f -> f(dir))
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.get()
何时不使用
如果您的责任链使用涉及频繁使用 `instanceof` 运算符,如下所示:
import static Math.PI as π
abstract class Shape {
String name
}
class Polygon extends Shape {
String name
double lengthSide
int numSides
}
class Circle extends Shape {
double radius
}
class CircleAreaCalculator {
def next
def area(shape) {
if (shape instanceof Circle) { (1)
return shape.radius ** 2 * π
} else {
next.area(shape)
}
}
}
class SquareAreaCalculator {
def next
def area(shape) {
if (shape instanceof Polygon && shape.numSides == 4) { (1)
return shape.lengthSide ** 2
} else {
next.area(shape)
}
}
}
class DefaultAreaCalculator {
def area(shape) {
throw new IllegalArgumentException("Don't know how to calculate area for $shape")
}
}
def chain = new CircleAreaCalculator(next: new SquareAreaCalculator(next: new DefaultAreaCalculator()))
def shapes = [
new Circle(name: 'Circle', radius: 5.0),
new Polygon(name: 'Square', lengthSide: 10.0, numSides: 4)
]
shapes.each { println chain.area(it) }
1 | instanceof 代码异味 |
这可能表明您不应使用责任链模式,而应考虑使用更丰富的类型,也许结合 Groovy 的多方法。例如,也许是这样:
// ...
class Square extends Polygon {
// ...
}
double area(Circle c) {
c.radius ** 2 * π
}
double area(Square s) {
s.lengthSide ** 2
}
def shapes = [
new Circle(radius: 5.0),
new Square(lengthSide: 10.0, numSides: 4)
]
shapes.each { println area(it) }
或者使用更传统的面向对象风格,像这样:
import static Math.PI as π
interface Shape {
double area()
}
abstract class Polygon implements Shape {
double lengthSide
int numSides
abstract double area()
}
class Circle implements Shape {
double radius
double area() {
radius ** 2 * π
}
}
class Square extends Polygon {
// ...
double area() {
lengthSide ** 2
}
}
def shapes = [
new Circle(radius: 5.0),
new Square(lengthSide: 10.0, numSides: 4)
]
shapes.each { println it.area() }
进一步探索
该模式的其他变体:
-
我们可以在传统示例中有一个显式接口,例如 `Lister`,以静态地为实现类型化,但由于鸭子类型,这是可选的。
-
我们可以使用链树而不是列表,例如,`if (animal.hasBackbone())` 委托给 `VertebrateHandler`,否则委托给 `InvertebrateHandler`。
-
即使我们处理了请求,我们也可以总是向下传递链(没有提前返回)。
-
我们可以在某个时候决定不响应并且不向下传递链(抢占式中止)。
-
我们可以使用 Groovy 的元编程能力将未知方法向下传递链,例如,将责任链与 `methodMissing` 的使用结合起来。
命令模式
命令模式是一种用于松散耦合客户端对象(希望执行一系列命令)和接收器对象(执行这些命令)的模式。客户端不直接与接收器对话,而是与一个中间对象交互,该中间对象然后将必要的命令中继到接收器。该模式在 JDK 中普遍使用,例如 Swing 中的 api:javax.swing.Action[] 类将 Swing 代码与按钮、菜单项和面板等接收器解耦。
显示典型类的类图是:

交互序列如下所示,适用于任意接收器:

使用传统类的示例
开启和关闭灯所需的类(参见前面维基百科参考中的示例)如下所示:
interface Command {
void execute()
}
// invoker class
class Switch {
private final Map<String, Command> commandMap = new HashMap<>()
void register(String commandName, Command command) {
commandMap[commandName] = command
}
void execute(String commandName) {
Command command = commandMap[commandName]
if (!command) {
throw new IllegalStateException("no command registered for " + commandName)
}
command.execute()
}
}
// receiver class
class Light {
void turnOn() {
println "The light is on"
}
void turnOff() {
println "The light is off"
}
}
class SwitchOnCommand implements Command {
Light light
@Override // Command
void execute() {
light.turnOn()
}
}
class SwitchOffCommand implements Command {
Light light
@Override // Command
void execute() {
light.turnOff()
}
}
Light lamp = new Light()
Command switchOn = new SwitchOnCommand(light: lamp)
Command switchOff = new SwitchOffCommand(light: lamp)
Switch mySwitch = new Switch()
mySwitch.register("on", switchOn)
mySwitch.register("off", switchOff)
mySwitch.execute("on")
mySwitch.execute("off")
我们的客户端脚本向中间人发送 `execute` 命令,并且对任何特定接收器、任何特定操作方法名称和参数一无所知。
简化变体
鉴于 Groovy 具有一流函数支持,我们可以通过使用闭包来替代实际的命令类(例如 `SwitchOnCommand`),如下所示:
interface Command {
void execute()
}
// invoker class
class Switch {
private final Map<String, Command> commandMap = [:]
void register(String commandName, Command command) {
commandMap[commandName] = command
}
void execute(String commandName) {
Command command = commandMap[commandName]
if (!command) {
throw new IllegalStateException("no command registered for $commandName")
}
command.execute()
}
}
// receiver class
class Light {
void turnOn() {
println 'The light is on'
}
void turnOff() {
println 'The light is off'
}
}
Light lamp = new Light()
Switch mySwitch = new Switch()
mySwitch.register("on", lamp.&turnOn) (1)
mySwitch.register("off", lamp.&turnOff) (1)
mySwitch.execute("on")
mySwitch.execute("off")
1 | 命令闭包(这里是方法闭包),但对于 Groovy 3+ 可以是 lambda/方法引用 |
我们可以通过使用 JDK 现有的 `Runnable` 接口和使用 switch 映射而不是单独的 `Switch` 类来进一步简化,如下所示:
class Light {
void turnOn() {
println 'The light is on'
}
void turnOff() {
println 'The light is off'
}
}
class Door {
static void unlock() {
println 'The door is unlocked'
}
}
Light lamp = new Light()
Map<String, Runnable> mySwitch = [
on: lamp::turnOn,
off: lamp::turnOff,
unlock: Door::unlock
]
mySwitch.on()
mySwitch.off()
mySwitch.unlock()
我们添加了一个额外的 `Door` 接收器,以说明如何扩展原始示例。运行此脚本将导致:
The light is on The light is off The door is unlocked
作为一种变体,如果命令名称对我们不重要,我们可以放弃使用 switch 映射,只保留一个要调用的任务列表,如下所示:
// ...
List<Runnable> tasks = [lamp::turnOn, lamp::turnOff, Door::unlock]
tasks.each{ it.run() }
组合模式
组合模式允许您以相同的方式处理对象的单个实例和对象组。该模式通常与对象层次结构一起使用。通常,一个或多个方法应以相同的方式可用于层次结构中的叶节点或组合节点。在这种情况下,组合节点通常为其每个子节点调用同名方法。
示例
考虑这种组合模式的使用,我们希望在 `Leaf` 或 `Composite` 对象上调用 `toString()`。

在 Java 中,`Component` 类至关重要,因为它提供了叶节点和组合节点使用的类型。在 Groovy 中,由于鸭子类型,我们不需要它用于该目的,但是,它仍然可以作为一个有用的地方来放置叶节点和组合节点之间的共同行为。
出于我们的目的,我们将组装以下组件层次结构。

以下是代码:
abstract class Component {
def name
def toString(indent) {
("-" * indent) + name
}
}
class Composite extends Component {
private children = []
def toString(indent) {
def s = super.toString(indent)
children.each { child ->
s += "\\n" + child.toString(indent + 1)
}
s
}
def leftShift(component) {
children << component
}
}
class Leaf extends Component { }
def root = new Composite(name: "root")
root << new Leaf(name: "leaf A")
def comp = new Composite(name: "comp B")
root << comp
root << new Leaf(name: "leaf C")
comp << new Leaf(name: "leaf B1")
comp << new Leaf(name: "leaf B2")
println root.toString(0)
以下是结果输出:
root -leaf A -comp B --leaf B1 --leaf B2 -leaf C
装饰器模式
装饰器模式提供了一种机制,用于修饰对象的行为而无需更改其基本接口。被装饰的对象应该能够在任何预期原始(未装饰)对象的地方进行替换。装饰通常不涉及修改原始对象的源代码,并且装饰器应该能够以灵活的方式组合,以生成具有多种修饰的对象。
传统示例
假设我们有以下 `Logger` 类。
class Logger {
def log(String message) {
println message
}
}
有时对日志消息加时间戳会很有用,或者有时我们可能想更改消息的大小写。我们可以尝试将所有这些功能构建到我们的 `Logger` 类中。如果这样做,`Logger` 类将变得非常复杂。此外,每个人都会获得所有功能,即使他们可能只想要一小部分功能。最后,功能交互将变得非常难以控制。
为了克服这些缺点,我们改为定义两个装饰器类。`Logger` 类的使用者可以自由地以他们想要的任何顺序用零个或多个装饰器类来修饰他们的基本日志记录器。这些类看起来像这样:
class TimeStampingLogger extends Logger {
private Logger logger
TimeStampingLogger(logger) {
this.logger = logger
}
def log(String message) {
def now = Calendar.instance
logger.log("$now.time: $message")
}
}
class UpperLogger extends Logger {
private Logger logger
UpperLogger(logger) {
this.logger = logger
}
def log(String message) {
logger.log(message.toUpperCase())
}
}
我们可以这样使用装饰器:
def logger = new UpperLogger(new TimeStampingLogger(new Logger()))
logger.log("G'day Mate")
// => Tue May 22 07:13:50 EST 2007: G'DAY MATE
您可以看到我们用两个装饰器都修饰了日志记录器行为。由于我们选择的应用装饰器的顺序,我们的日志消息以大写字母开头,并且时间戳是正常大小写。如果我们交换顺序,让我们看看会发生什么:
logger = new TimeStampingLogger(new UpperLogger(new Logger()))
logger.log('Hi There')
// => TUE MAY 22 07:13:50 EST 2007: HI THERE
现在时间戳本身也已更改为大写。
用闭包或 lambda 简化
闭包使得代码表示变得容易。我们可以利用这个事实来创建一个通用日志类,该类将装饰代码作为闭包接受。这为我们省去了定义许多装饰类的麻烦。
class DecoratingLogger {
def decoration = Closure.IDENTITY
def log(String message) {
println decoration(message)
}
}
def upper = { it.toUpperCase() }
def stamp = { "$Calendar.instance.time: $it" }
def logger = new DecoratingLogger(decoration: stamp << upper)
logger.log("G'day Mate")
// Sat Aug 29 15:28:29 AEST 2020: G'DAY MATE
我们可以使用与 lambda 相同的方法:
import java.util.function.Function
class DecoratingLogger {
Function<String, String> decoration = Function.identity()
def log(String message) {
println decoration.apply(message)
}
}
Function<String, String> upper = s -> s.toUpperCase()
Function<String, String> stamp = s -> "$Calendar.instance.time: $s"
def logger = new DecoratingLogger(decoration: upper.andThen(stamp))
logger.log("G'day Mate")
// => Sat Aug 29 15:38:28 AEST 2020: G'DAY MATE
一点动态行为
我们之前的装饰器是特定于 `Logger` 对象的。我们可以使用 Groovy 的元对象编程能力来创建一个更通用的装饰器。考虑这个类:
class GenericLowerDecorator {
private delegate
GenericLowerDecorator(delegate) {
this.delegate = delegate
}
def invokeMethod(String name, args) {
def newargs = args.collect { arg ->
if (arg instanceof String) {
return arg.toLowerCase()
} else {
return arg
}
}
delegate.invokeMethod(name, newargs)
}
}
它接受任何类并对其进行装饰,以便任何 `String` 方法参数将自动更改为小写。
logger = new GenericLowerDecorator(new TimeStampingLogger(new Logger()))
logger.log('IMPORTANT Message')
// => Tue May 22 07:27:18 EST 2007: important message
在这里要小心顺序。原始装饰器仅限于装饰 `Logger` 对象。这个装饰器适用于任何对象类型,所以我们不能交换顺序,也就是说,这行不通:
// Can't mix and match Interface-Oriented and Generic decorators // logger = new TimeStampingLogger(new GenericLowerDecorator(new Logger()))
我们可以通过在运行时生成适当的 Proxy 类型来克服这个限制,但我们在这里不会使示例复杂化。
运行时行为修饰
您还可以考虑使用 Groovy 1.1 中的 `ExpandoMetaClass` 来动态地用行为修饰一个类。这并非装饰器模式的正常用法(它当然没有那么灵活),但在某些情况下可能有助于您在不创建新类的情况下实现类似的结果。
代码如下所示:
// current mechanism to enable ExpandoMetaClass
GroovySystem.metaClassRegistry.metaClassCreationHandle = new ExpandoMetaClassCreationHandle()
def logger = new Logger()
logger.metaClass.log = { String m -> println 'message: ' + m.toUpperCase() }
logger.log('x')
// => message: X
这实现了类似于应用单个装饰器的结果,但我们无法轻松地动态应用和移除修饰。
更动态的装饰
假设我们有一个计算器类(实际上任何类都可以)。
class Calc {
def add(a, b) { a + b }
}
我们可能对随时间观察类的使用情况感兴趣。如果它深埋在我们的代码库中,可能很难确定何时调用它以及使用什么参数。此外,可能很难知道它是否表现良好。我们可以轻松地制作一个通用跟踪装饰器,每当调用 `Calc` 类上的任何方法时,它都会打印出跟踪信息,并提供执行所需时间的计时信息。以下是跟踪装饰器的代码:
class TracingDecorator {
private delegate
TracingDecorator(delegate) {
this.delegate = delegate
}
def invokeMethod(String name, args) {
println "Calling $name$args"
def before = System.currentTimeMillis()
def result = delegate.invokeMethod(name, args)
println "Got $result in ${System.currentTimeMillis()-before} ms"
result
}
}
以下是在脚本中使用该类的方法:
def tracedCalc = new TracingDecorator(new Calc())
assert 15 == tracedCalc.add(3, 12)
运行此脚本后,您会看到以下内容:
Calling add{3, 12} Got 15 in 31 ms
使用拦截器进行装饰
上述计时示例(通过 `invokeMethod`)挂钩到 Groovy 对象的生命周期。这种元编程风格非常重要,Groovy 对使用拦截器的这种装饰风格有特殊支持。
Groovy 甚至内置了 `TracingInterceptor`。我们可以这样扩展内置类:
class TimingInterceptor extends TracingInterceptor {
private beforeTime
def beforeInvoke(object, String methodName, Object[] arguments) {
super.beforeInvoke(object, methodName, arguments)
beforeTime = System.currentTimeMillis()
}
Object afterInvoke(Object object, String methodName, Object[] arguments, Object result) {
super.afterInvoke(object, methodName, arguments, result)
def duration = System.currentTimeMillis() - beforeTime
writer.write("Duration: $duration ms\\n")
writer.flush()
result
}
}
以下是使用这个新类的示例:
def proxy = ProxyMetaClass.getInstance(Calc)
proxy.interceptor = new TimingInterceptor()
proxy.use {
assert 7 == new Calc().add(1, 6)
}
以下是输出:
before Calc.ctor() after Calc.ctor() Duration: 0 ms before Calc.add(java.lang.Integer, java.lang.Integer) after Calc.add(java.lang.Integer, java.lang.Integer) Duration: 2 ms
使用 java.lang.reflect.Proxy 进行装饰
如果您尝试装饰一个对象(即,仅仅是类的特定实例,而不是类本身),那么您可以使用 Java 的 `java.lang.reflect.Proxy`。Groovy 使使用它比纯 Java 更容易。下面是一个摘自 Grails 项目的代码示例,它包装了一个 `java.sql.Connection`,使其 close 方法成为一个无操作:
protected Sql getGroovySql() {
final Connection con = session.connection()
def invoker = { object, method, args ->
if (method.name == "close") {
log.debug("ignoring call to Connection.close() for use by groovy.sql.Sql")
} else {
log.trace("delegating $method")
return con.invokeMethod(method.name, args)
}
} as InvocationHandler;
def proxy = Proxy.newProxyInstance( getClass().getClassLoader(), [Connection] as Class[], invoker )
return new Sql(proxy)
}
如果有许多方法需要拦截,那么这种方法可以修改为通过方法名在映射中查找闭包并调用它。
使用 Spring 进行装饰
Spring 框架允许通过拦截器(您可能听说过通知或切面)应用装饰器。您也可以从 Groovy 中利用这种机制。
首先定义一个您想要装饰的类(我们还将使用一个接口,这是 Spring 的正常做法):
以下是接口:
interface Calc {
def add(a, b)
}
以下是类:
class CalcImpl implements Calc {
def add(a, b) { a + b }
}
现在,我们在名为 `beans.xml` 的文件中定义我们的配置,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang https://www.springframework.org/schema/lang/spring-lang.xsd">
<bean id="performanceInterceptor" autowire="no"
class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor">
<property name="loggerName" value="performance"/>
</bean>
<bean id="calc" class="util.CalcImpl"/>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames" value="calc"/>
<property name="interceptorNames" value="performanceInterceptor"/>
</bean>
</beans>
现在,我们的脚本看起来像这样:
@Grab('org.springframework:spring-context:5.2.8.RELEASE')
import org.springframework.context.support.ClassPathXmlApplicationContext
def ctx = new ClassPathXmlApplicationContext('beans.xml')
def calc = ctx.getBean('calc')
println calc.add(3, 25)
当我们运行它时,我们会看到结果:
21/05/2007 23:02:35 org.springframework.aop.interceptor.PerformanceMonitorInterceptor invokeUnderTrace FINEST: StopWatch 'util.Calc.add': running time (millis) = 16
您可能需要调整 `logging.properties` 文件,以显示日志级别为 `FINEST` 的消息。
使用 GPars 的异步装饰器
以下示例的灵感来自 Panini 编程语言的一些早期示例代码。如今,您会看到这种风格与 JavaScript 中的异步函数一起使用。
@Grab('org.codehaus.gpars:gpars:0.10')
import static groovyx.gpars.GParsPool.withPool
interface Document {
void print()
String getText()
}
class DocumentImpl implements Document {
def document
void print() { println document }
String getText() { document }
}
def words(String text) {
text.replaceAll('[^a-zA-Z]', ' ').trim().split("\\\\s+")*.toLowerCase()
}
def avgWordLength = {
def words = words(it.text)
sprintf "Avg Word Length: %4.2f", words*.size().sum() / words.size()
}
def modeWord = {
def wordGroups = words(it.text).groupBy {it}.collectEntries { k, v -> [k, v.size()] }
def maxSize = wordGroups*.value.max()
def maxWords = wordGroups.findAll { it.value == maxSize }
"Mode Word(s): ${maxWords*.key.join(', ')} ($maxSize occurrences)"
}
def wordCount = { d -> "Word Count: " + words(d.text).size() }
def asyncDecorator(Document d, Closure c) {
ProxyGenerator.INSTANCE.instantiateDelegate([print: {
withPool {
def result = c.callAsync(d)
d.print()
println result.get()
}
}], [Document], d)
}
Document d = asyncDecorator(asyncDecorator(asyncDecorator(
new DocumentImpl(document:"This is the file with the words in it\\n\\t\\nDo you see the words?\\n"),
// new DocumentImpl(document: new File('AsyncDecorator.groovy').text),
wordCount), modeWord), avgWordLength)
d.print()
委托模式
委托模式是一种技术,其中对象的行为(公共方法)通过将责任委托给一个或多个关联对象来实现。
Groovy 允许应用传统风格的委托模式,例如,参见 用委托替换超类。
使用 ExpandoMetaClass 实现委托模式
groovy.lang.ExpandoMetaClass 允许将这种模式封装在库中。这使得 Groovy 能够模拟 Ruby 语言中可用的类似库。
考虑以下库类:
class Delegator {
private targetClass
private delegate
Delegator(targetClass, delegate) {
this.targetClass = targetClass
this.delegate = delegate
}
def delegate(String methodName) {
delegate(methodName, methodName)
}
def delegate(String methodName, String asMethodName) {
targetClass.metaClass."$asMethodName" = delegate.&"$methodName"
}
def delegateAll(String[] names) {
names.each { delegate(it) }
}
def delegateAll(Map names) {
names.each { k, v -> delegate(k, v) }
}
def delegateAll() {
delegate.class.methods*.name.each { delegate(it) }
}
}
有了它在你的 classpath 中,你现在可以动态地应用委托模式,如下面的例子所示。首先,考虑我们有以下类:
class Person {
String name
}
class MortgageLender {
def borrowAmount(amount) {
"borrow \\$$amount"
}
def borrowFor(thing) {
"buy \\$thing"
}
}
def lender = new MortgageLender()
def delegator = new Delegator(Person, lender)
我们现在可以使用委托者自动从出借者对象借用方法来扩展Person类。我们可以按原样借用方法,也可以重命名:
delegator.delegate 'borrowFor'
delegator.delegate 'borrowAmount', 'getMoney'
def p = new Person()
println p.borrowFor('present') // => buy present
println p.getMoney(50)
上面第一行通过委托给 `lender` 对象,将 `borrowFor` 方法添加到 `Person` 类。第二行通过委托给 `lender` 对象的 `borrowAmount` 方法,将 `getMoney` 方法添加到 `Person` 类。
或者,我们可以借用多个方法,如下所示:
delegator.delegateAll 'borrowFor', 'borrowAmount'
这将这两个方法添加到 `Person` 类中。
或者如果我们想要所有方法,像这样:
delegator.delegateAll()
这将使委托对象中的所有方法在 `Person` 类中可用。
或者,我们可以使用映射符号来重命名多个方法:
delegator.delegateAll borrowAmount:'getMoney', borrowFor:'getThing'
使用 @Delegate 注解实现委托模式
自 1.6 版本以来,您可以使用基于 AST 转换的内置委托机制。
这使得委托变得更加容易:
class Person {
def name
@Delegate MortgageLender mortgageLender = new MortgageLender()
}
class MortgageLender {
def borrowAmount(amount) {
"borrow \\$$amount"
}
def borrowFor(thing) {
"buy $thing"
}
}
def p = new Person()
assert "buy present" == p.borrowFor('present')
assert "borrow \\$50" == p.borrowAmount(50)
享元模式
享元模式是一种大幅减少内存需求的模式,它不要求在处理包含许多大多相同事物的系统时,大量创建重量级对象。例如,如果一个文档使用了一个复杂的字符类来建模,该字符类了解 unicode、字体、定位等,那么如果文档中的每个物理字符都需要自己的字符类实例,那么大型文档的内存需求可能会非常大。相反,字符本身可能保存在字符串中,我们可能只有一个字符类(或少量字符类,例如每种字体类型一个字符类),该字符类了解如何处理字符的细节。
在这种情况下,我们称与许多其他事物共享的状态(例如字符类型)为内在状态。它被捕获在重量级类中。区分物理字符的状态(可能只是其 ASCII 码或 Unicode)称为其外在状态。
示例
首先,我们将模拟一些复杂的飞机(第一架是第二架的恶作剧竞争对手——尽管这与示例无关)。
class Boeing797 {
def wingspan = '80.8 m'
def capacity = 1000
def speed = '1046 km/h'
def range = '14400 km'
// ...
}

class Airbus380 {
def wingspan = '79.8 m'
def capacity = 555
def speed = '912 km/h'
def range = '10370 km'
// ...
}

如果我们想对我们的机队进行建模,我们第一次尝试可能会涉及使用这些重量级对象的许多实例。但事实证明,每个飞机只有少量状态(我们的外部状态)发生变化,所以我们将为重量级对象使用单例,并单独捕获外部状态(下面代码中的购买日期和资产编号)。
class FlyweightFactory {
static instances = [797: new Boeing797(), 380: new Airbus380()]
}
class Aircraft {
private type // intrinsic state
private assetNumber // extrinsic state
private bought // extrinsic state
Aircraft(typeCode, assetNumber, bought) {
type = FlyweightFactory.instances[typeCode]
this.assetNumber = assetNumber
this.bought = bought
}
def describe() {
println """
Asset Number: $assetNumber
Capacity: $type.capacity people
Speed: $type.speed
Range: $type.range
Bought: $bought
"""
}
}
def fleet = [
new Aircraft(380, 1001, '10-May-2007'),
new Aircraft(380, 1002, '10-Nov-2007'),
new Aircraft(797, 1003, '10-May-2008'),
new Aircraft(797, 1004, '10-Nov-2008')
]
fleet.each { p -> p.describe() }
因此,即使我们的机队包含数百架飞机,每种飞机类型我们也只会有一个重量级对象。
作为进一步的效率措施,我们可能会使用享元对象的延迟创建,而不是像上面示例那样预先创建初始映射。
运行此脚本的结果是:
Asset Number: 1001 Capacity: 555 people Speed: 912 km/h Range: 10370 km Bought: 10-May-2007 Asset Number: 1002 Capacity: 555 people Speed: 912 km/h Range: 10370 km Bought: 10-Nov-2007 Asset Number: 1003 Capacity: 1000 people Speed: 1046 km/h Range: 14400 km Bought: 10-May-2008 Asset Number: 1004 Capacity: 1000 people Speed: 1046 km/h Range: 14400 km Bought: 10-Nov-2008
迭代器模式
迭代器模式允许对聚合对象的元素进行顺序访问,而无需暴露其底层表示。
Groovy 在其许多闭包操作符中内置了迭代器模式,例如 `each` 和 `eachWithIndex`,以及 `for .. in` 循环。
例如
def printAll(container) {
for (item in container) { println item }
}
def numbers = [ 1,2,3,4 ]
def months = [ Mar:31, Apr:30, May:31 ]
def colors = [ java.awt.Color.BLACK, java.awt.Color.WHITE ]
printAll numbers
printAll months
printAll colors
结果输出:
1 2 3 4 May=31 Mar=31 Apr=30 java.awt.Color[r=0,g=0,b=0] java.awt.Color[r=255,g=255,b=255]
另一个例子:
colors.eachWithIndex { item, pos ->
println "Position $pos contains '$item'"
}
结果是:
Position 0 contains 'java.awt.Color[r=0,g=0,b=0]' Position 1 contains 'java.awt.Color[r=255,g=255,b=255]'
迭代器模式也内置在其他特殊运算符中,例如用于处理流、URL、文件、目录和正则表达式匹配的 `eachByte`、`eachFile`、`eachDir`、`eachLine`、`eachObject`、`eachMatch` 运算符。
借用我的资源模式
借用我的资源模式确保资源在超出作用域后会被确定性地释放。
这种模式内置在许多 Groovy 辅助方法中。如果您需要以 Groovy 支持之外的方式处理资源,您应该考虑自己使用它。
示例
考虑以下处理文件的代码。首先,我们可能会向文件写入一些行,然后打印其大小:
def f = new File('junk.txt')
f.withPrintWriter { pw ->
pw.println(new Date())
pw.println(this.class.name)
}
println f.size()
// => 42
我们还可以一次一行地读回文件的内容并打印出每一行:
f.eachLine { line ->
println line
}
// =>
// Mon Jun 18 22:38:17 EST 2007
// RunPattern
请注意,Groovy 在幕后使用了普通的 Java `Reader` 和 `PrintWriter` 对象,但代码编写者无需担心显式创建或关闭这些资源。内置的 Groovy 方法将各自的阅读器或写入器借给闭包代码,然后自行清理。因此,您正在使用此模式而无需做任何工作。
然而,有时您希望做一些与使用 Groovy 内置机制免费获得的结果略有不同的事情。您应该考虑在您自己的资源处理操作中利用此模式。
考虑您将如何处理文件中每一行的单词列表。我们实际上也可以使用 Groovy 的内置函数来做到这一点,但请耐心等待,并假设我们必须自己进行一些资源处理。以下是我们不使用此模式时可能编写的代码:
def reader = f.newReader()
reader.splitEachLine(' ') { wordList ->
println wordList
}
reader.close()
// =>
// [ "Mon", "Jun", "18", "22:38:17", "EST", "2007" ]
// [ "RunPattern" ]
注意,我们现在在代码中有一个显式的 `close()` 调用。如果我们没有正确编写它(这里我们没有将代码放在 `try … finally` 块中),我们就有留下文件句柄未关闭的风险。
现在让我们应用借用模式。首先,我们将编写一个辅助方法:
def withListOfWordsForEachLine(File f, Closure c) {
def r = f.newReader()
try {
r.splitEachLine(' ', c)
} finally {
r?.close()
}
}
现在,我们可以将代码重写如下:
withListOfWordsForEachLine(f) { wordList ->
println wordList
}
// =>
// [ "Mon", "Jun", "18", "22:38:17", "EST", "2007" ]
// [ "RunPattern" ]
这简单多了,并且去除了显式的 `close()`。现在在一个地方解决了这个问题,所以我们可以在一个地方应用适当级别的测试或审查,以确保我们没有问题。
使用幺半群
幺半群允许将聚合算法的机制与与该聚合相关的算法特定逻辑分离。它通常被认为是一种函数式设计模式。
也许通过一个例子最容易理解。考虑整数求和、整数乘积和字符串连接的代码。我们可能会注意到各种相似之处:
def nums = [1, 2, 3, 4]
def sum = 0 (1)
for (num in nums) { sum += num } (2)
assert sum == 10
def product = 1 (1)
for (num in nums) { product *= num } (2)
assert product == 24
def letters = ['a', 'b', 'c']
def concat = '' (1)
for (letter in letters) { concat += letter } (2)
assert concat == 'abc'
1 | 初始化聚合计数器 |
2 | 使用 for/while/迭代循环遍历元素,调整计数器 |
我们可以去除重复的聚合编码,并找出每个算法的重要差异。我们可以改为使用 Groovy 的 `inject` 方法。这在函数式编程术语中是一个折叠操作。
assert nums.inject(0){ total, next -> total + next } == 10
assert nums.inject(1){ total, next -> total * next } == 24
assert letters.inject(''){ total, next -> total + next } == 'abc'
这里,第一个参数是初始值,提供的闭包包含算法特定逻辑。
同样,对于 Groovy 3+,我们可以使用 JDK 流 API 和 lambda 语法,如下所示:
assert nums.stream().reduce(0, (total, next) -> total + next) == 10
assert nums.stream().reduce(1, (total, next) -> total * next) == 24
assert letters.stream().reduce('', (total, next) -> total + next) == 'abc'
一点点形式化
看着这些例子,我们可能会认为所有聚合都可以通过这种方式支持。实际上,我们会寻找某些特性来确保这种聚合模式适用:
-
闭包:执行聚合步骤应该产生与被聚合元素相同类型的结果。
此处使用“闭包”一词,我们仅指在操作下闭合,而非 Groovy 的 `Closure` 类。 |
-
结合律:我们应用聚合步骤的顺序不重要。
-
单位元(有时也称为“零”元素):应该存在一个元素,与任何元素聚合后返回原始元素。
如果您的算法不满足所有幺半群性质,这并不意味着聚合不可能。这只意味着您不会获得幺半群的所有好处(我们稍后会介绍),或者您可能需要做更多的工作。此外,您可能会稍微调整您的数据结构,将您的问题转化为涉及幺半群的问题。我们稍后将在本节中介绍这个主题。
幺半群的优点
考虑将整数 10 到 16 相加。由于整数的加法运算是一个幺半群,我们已经知道我们可以节省编写代码,而是使用我们之前在 `inject` 示例中看到的方法。还有一些其他很好的属性。
由于闭包属性,如果我们有一个像 `sum(Integer a, Integer b)` 这样的成对方法,那么对于幺半群,我们总是可以将该方法扩展为适用于列表,例如 `sum(List<Integer> nums)` 或 `sum(Integer first, Integer… rest)`。
由于结合律,我们可以采用一些有趣的方式来解决聚合问题,包括:
-
将问题分解成更小部分的“分治算法”。
-
各种增量算法,例如,如果之前计算过 1..4 的和,那么备忘录化可以允许从 1..5 求和时从中间开始,通过重用缓存值。
-
固有的并行化可以利用多核。
我们只详细看看其中的第一个。对于多核处理器,一个核心可以添加 `10` 加上 `11`,另一个核心 `12` 加上 `13`,依此类推。如果需要,我们将使用单位元(在我们的示例中显示为添加到 `16`)。然后中间结果也可以并发地相加,依此类推,直到达到结果。

我们减少了需要编写的代码量,并且还获得了潜在的性能提升。
以下是我们如何使用 GPars 并发和并行框架对前面示例进行编码(显示了两种替代方案):
def nums = 10..16
GParsPool.withPool {
assert 91 == nums.injectParallel(0){ total, next -> total + next }
assert 91 == nums.parallel.reduce(0, (total, next) -> total + next)
}
处理非幺半群
假设我们要找出 1..10 之间数字的平均值。Groovy 有一个内置方法可以做到这一点:
assert (1..10).average() == 5.5
现在,假设我们想构建自己的幺半群解决方案,而不是使用内置版本。找到单位元似乎很困难。毕竟:
assert (0..10).average() == 5
同样,如果我们要编写成对的聚合闭包,它可能是这样的:
def avg = { a, b -> (a + b) / 2 }
我们可以在这里使用什么 `b` 作为单位元,以便我们的方程返回原始值?我们需要使用 `a`,但那不是一个固定值,所以没有单位元。
此外,对于 `avg` 的这种初步尝试定义,结合律也不成立,正如这些例子所示:
assert 6 == avg(avg(10, 2), 6)
assert 7 == avg(10, avg(2, 6))
还有,我们的闭包属性呢?我们原来的数字是整数,但我们的平均值(`5.5`)不是。我们可以通过让我们的平均值适用于任何 `Number` 实例来解决这个问题,但这并非总是那么容易。
这个问题似乎不适合幺半群解决方案。然而,有许多方法可以将幺半群引入解决方案中。
我们可以将其分成两部分:
def nums = 1..10
def total = nums.sum()
def avg = total / nums.size()
assert avg == 5.5
`sum()` 的计算可以遵循幺半群规则,然后我们的最后一步可以计算平均值。我们甚至可以使用 GPars 进行并发版本:
withPool {
assert 5.5 == nums.sumParallel() / nums.size()
}
在这里,我们使用了内置的 `sum()` 方法(以及 GPars 示例中的 `sumParallel()`),但如果您是手动操作,计算的这部分幺半群性质将使您更容易编写自己的代码。
或者,我们可以引入一个辅助数据结构,将问题重新构造为幺半群。与其只保留总数,不如保留一个包含总数和数字计数的列表。代码可能看起来像这样:
def holder = nums
.collect{ [it, 1] }
.inject{ a, b -> [a[0] + b[0], a[1] + b[1]] }
def avg = holder[0] / holder[1]
assert avg == 5.5
或者,更花哨一点,我们可以为我们的数据结构引入一个类,甚至可以并发计算:
class AverageHolder {
int total
int count
AverageHolder plus(AverageHolder other) {
return new AverageHolder(total: total + other.total,
count: count + other.count)
}
static final AverageHolder ZERO =
new AverageHolder(total: 0, count: 0)
}
def asHolder = {
it instanceof Integer ? new AverageHolder(total: it, count : 1) : it
}
def pairwiseAggregate = { aggregate, next ->
asHolder(aggregate) + asHolder(next)
}
withPool {
def holder = nums.injectParallel(AverageHolder.ZERO, pairwiseAggregate)
def avg = holder.with{ total / count }
assert avg == 5.5
}
空对象模式
空对象模式涉及使用一个特殊的占位符对象来表示空值。通常,如果您有一个指向空值的引用,您不能调用 `reference.field` 或 `reference.method()`,否则会收到可怕的 `NullPointerException`。空对象模式使用一个特殊对象来表示空值,而不是使用实际的 `null`。这允许您在空对象上调用字段和方法引用。使用空对象的结果在语义上应该等同于不做任何事情。
简单示例
假设我们有以下系统:
class Job {
def salary
}
class Person {
def name
def Job job
}
def people = [
new Person(name: 'Tom', job: new Job(salary: 1000)),
new Person(name: 'Dick', job: new Job(salary: 1200)),
]
def biggestSalary = people.collect { p -> p.job.salary }.max()
println biggestSalary
运行时,这会打印 `1200`。现在假设我们调用:
people << new Person(name: 'Harry')
如果我们现在再次尝试计算 `biggestSalary`,我们会收到一个空指针异常。
为了克服这个问题,我们可以引入一个 `NullJob` 类,并将上面的语句更改为:
class NullJob extends Job { def salary = 0 }
people << new Person(name: 'Harry', job: new NullJob())
biggestSalary = people.collect { p -> p.job.salary }.max()
println biggestSalary
这符合我们的要求,但并非总是 Groovy 中最好的做法。Groovy 的安全解引用运算符(`?.`)和空感知闭包通常允许 Groovy 避免创建特殊的空对象或空类。下面通过一个更 Groovy 的方式来编写上述示例来加以说明:
people << new Person(name:'Harry')
biggestSalary = people.collect { p -> p.job?.salary }.max()
println biggestSalary
这里发生了两件事,使这能够正常工作。首先,`max()` 是“空感知”的,所以 [300, null, 400].max() == 400。其次,使用 `?.` 运算符,像 `p?.job?.salary` 这样的表达式将等于 null,如果 `salary` 等于 null,或者如果 `job` 等于 null,或者如果 `p` 等于 null。您不需要编写复杂的嵌套 if ... then ... else 来避免 `NullPointerException`。
树形示例
考虑以下示例,我们希望计算树形结构中所有值的大小、累积和以及累积积。
我们第一次尝试在计算方法中包含了处理空值的特殊逻辑。
class NullHandlingTree {
def left, right, value
def size() {
1 + (left ? left.size() : 0) + (right ? right.size() : 0)
}
def sum() {
value + (left ? left.sum() : 0) + (right ? right.sum() : 0)
}
def product() {
value * (left ? left.product() : 1) * (right ? right.product() : 1)
}
}
def root = new NullHandlingTree(
value: 2,
left: new NullHandlingTree(
value: 3,
right: new NullHandlingTree(value: 4),
left: new NullHandlingTree(value: 5)
)
)
println root.size()
println root.sum()
println root.product()
如果我们引入空对象模式(此处通过定义 `NullTree` 类),我们现在可以简化 `size()`、`sum()` 和 `product()` 方法中的逻辑。这些方法现在更清晰地表达了正常(现在是通用)情况下的逻辑。`NullTree` 中的每个方法都返回一个表示不执行任何操作的值。
class Tree {
def left = new NullTree(), right = new NullTree(), value
def size() {
1 + left.size() + right.size()
}
def sum() {
value + left.sum() + right.sum()
}
def product() {
value * left.product() * right.product()
}
}
class NullTree {
def size() { 0 }
def sum() { 0 }
def product() { 1 }
}
def root = new Tree(
value: 2,
left: new Tree(
value: 3,
right: new Tree(value: 4),
left: new Tree(value: 5)
)
)
println root.size()
println root.sum()
println root.product()
运行这两个示例的结果都是:
4 14 120
注意:空对象模式的一个细微变体是将其与单例模式结合。因此,我们不会像上面所示那样在需要空对象的地方编写 `new NullTree()`。相反,我们将有一个单例空对象实例,我们根据需要在数据结构中放置它。
观察者模式
观察者模式允许一个或多个观察者被通知关于主题对象的变化或事件。

示例
以下是经典模式的典型实现:
interface Observer {
void update(message)
}
class Subject {
private List observers = []
void register(observer) {
observers << observer
}
void unregister(observer) {
observers -= observer
}
void notifyAll(message) {
observers.each{ it.update(message) }
}
}
class ConcreteObserver1 implements Observer {
def messages = []
void update(message) {
messages << message
}
}
class ConcreteObserver2 implements Observer {
def messages = []
void update(message) {
messages << message.toUpperCase()
}
}
def o1a = new ConcreteObserver1()
def o1b = new ConcreteObserver1()
def o2 = new ConcreteObserver2()
def observers = [o1a, o1b, o2]
new Subject().with {
register(o1a)
register(o2)
notifyAll('one')
}
new Subject().with {
register(o1b)
register(o2)
notifyAll('two')
}
def expected = [['one'], ['two'], ['ONE', 'TWO']]
assert observers*.messages == expected
使用闭包,我们可以避免创建具体的观察者类,如下所示:
interface Observer {
void update(message)
}
class Subject {
private List observers = []
void register(Observer observer) {
observers << observer
}
void unregister(observer) {
observers -= observer
}
void notifyAll(message) {
observers.each{ it.update(message) }
}
}
def messages1a = [], messages1b = [], messages2 = []
def o2 = { messages2 << it.toUpperCase() }
new Subject().with {
register{ messages1a << it }
register(o2)
notifyAll('one')
}
new Subject().with {
register{ messages1b << it }
register(o2)
notifyAll('two')
}
def expected = [['one'], ['two'], ['ONE', 'TWO']]
assert [messages1a, messages1b, messages2] == expected
作为 Groovy 3+ 的一个变体,让我们考虑放弃 `Observer` 接口并使用 lambda,如下所示:
import java.util.function.Consumer
class Subject {
private List<Consumer> observers = []
void register(Consumer observer) {
observers << observer
}
void unregister(observer) {
observers -= observer
}
void notifyAll(message) {
observers.each{ it.accept(message) }
}
}
def messages1a = [], messages1b = [], messages2 = []
def o2 = { messages2 << it.toUpperCase() }
new Subject().with {
register(s -> messages1a << s)
register(s -> messages2 << s.toUpperCase())
notifyAll('one')
}
new Subject().with {
register(s -> messages1b << s)
register(s -> messages2 << s.toUpperCase())
notifyAll('two')
}
def expected = [['one'], ['two'], ['ONE', 'TWO']]
assert [messages1a, messages1b, messages2] == expected
我们现在正在调用 `Consumer` 的 `accept` 方法,而不是 `Observer` 的 `update` 方法。
@Bindable 和 @Vetoable
JDK 包含一些遵循观察者模式的内置类。`java.util.Observer` 和 `java.util.Observable` 类自 JDK 9 起因各种限制而被弃用。相反,建议您使用 `java.beans` 包中更强大的类,例如 `java.beans.PropertyChangeListener`。幸运的是,Groovy 有一些内置转换(groovy.beans.Bindable 和 groovy.beans.Vetoable),它们支持该包中的一些关键类。
import groovy.beans.*
import java.beans.*
class PersonBean {
@Bindable String first
@Bindable String last
@Vetoable Integer age
}
def messages = [:].withDefault{[]}
new PersonBean().with {
addPropertyChangeListener{ PropertyChangeEvent ev ->
messages[ev.propertyName] << "prop: $ev.newValue"
}
addVetoableChangeListener{ PropertyChangeEvent ev ->
def name = ev.propertyName
if (name == 'age' && ev.newValue > 40)
throw new PropertyVetoException()
messages[name] << "veto: $ev.newValue"
}
first = 'John'
age = 35
last = 'Smith'
first = 'Jane'
age = 42
}
def expected = [
first:['prop: John', 'prop: Jane'],
age:['veto: 35'],
last:['prop: Smith']
]
assert messages == expected
在这里,`addPropertyChangeListener` 等方法扮演着与之前示例中 `registerObserver` 相同的角色。有一个 `firePropertyChange` 方法对应于之前示例中的 `notifyAll`/`notifyObservers`,但 Groovy 会自动添加它,因此在源代码中不可见。还有一个 `propertyChange` 方法对应于之前示例中的 `update` 方法,尽管同样,此处不可见。
美化我的库模式
美化我的库模式提出了一种扩展库的方法,该库几乎可以满足您的所有需求,但只需要一点点额外功能。它假设您没有感兴趣的库的源代码。
示例
假设我们想使用 Groovy 中内置的 Integer 功能(它建立在 Java 中已有的功能之上)。这些库几乎拥有我们想要的所有功能,但并非全部。我们可能没有 Groovy 和 Java 库的所有源代码,因此我们无法直接更改库。相反,我们增强了库。Groovy 有多种方法可以做到这一点。一种方法是使用 Category。
首先,我们将定义一个合适的类别。
class EnhancedInteger {
static boolean greaterThanAll(Integer self, Object[] others) {
greaterThanAll(self, others)
}
static boolean greaterThanAll(Integer self, others) {
others.every { self > it }
}
}
我们添加了两个方法,通过提供 `greaterThanAll` 方法来增强 Integer 方法。类别遵循约定,它们被定义为静态方法,其中包含一个特殊的第一参数,表示我们希望扩展的类。`greaterThanAll(Integer self, others)` 静态方法变为 `greaterThanAll(other)` 实例方法。
我们定义了两个版本的 `greaterThanAll`。一个用于集合、范围等。另一个用于可变数量的 `Integer` 参数。
以下是使用此类别的方法:
use(EnhancedInteger) {
assert 4.greaterThanAll(1, 2, 3)
assert !5.greaterThanAll(2, 4, 6)
assert 5.greaterThanAll(-4..4)
assert 5.greaterThanAll([])
assert !5.greaterThanAll([4, 5])
}
如您所见,使用这种技术,您可以有效地丰富原始类,而无需访问其源代码。此外,您可以在系统的不同部分应用不同的增强功能,并在需要时使用未增强的对象。
代理模式
代理模式允许一个对象充当另一个对象的假装替代品。通常,使用代理的人并不知道他们没有使用真实的对象。当真实对象难以创建或使用时,该模式非常有用:它可能存在于网络连接上,或者是一个内存中的大对象,或者是一个文件、数据库或其他昂贵或无法复制的资源。
示例
代理模式的一个常见用途是与不同 JVM 中的远程对象通信。这是用于创建通过套接字与服务器对象通信的代理的客户端代码以及一个示例用法:
class AccumulatorProxy {
def accumulate(args) {
def result
def s = new Socket("localhost", 54321)
s.withObjectStreams { ois, oos ->
oos << args
result = ois.readObject()
}
s.close()
return result
}
}
println new AccumulatorProxy().accumulate([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
// => 55
以下是您的服务器代码可能的样子(请先启动此代码):
class Accumulator {
def accumulate(args) {
args.inject(0) { total, arg -> total += arg }
}
}
def port = 54321
def accumulator = new Accumulator()
def server = new ServerSocket(port)
println "Starting server on port $port"
while(true) {
server.accept() { socket ->
socket.withObjectStreams { ois, oos ->
def args = ois.readObject()
oos << accumulator.accumulate(args)
}
}
}
单例模式
单例模式用于确保某个特定类只创建一个对象。当系统中需要正好一个对象来协调操作时,这会很有用;也许是为了效率,因为创建大量相同对象会浪费资源,也许是因为需要一个单一控制点来执行特定算法,或者当一个对象用于与不可共享资源交互时。
单例模式的缺点包括:
-
它会降低重用性。例如,如果您想将继承与单例一起使用,就会出现问题。如果 `SingletonB` 扩展 `SingletonA`,那么应该每个类正好(最多)有一个实例,还是一个类的对象创建会禁止另一个类的对象创建?另外,如果您决定两个类都可以有一个实例,那么如何覆盖静态的 `getInstance()` 方法?
-
由于静态方法,通常也难以测试单例,但 Groovy 可以根据需要提供支持。
示例:经典的 Java 单例
假设我们希望创建一个用于收集投票的类。因为获得正确的投票数量可能非常重要,我们决定使用单例模式。系统只会有唯一的 `VoteCollector` 对象,这使得我们更容易推理该对象的创建和使用。
class VoteCollector {
def votes = 0
private static final INSTANCE = new VoteCollector()
static getInstance() { return INSTANCE }
private VoteCollector() { }
def display() { println "Collector:${hashCode()}, Votes:$votes" }
}
此代码中的一些兴趣点:
-
它有一个私有构造函数,因此我们的系统中不能创建 `VoteCollector` 对象(除了我们创建的 `INSTANCE`)。
-
INSTANCE 也是私有的,所以一旦设置就不能更改。
-
目前我们还没有使投票更新线程安全(这不增加本示例)。
-
投票收集器实例不是惰性创建的(如果我们从不引用该类,则不会创建该实例;但是,一旦我们引用该类,即使最初不需要,也会创建该实例)。
我们可以在某些脚本代码中这样使用这个单例类:
def collector = VoteCollector.instance
collector.display()
collector.votes++
collector = null
Thread.start{
def collector2 = VoteCollector.instance
collector2.display()
collector2.votes++
collector2 = null
}.join()
def collector3 = VoteCollector.instance
collector3.display()
这里我们使用了实例 3 次。第二次使用甚至在不同的线程中(但不要在新类加载器的场景中尝试这样做)。
运行此脚本会生成(您的哈希码值会有所不同):
Collector:15959960, Votes:0 Collector:15959960, Votes:1 Collector:15959960, Votes:2
该模式的变体:
-
为了支持延迟加载和多线程,我们可以只在 `getInstance()` 方法中使用 `synchronized` 关键字。这会带来性能损失,但可以工作。
-
我们可以考虑涉及双重检查锁定和 `volatile` 关键字的变体,但请参阅此处对这种方法的限制。
示例:通过元编程实现单例
Groovy 的元编程能力允许以更基本的方式实现单例模式等概念。这个例子说明了一种使用 Groovy 元编程能力来实现单例模式的简单方法,但不一定是最高效的方法。
假设我们想跟踪计算器执行的总计算次数。一种方法是使用计算器类的单例并在类中保存一个变量来记录计数。
首先我们定义一些基类。一个执行计算并记录计算次数的 `Calculator` 类,以及一个作为计算器门面的 `Client` 类。
class Calculator {
private total = 0
def add(a, b) { total++; a + b }
def getTotalCalculations() { 'Total Calculations: ' + total }
String toString() { 'Calc: ' + hashCode() }
}
class Client {
def calc = new Calculator()
def executeCalc(a, b) { calc.add(a, b) }
String toString() { 'Client: ' + hashCode() }
}
现在我们可以定义并注册一个 `MetaClass`,它会拦截所有创建 `Calculator` 对象的尝试,并始终提供一个预先创建的实例。我们还将这个 `MetaClass` 注册到 Groovy 系统中:
class CalculatorMetaClass extends MetaClassImpl {
private static final INSTANCE = new Calculator()
CalculatorMetaClass() { super(Calculator) }
def invokeConstructor(Object[] arguments) { return INSTANCE }
}
def registry = GroovySystem.metaClassRegistry
registry.setMetaClass(Calculator, new CalculatorMetaClass())
现在我们从脚本中使用 `Client` 类的实例。`Client` 类将尝试创建计算器的新实例,但始终会得到单例。
def client = new Client()
assert 3 == client.executeCalc(1, 2)
println "$client, $client.calc, $client.calc.totalCalculations"
client = new Client()
assert 4 == client.executeCalc(2, 2)
println "$client, $client.calc, $client.calc.totalCalculations"
以下是运行此脚本的结果(您的哈希码值可能会有所不同):
Client: 7306473, Calc: 24230857, Total Calculations: 1 Client: 31436753, Calc: 24230857, Total Calculations: 2
Guice 示例
我们也可以使用 Guice 实现单例模式。
再考虑计算器示例。
Guice 是一个面向 Java 的框架,支持面向接口设计。因此,我们首先创建一个 `Calculator` 接口。然后我们可以创建 `CalculatorImpl` 实现和一个 `Client` 对象,我们的脚本将与之交互。`Client` 类在此示例中并非严格必需,但它允许我们展示非单例实例是默认值。以下是代码:
@Grapes([@Grab('aopalliance:aopalliance:1.0'), @Grab('com.google.code.guice:guice:1.0')])
import com.google.inject.*
interface Calculator {
def add(a, b)
}
class CalculatorImpl implements Calculator {
private total = 0
def add(a, b) { total++; a + b }
def getTotalCalculations() { 'Total Calculations: ' + total }
String toString() { 'Calc: ' + hashCode() }
}
class Client {
@Inject Calculator calc
def executeCalc(a, b) { calc.add(a, b) }
String toString() { 'Client: ' + hashCode() }
}
def injector = Guice.createInjector (
[configure: { binding ->
binding.bind(Calculator)
.to(CalculatorImpl)
.asEagerSingleton() } ] as Module
)
def client = injector.getInstance(Client)
assert 3 == client.executeCalc(1, 2)
println "$client, $client.calc, $client.calc.totalCalculations"
client = injector.getInstance(Client)
assert 4 == client.executeCalc(2, 2)
println "$client, $client.calc, $client.calc.totalCalculations"
请注意 `Client` 类中的 `@Inject` 注解。我们总是可以直接在源代码中知道哪些字段将被注入。
在此示例中,我们选择使用显式绑定。我们所有的依赖项(目前此示例中只有一个)都在绑定中配置。Guice 注入器了解绑定并在我们创建对象时根据需要注入依赖项。要使单例模式成立,您必须始终使用 Guice 创建实例。到目前为止所示的任何内容都无法阻止您手动使用 `new CalculatorImpl()` 创建另一个计算器实例,这当然会违反所需的单例行为。
在其他场景中(尽管可能在大型系统中不常见),我们可以选择使用注解来表达依赖关系,如下面的示例所示:
@Grapes([@Grab('aopalliance:aopalliance:1.0'), @Grab('com.google.code.guice:guice:1.0')])
import com.google.inject.*
@ImplementedBy(CalculatorImpl)
interface Calculator {
// as before ...
}
@Singleton
class CalculatorImpl implements Calculator {
// as before ...
}
class Client {
// as before ...
}
def injector = Guice.createInjector()
// ...
请注意 `CalculatorImpl` 类上的 `@Singleton` 注解和 `Calculator` 接口中的 `@ImplementedBy` 注解。
运行后,上述示例(使用任何一种方法)会产生(您的哈希码值会有所不同):
Client: 8897128, Calc: 17431955, Total Calculations: 1 Client: 21145613, Calc: 17431955, Total Calculations: 2
您可以看到,每当我们请求实例时,都会获得一个新的客户端对象,但它注入的是同一个计算器对象。
Spring 示例
我们可以再次使用 Spring 来完成计算器示例,如下所示:
@Grapes([@Grab('org.springframework:spring-core:5.2.8.RELEASE'), @Grab('org.springframework:spring-beans:5.2.8.RELEASE')])
import org.springframework.beans.factory.support.*
interface Calculator {
def add(a, b)
}
class CalculatorImpl implements Calculator {
private total = 0
def add(a, b) { total++; a + b }
def getTotalCalculations() { 'Total Calculations: ' + total }
String toString() { 'Calc: ' + hashCode() }
}
class Client {
Client(Calculator calc) { this.calc = calc }
def calc
def executeCalc(a, b) { calc.add(a, b) }
String toString() { 'Client: ' + hashCode() }
}
// Here we 'wire' up our dependencies through the API. Alternatively,
// we could use XML-based configuration or the Grails Bean Builder DSL.
def factory = new DefaultListableBeanFactory()
factory.registerBeanDefinition('calc', new RootBeanDefinition(CalculatorImpl))
def beanDef = new RootBeanDefinition(Client, false)
beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_AUTODETECT)
factory.registerBeanDefinition('client', beanDef)
def client = factory.getBean('client')
assert 3 == client.executeCalc(1, 2)
println "$client, $client.calc, $client.calc.totalCalculations"
client = factory.getBean('client')
assert 4 == client.executeCalc(2, 2)
println "$client, $client.calc, $client.calc.totalCalculations"
以下是结果(您的哈希码值会有所不同):
Client: 29418586, Calc: 10580099, Total Calculations: 1 Client: 14800362, Calc: 10580099, Total Calculations: 2
状态模式
状态模式提供了一种结构化的方法来划分复杂系统中的行为。系统的整体行为被划分为定义明确的状态。通常,每个状态都由一个类实现。系统的整体行为可以首先通过了解系统的当前状态来确定;其次,通过理解在该状态下可能的行为(体现在对应状态的类的方法中)来确定。
示例
这是一个例子:
class Client {
def context = new Context()
def connect() {
context.state.connect()
}
def disconnect() {
context.state.disconnect()
}
def send_message(message) {
context.state.send_message(message)
}
def receive_message() {
context.state.receive_message()
}
}
class Context {
def state = new Offline(this)
}
class ClientState {
def context
ClientState(context) {
this.context = context
inform()
}
}
class Offline extends ClientState {
Offline(context) {
super(context)
}
def inform() {
println "offline"
}
def connect() {
context.state = new Online(context)
}
def disconnect() {
println "error: not connected"
}
def send_message(message) {
println "error: not connected"
}
def receive_message() {
println "error: not connected"
}
}
class Online extends ClientState {
Online(context) {
super(context)
}
def inform() {
println "connected"
}
def connect() {
println "error: already connected"
}
def disconnect() {
context.state = new Offline(context)
}
def send_message(message) {
println "\"$message\" sent"
}
def receive_message() {
println "message received"
}
}
client = new Client()
client.send_message("Hello")
client.connect()
client.send_message("Hello")
client.connect()
client.receive_message()
client.disconnect()
这是输出
offline error: not connected connected "Hello" sent error: already connected message received offline
然而,像 Groovy 这样的动态语言的一大优点是,我们可以根据特定需求以多种不同方式表达此示例。此示例的一些潜在变体如下所示。
变体 1:利用面向接口设计
我们可以采取的一种方法是利用面向接口设计。为此,我们可以引入以下接口:
interface State {
def connect()
def disconnect()
def send_message(message)
def receive_message()
}
然后,我们可以修改 `Client`、`Online` 和 `Offline` 类来实现该接口,例如:
class Client implements State {
// ... as before ...
}
class Online implements State {
// ... as before ...
}
class Offline implements State {
// ... as before ...
}
你可能会问:我们是不是只是引入了额外的样板代码?我们不能依赖鸭子类型来做到这一点吗?答案是“是”和“否”。我们可以摆脱鸭子类型,但状态模式的关键意图之一是划分复杂性。如果我们知道客户端类和每个状态类都满足一个接口,那么我们已经在复杂性周围设置了一些关键边界。我们可以孤立地查看任何状态类,并了解该状态可能行为的范围。
我们不必为此使用接口,但它有助于表达这种特定划分风格的意图,并且有助于减小单元测试的大小(如果使用对面向接口设计支持较少的语言,我们将不得不进行额外的测试来表达此意图)。
变体 2:提取状态模式逻辑
或者,结合其他变体,我们可能会决定将一些状态模式逻辑提取到辅助类中。例如,我们可以在状态模式包/jar/脚本中定义以下类:
abstract class InstanceProvider {
static def registry = GroovySystem.metaClassRegistry
static def create(objectClass, param) {
registry.getMetaClass(objectClass).invokeConstructor([param] as Object[])
}
}
abstract class Context {
private context
protected setContext(context) {
this.context = context
}
def invokeMethod(String name, Object arg) {
context.invokeMethod(name, arg)
}
def startFrom(initialState) {
setContext(InstanceProvider.create(initialState, this))
}
}
abstract class State {
private client
State(client) { this.client = client }
def transitionTo(nextState) {
client.setContext(InstanceProvider.create(nextState, client))
}
}
这都是相当通用的,可以在任何我们想引入状态模式的地方使用。现在我们的代码看起来像这样:
class Client extends Context {
Client() {
startFrom(Offline)
}
}
class Offline extends State {
Offline(client) {
super(client)
println "offline"
}
def connect() {
transitionTo(Online)
}
def disconnect() {
println "error: not connected"
}
def send_message(message) {
println "error: not connected"
}
def receive_message() {
println "error: not connected"
}
}
class Online extends State {
Online(client) {
super(client)
println "connected"
}
def connect() {
println "error: already connected"
}
def disconnect() {
transitionTo(Offline)
}
def send_message(message) {
println "\"$message\" sent"
}
def receive_message() {
println "message received"
}
}
client = new Client()
client.send_message("Hello")
client.connect()
client.send_message("Hello")
client.connect()
client.receive_message()
client.disconnect()
您可以在这里看到 `startFrom` 和 `transitionTo` 方法开始赋予我们的示例代码 DSL 的感觉。
变体 3:引入 DSL
或者,结合其他变体,我们可能会决定完全采用领域特定语言 (DSL) 方法来处理此示例。
我们可以定义以下通用辅助函数(首先在此处讨论):
class Grammar {
def fsm
def event
def fromState
def toState
Grammar(a_fsm) {
fsm = a_fsm
}
def on(a_event) {
event = a_event
this
}
def on(a_event, a_transitioner) {
on(a_event)
a_transitioner.delegate = this
a_transitioner.call()
this
}
def from(a_fromState) {
fromState = a_fromState
this
}
def to(a_toState) {
assert a_toState, "Invalid toState: $a_toState"
toState = a_toState
fsm.registerTransition(this)
this
}
def isValid() {
event && fromState && toState
}
public String toString() {
"$event: $fromState=>$toState"
}
}
class FiniteStateMachine {
def transitions = [:]
def initialState
def currentState
FiniteStateMachine(a_initialState) {
assert a_initialState, "You need to provide an initial state"
initialState = a_initialState
currentState = a_initialState
}
def record() {
Grammar.newInstance(this)
}
def reset() {
currentState = initialState
}
def isState(a_state) {
currentState == a_state
}
def registerTransition(a_grammar) {
assert a_grammar.isValid(), "Invalid transition ($a_grammar)"
def transition
def event = a_grammar.event
def fromState = a_grammar.fromState
def toState = a_grammar.toState
if (!transitions[event]) {
transitions[event] = [:]
}
transition = transitions[event]
assert !transition[fromState], "Duplicate fromState $fromState for transition $a_grammar"
transition[fromState] = toState
}
def fire(a_event) {
assert currentState, "Invalid current state '$currentState': passed into constructor"
assert transitions.containsKey(a_event), "Invalid event '$a_event', should be one of ${transitions.keySet()}"
def transition = transitions[a_event]
def nextState = transition[currentState]
assert nextState, "There is no transition from '$currentState' to any other state"
currentState = nextState
currentState
}
}
现在我们可以像这样定义和测试我们的状态机:
class StatePatternDslTest extends GroovyTestCase {
private fsm
protected void setUp() {
fsm = FiniteStateMachine.newInstance('offline')
def recorder = fsm.record()
recorder.on('connect').from('offline').to('online')
recorder.on('disconnect').from('online').to('offline')
recorder.on('send_message').from('online').to('online')
recorder.on('receive_message').from('online').to('online')
}
void testInitialState() {
assert fsm.isState('offline')
}
void testOfflineState() {
shouldFail{
fsm.fire('send_message')
}
shouldFail{
fsm.fire('receive_message')
}
shouldFail{
fsm.fire('disconnect')
}
assert 'online' == fsm.fire('connect')
}
void testOnlineState() {
fsm.fire('connect')
fsm.fire('send_message')
fsm.fire('receive_message')
shouldFail{
fsm.fire('connect')
}
assert 'offline' == fsm.fire('disconnect')
}
}
这个例子并不完全等同于其他例子。它不使用预定义的 `Online` 和 `Offline` 类。相反,它根据需要动态定义整个状态机。有关此风格的更详细示例,请参阅之前的参考。
策略模式
策略模式允许您将特定算法抽象出来,使其独立于用法。这使您可以轻松地更换所使用的算法,而无需更改调用代码。该模式的一般形式是:

在 Groovy 中,由于它能够使用匿名方法(我们松散地称之为闭包)将代码视为一等公民,因此对策略模式的需求大大减少。您可以简单地将算法放在闭包中。
使用传统类层次结构的示例
首先,让我们看看封装策略模式的传统方法。
interface Calc {
def execute(n, m)
}
class CalcByMult implements Calc {
def execute(n, m) { n * m }
}
class CalcByManyAdds implements Calc {
def execute(n, m) {
def result = 0
n.times{
result += m
}
result
}
}
def sampleData = [
[3, 4, 12],
[5, -5, -25]
]
Calc[] multiplicationStrategies = [
new CalcByMult(),
new CalcByManyAdds()
]
sampleData.each{ data ->
multiplicationStrategies.each { calc ->
assert data[2] == calc.execute(data[0], data[1])
}
}
在这里,我们定义了一个接口 Calc
,具体的策略类将实现它(我们也可以使用抽象类)。然后我们定义了两种进行简单乘法的算法:CalcByMult
是常规方法,而 CalcByManyAdds
只使用加法(不要尝试对负数使用此方法——是的,我们可以修复它,但这只会使示例更长)。然后我们使用正常的多态性来调用算法。
使用闭包的例子
这是使用闭包实现相同目的的更 Groovy 的方式
def multiplicationStrategies = [
{ n, m -> n * m },
{ n, m -> def result = 0; n.times{ result += m }; result }
]
def sampleData = [
[3, 4, 12],
[5, -5, -25]
]
sampleData.each{ data ->
multiplicationStrategies.each { calc ->
assert data[2] == calc(data[0], data[1])
}
}
使用 Lambda 的例子
对于 Groovy 3+,我们可以利用 Lambda 语法
interface Calc {
def execute(n, m)
}
List<Calc> multiplicationStrategies = [
(n, m) -> n * m,
(n, m) -> { def result = 0; n.times{ result += m }; result }
]
def sampleData = [
[3, 4, 12],
[5, -5, -25]
]
sampleData.each{ data ->
multiplicationStrategies.each { calc ->
assert data[2] == calc(data[0], data[1])
}
}
或者我们可以使用内置的 JDK BiFunction
类
import java.util.function.BiFunction
List<BiFunction<Integer, Integer, Integer>> multiplicationStrategies = [
(n, m) -> n * m,
(n, m) -> { def result = 0; n.times{ result += m }; result }
]
def sampleData = [
[3, 4, 12],
[5, -5, -25]
]
sampleData.each{ data ->
multiplicationStrategies.each { calc ->
assert data[2] == calc(data[0], data[1])
}
}
模板方法模式
模板方法模式抽象了几个算法的细节。算法的通用部分包含在基类中。特定的实现细节则捕获在子类中。所涉及类的通用模式如下所示

传统类的例子
在这个例子中,基类 Accumulator
捕获了累加算法的精髓。子类 Sum
和 Product
提供了使用通用累加算法的特定定制方式。
abstract class Accumulator {
protected initial
abstract doAccumulate(total, v)
def accumulate(values) {
def total = initial
values.each { v -> total = doAccumulate(total, v) }
total
}
}
class Sum extends Accumulator {
def Sum() { initial = 0 }
def doAccumulate(total, v) { total + v }
}
class Product extends Accumulator {
def Product() { initial = 1 }
def doAccumulate(total, v) { total * v }
}
assert 10 == new Sum().accumulate([1,2,3,4])
assert 24 == new Product().accumulate([1,2,3,4])
简化策略的例子
在这种特殊情况下,您可以使用 Groovy 的 inject 方法,通过闭包实现类似的结果
Closure addAll = { total, item -> total += item }
def accumulated = [1, 2, 3, 4].inject(0, addAll)
assert accumulated == 10
由于鸭子类型,这也适用于支持添加(Groovy 中的 plus()
)方法的其他对象,例如:
accumulated = [ "1", "2", "3", "4" ].inject("", addAll)
assert accumulated == "1234"
我们也可以如下实现乘法情况(重写为一行代码)
assert 24 == [1, 2, 3, 4].inject(1) { total, item -> total *= item }
以这种方式使用闭包看起来像策略模式,但如果我们意识到 Groovy 的 inject
方法是我们模板方法的通用部分,那么闭包就成为模板模式的定制部分。
对于 Groovy 3+,我们可以使用 Lambda 语法作为闭包语法的替代方案
assert 10 == [1, 2, 3, 4].stream().reduce(0, (l, r) -> l + r)
assert 24 == [1, 2, 3, 4].stream().reduce(1, (l, r) -> l * r)
assert '1234' == ['1', '2', '3', '4'].stream().reduce('', (l, r) -> l + r)
这里,stream api 的 reduce
方法是我们模板算法的通用部分,而 lambda 表达式是模板方法模式的定制部分。
访问者模式
访问者模式是一种众所周知但不常使用的模式。也许这是因为它一开始看起来有点复杂。但是一旦你熟悉了它,它就成为一种强大的方式来演进你的代码,而且正如我们将看到的,Groovy 提供了一些方法来减少一些复杂性,所以没有理由不考虑使用这种模式。
该模式的目标是将算法与对象结构分离。这种分离的实际结果是能够向现有对象结构添加新操作,而无需修改这些结构。
简单示例
本例探讨了如何计算形状(或形状集合)的边界。我们的首次尝试使用了传统的访问者模式。我们很快会看到一种更 Groovy 的实现方式。
abstract class Shape { }
@ToString(includeNames=true)
class Rectangle extends Shape {
def x, y, w, h
Rectangle(x, y, w, h) {
this.x = x; this.y = y; this.w = w; this.h = h
}
def union(rect) {
if (!rect) return this
def minx = [rect.x, x].min()
def maxx = [rect.x + rect.w, x + w].max()
def miny = [rect.y, y].min()
def maxy = [rect.y + rect.h, y + h].max()
new Rectangle(minx, miny, maxx - minx, maxy - miny)
}
def accept(visitor) {
visitor.visit_rectangle(this)
}
}
class Line extends Shape {
def x1, y1, x2, y2
Line(x1, y1, x2, y2) {
this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2
}
def accept(visitor){
visitor.visit_line(this)
}
}
class Group extends Shape {
def shapes = []
def add(shape) { shapes += shape }
def remove(shape) { shapes -= shape }
def accept(visitor) {
visitor.visit_group(this)
}
}
class BoundingRectangleVisitor {
def bounds
def visit_rectangle(rectangle) {
if (bounds)
bounds = bounds.union(rectangle)
else
bounds = rectangle
}
def visit_line(line) {
def line_bounds = new Rectangle([line.x1, line.x2].min(),
[line.y1, line.y2].min(),
line.x2 - line.y1,
line.x2 - line.y2)
if (bounds)
bounds = bounds.union(line_bounds)
else
bounds = line_bounds
}
def visit_group(group) {
group.shapes.each { shape -> shape.accept(this) }
}
}
def group = new Group()
group.add(new Rectangle(100, 40, 10, 5))
group.add(new Rectangle(100, 70, 10, 5))
group.add(new Line(90, 30, 60, 5))
def visitor = new BoundingRectangleVisitor()
group.accept(visitor)
bounding_box = visitor.bounds
assert bounding_box.toString() == 'Rectangle(x:60, y:5, w:50, h:70)'
这花费了相当多的代码,但现在的想法是,我们可以通过添加新的访问者来添加更多的算法,而我们的形状类保持不变,例如,我们可以添加一个总面积访问者或一个碰撞检测访问者。
我们可以通过如下方式利用 Groovy 闭包来提高代码的清晰度(并将其缩小约一半)
abstract class Shape {
def accept(Closure yield) { yield(this) }
}
@ToString(includeNames=true)
class Rectangle extends Shape {
def x, y, w, h
def bounds() { this }
def union(rect) {
if (!rect) return this
def minx = [ rect.x, x ].min()
def maxx = [ rect.x + rect.w, x + w ].max()
def miny = [ rect.y, y ].min()
def maxy = [ rect.y + rect.h, y + h ].max()
new Rectangle(x:minx, y:miny, w:maxx - minx, h:maxy - miny)
}
}
class Line extends Shape {
def x1, y1, x2, y2
def bounds() {
new Rectangle(x:[x1, x2].min(), y:[y1, y2].min(),
w:(x2 - x1).abs(), h:(y2 - y1).abs())
}
}
class Group {
def shapes = []
def leftShift(shape) { shapes += shape }
def accept(Closure yield) { shapes.each{it.accept(yield)} }
}
def group = new Group()
group << new Rectangle(x:100, y:40, w:10, h:5)
group << new Rectangle(x:100, y:70, w:10, h:5)
group << new Line(x1:90, y1:30, x2:60, y2:5)
def bounds
group.accept{ bounds = it.bounds().union(bounds) }
assert bounds.toString() == 'Rectangle(x:60, y:5, w:50, h:70)'
或者,使用 lambda 表达式,如下所示:
abstract class Shape {
def accept(Function<Shape, Shape> yield) { yield.apply(this) }
}
@ToString(includeNames=true)
class Rectangle extends Shape {
/* ... same as with Closures ... */
}
class Line extends Shape {
/* ... same as with Closures ... */
}
class Group {
def shapes = []
def leftShift(shape) { shapes += shape }
def accept(Function<Shape, Shape> yield) {
shapes.stream().forEach(s -> s.accept(yield))
}
}
def group = new Group()
group << new Rectangle(x:100, y:40, w:10, h:5)
group << new Rectangle(x:100, y:70, w:10, h:5)
group << new Line(x1:90, y1:30, x2:60, y2:5)
def bounds
group.accept(s -> { bounds = s.bounds().union(bounds) })
assert bounds.toString() == 'Rectangle(x:60, y:5, w:50, h:70)'
高级示例
让我们考虑另一个例子来说明该模式的更多要点。
interface Visitor {
void visit(NodeType1 n1)
void visit(NodeType2 n2)
}
interface Visitable {
void accept(Visitor visitor)
}
class NodeType1 implements Visitable {
Visitable[] children = new Visitable[0]
void accept(Visitor visitor) {
visitor.visit(this)
for(int i = 0; i < children.length; ++i) {
children[i].accept(visitor)
}
}
}
class NodeType2 implements Visitable {
Visitable[] children = new Visitable[0]
void accept(Visitor visitor) {
visitor.visit(this)
for(int i = 0; i < children.length; ++i) {
children[i].accept(visitor)
}
}
}
class NodeType1Counter implements Visitor {
int count = 0
void visit(NodeType1 n1) {
count++
}
void visit(NodeType2 n2){}
}
如果我们现在在这样的树上使用 NodeType1Counter
NodeType1 root = new NodeType1()
root.children = new Visitable[]{new NodeType1(), new NodeType2()}
def counter = new NodeType1Counter()
root.accept(counter)
assert counter.count == 2
那么我们有一个 NodeType1
对象作为根,其中一个子节点也是 NodeType1
实例。另一个子节点是 NodeType2
实例。这意味着在这里使用 NodeType1Counter
应该计算 2 个 NodeType1
对象,正如最后一条语句所验证的那样。
这个例子说明了访问者模式的一些优点。例如,尽管我们的访问者有状态(NodeType1
对象的计数),但对象树本身并未改变。类似地,如果我们要有一个访问者来计数所有节点类型,或者一个计数有多少种不同类型被使用,或者一个使用节点类型特有的方法收集信息,那么同样,只需要编写访问者即可。
在这种情况下,我们可能需要做相当多的工作。我们可能必须更改 Visitor
接口以接受新类型,并且根据该接口的更改可能需要更改大多数现有访问者,而且我们必须编写新类型本身。一个更好的方法是编写一个访问者的默认实现,所有具体访问者都将扩展它。我们很快会看到这种方法的使用。
那你就麻烦了。由于节点描述了如何迭代,你无法影响并在某个点停止迭代或改变顺序。所以也许我们应该稍微改变一下,变成这样
interface Visitor {
void visit(NodeType1 n1)
void visit(NodeType2 n2)
}
class DefaultVisitor implements Visitor{
void visit(NodeType1 n1) {
for(int i = 0; i < n1.children.length; ++i) {
n1.children[i].accept(this)
}
}
void visit(NodeType2 n2) {
for(int i = 0; i < n2.children.length; ++i) {
n2.children[i].accept(this)
}
}
}
interface Visitable {
void accept(Visitor visitor)
}
class NodeType1 implements Visitable {
Visitable[] children = new Visitable[0]
void accept(Visitor visitor) {
visitor.visit(this)
}
}
class NodeType2 implements Visitable {
Visitable[] children = new Visitable[0];
void accept(Visitor visitor) {
visitor.visit(this)
}
}
class NodeType1Counter extends DefaultVisitor {
int count = 0
void visit(NodeType1 n1) {
count++
super.visit(n1)
}
}
一些小改动却产生了巨大影响。访问者现在是递归的,并告诉我如何迭代。节点中的实现被最小化为 visitor.visit(this)
,DefaultVisitor
现在能够捕获新类型,我们可以通过不委托给父类来停止迭代。当然,现在最大的缺点是它不再是迭代的,但你无法获得所有好处。
现在的问题是如何让它更 Groovy 一点。你难道不觉得 visitor.visit(this)
很奇怪吗?它为什么在那里?答案是为了模拟双重分派。在 Java 中,使用编译时类型,所以对于 visitor.visit(children[i])
,编译器将无法找到正确的方法,因为 Visitor
不包含方法 visit(Visitable)
。即使它包含,我们也会希望使用 NodeType1
或 NodeType2
访问更特殊的方法。
现在 Groovy 不使用静态类型,Groovy 使用运行时类型。这意味着我们可以毫无问题地使用 visitor.visit(children[i])
。既然我们已经将 accept 方法最小化到只做双重分派部分,并且 Groovy 的运行时类型系统已经涵盖了这一点,那么我们还需要 accept 方法吗?不完全需要,但我们甚至可以做得更多。我们有不知道如何处理未知树元素的缺点。我们必须为此扩展 Visitor
接口,导致 DefaultVisitor
的更改,然后我们必须提供一个有用的默认值,例如迭代节点或什么都不做。现在使用 Groovy,我们可以通过添加一个什么都不做的 visit(Visitable)
方法来捕获这种情况。顺便说一下,这在 Java 中也是一样的。
但我们不要止步于此。我们还需要 Visitor
接口吗?如果我们没有 accept 方法,那么我们根本不需要 Visitor
接口。所以新代码将是
class DefaultVisitor {
void visit(NodeType1 n1) {
n1.children.each { visit(it) }
}
void visit(NodeType2 n2) {
n2.children.each { visit(it) }
}
void visit(Visitable v) { }
}
interface Visitable { }
class NodeType1 implements Visitable {
Visitable[] children = []
}
class NodeType2 implements Visitable {
Visitable[] children = []
}
class NodeType1Counter extends DefaultVisitor {
int count = 0
void visit(NodeType1 n1) {
count++
super.visit(n1)
}
}
看起来我们在这里节省了几行代码,但我们做得更多了。Visitable
节点现在不引用任何 Visitor
类或接口。这大概是您在这里所期望的最佳分离级别了,但我们可以走得更远。让我们稍微改变一下 Visitable
接口,让它返回我们接下来要访问的子节点。这使我们能够实现一个通用的迭代方法。
class DefaultVisitor {
void visit(Visitable v) {
doIteration(v)
}
void doIteration(Visitable v) {
v.children.each {
visit(it)
}
}
}
interface Visitable {
Visitable[] getChildren()
}
class NodeType1 implements Visitable {
Visitable[] children = []
}
class NodeType2 implements Visitable {
Visitable[] children = []
}
class NodeType1Counter extends DefaultVisitor {
int count = 0
void visit(NodeType1 n1) {
count++
super.visit(n1)
}
}
DefaultVisitor
现在看起来有点不同。它有一个 doIteration
方法,该方法将获取它应该迭代的子节点,然后对每个元素调用 visit。默认情况下,这将调用 visit(Visitable)
,然后迭代该子节点的子节点。Visitable
也已更改,以确保任何节点都能够返回子节点(即使为空)。我们不必更改 NodeType1
和 NodeType2
类,因为 children 字段的定义方式已经使它们成为属性,这意味着 Groovy 非常好心地为我们生成了一个 get 方法。现在真正有趣的部分是 NodeType1Counter
,它之所以有趣是因为我们没有更改它。super.visit(n1)
现在将调用 visit(Visitable)
,后者将调用 doIteration
,从而开始下一级迭代。所以没有变化。但是 visit(it)
将在它是 NodeType1
类型时调用 visit(NodeType1)
。实际上,我们不需要 doIteration
方法,我们也可以在 visit(Visitable)
中完成,但这种变体有一些好处。它允许我们编写一个新的 Visitor
,用于错误情况,它会覆盖 visit(Visitable
),当然这意味着我们不能调用 super.visit(n1)
,而必须调用 doIteration(n1)
。
最终,我们获得了约 40% 的代码减少,一个健壮稳定的架构,并且我们完全将 Visitor 从 Visitable 中移除。要在 Java 中实现相同的功能,您可能需要诉诸反射。
访问者模式有时被描述为不适合极限编程技术,因为您需要一直对许多类进行更改。通过我们的设计,如果我们添加新类型,我们无需更改任何内容。因此,当使用 Groovy 时,该模式非常适合敏捷方法。
访问者模式有变体,例如无环访问者模式,它们试图解决为特殊访问者添加新节点类型的问题。这些访问者的实现有它们自己的代码异味,例如使用类型转换、过度使用 instanceof
以及其他技巧。更重要的是,这些方法试图解决的问题在 Groovy 版本中不会出现。我们建议避免这种模式的变体。
最后,以防不明显,NodeType1Counter
也可以在 Java 中实现。Groovy 会识别访问方法并根据需要调用它们,因为 DefaultVisitor
仍然是 Groovy,并且完成了所有神奇的工作。
3.25.2. 参考文献
-
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (1995)。设计模式:可复用面向对象软件的基础。Addison-Wesley。ISBN 0-201-63361-2。
-
设计模式的规范参考。
-
-
Martin Fowler (1999)。重构:改善既有代码的设计。Addison-Wesley。ISBN 0-201-48567-2。
-
Joshua Kerievsky (2004)。重构到模式。Addison-Wesley。ISBN 0-321-21335-1。
-
Eric Freeman, Elisabeth Freeman, Kathy Sierra, Bert Bates (2004)。Head First 设计模式。O’Reilly。ISBN 0-596-00712-4。*一本值得一读的好书,既有信息性又有趣。
-
Dierk Koenig 与 Andrew Glover, Paul King, Guillaume Laforge 和 Jon Skeet (2007)。Groovy in Action。Manning。ISBN 1-932394-84-2。
-
讨论访问者、构建者和其他模式。
-
-
Brad Appleton (1999)。披萨倒置——一种高效资源消耗模式。
-
许多软件工程师最常用的模式之一!
-
-
Neil Ford 著《动态语言中的设计模式》。动态语言中的设计模式。
4. 致谢
4.1. 贡献者
Groovy 团队衷心感谢本文档的贡献者(按姓氏字母顺序排列)
4.2. 许可证
本作品根据Apache 许可证,版本 2.0 授权。