Groovy 4.0 版本发布说明
Groovy 4 在早期版本的 Groovy 的现有特性的基础上进行了构建。此外,它还包含了许多新特性,并简化了 Groovy 代码库的各个方面。
|
重要的命名/结构更改
Maven 坐标更改
在 Groovy 4.0 中,Groovy 的 Maven 坐标的 groupId 已从 org.codehaus.groovy
更改为 org.apache.groovy
。请相应地更新您的 Gradle/Maven/其他构建设置。
旧版包删除
Java 平台模块系统 (JPMS) 要求不同模块中的类具有不同的包名(称为“拆分打包要求”)。Groovy 有自己的“模块”,这些模块在历史上并非按照此要求进行结构化。
Groovy 3 提供了大量类的重复版本(在旧包和新包中),以允许 Groovy 用户迁移到新的 JPMS 兼容包名。有关更多详细信息,请参见 Groovy 3 版本发布说明。Groovy 4 不再提供重复的旧版类。
简而言之,是时候停止使用 groovy.util.XmlSlurper
并开始使用 groovy.xml.XmlSlurper
了。类似地,您现在应该使用 groovy.xml.XmlParser
、groovy.ant.AntBuilder
、groovy.test.GroovyTestCase
以及前面提到的 Groovy 3 版本发布说明中提到的其他类。
groovy-all 的模块更改
根据用户反馈和下载统计数据,我们重新调整了 groovy-all
pom 中包含的模块 (GROOVY-9647)。groovy-yaml
模块使用相当广泛,现在已包含在 groovy-all
中。groovy-testng
模块使用较少,不再包含在 groovy-all
中。如果需要,请调整您的构建脚本依赖项。如果您使用的是 Groovy 发行版,则无需进行任何更改,因为它包含可选模块。
新特性
switch 表达式
Groovy 一直以来都拥有非常强大的 switch 语句,但有时 switch 表达式 会更方便。
在 switch 语句中,具有贯穿行为的 case 分支通常比处理一个 case 然后退出 switch 的分支要少得多。break
语句会使代码变得杂乱,如以下代码所示。
def result
switch(i) {
case 0: result = 'zero'; break
case 1: result = 'one'; break
case 2: result = 'two'; break
default: throw new IllegalStateException('unknown number')
}
一个常见的技巧是引入一个方法来包装 switch。在简单的情况下,多个语句可能会简化为单个 return 语句。break
语句不见了,不过被 return
语句替换了。
def stringify(int i) {
switch(i) {
case 0: return 'zero'
case 1: return 'one'
case 2: return 'two'
default: throw new IllegalStateException('unknown number')
}
}
def result = stringify(i)
switch 表达式(大量借鉴了 Java)提供了更友好的替代方案。
def result = switch(i) {
case 0 -> 'zero'
case 1 -> 'one'
case 2 -> 'two'
default -> throw new IllegalStateException('unknown number')
}
在这里,右侧(位于 ->
后面)必须是一个表达式。如果需要多个语句,可以使用块。例如,前面示例中的第一个 case 分支可以改写为
case 0 -> { def a = 'ze'; def b = 'ro'; a + b }
switch 表达式也可以使用传统的 :
形式,包含多个语句,但在这种情况下,必须执行 yield
语句。
def result = switch(i) {
case 0:
def a = 'ze'
def b = 'ro'
if (true) yield a + b
else yield b + a
case 1:
yield 'one'
case 2:
yield 'two'
default:
throw new IllegalStateException('unknown number')
}
->
和 :
形式不能混合使用。
所有正常的 Groovy case 表达式仍然得到支持,例如
class Custom {
def isCase(o) { o == -1 }
}
class Coord {
int x, y
}
def items = [10, -1, 5, null, 41, 3.5f, 38, 99, new Coord(x: 4, y: 5), 'foo']
def result = items.collect { a ->
switch(a) {
case null -> 'null'
case 5 -> 'five'
case new Custom() -> 'custom'
case 0..15 -> 'range'
case [37, 41, 43] -> 'prime'
case Float -> 'float'
case { it instanceof Number && it % 2 == 0 } -> 'even'
case Coord -> a.with { "x: $x, y: $y" }
case ~/../ -> 'two chars'
default -> 'none of the above'
}
}
assert result == ['range', 'custom', 'five', 'null', 'prime', 'float',
'even', 'two chars', 'x: 4, y: 5', 'none of the above']
switch 表达式在传统上可能使用访问者模式的情况下特别方便,例如
import groovy.transform.Immutable
interface Expr { }
@Immutable class IntExpr implements Expr { int i }
@Immutable class NegExpr implements Expr { Expr n }
@Immutable class AddExpr implements Expr { Expr left, right }
@Immutable class MulExpr implements Expr { Expr left, right }
int eval(Expr e) {
e.with {
switch(it) {
case IntExpr -> i
case NegExpr -> -eval(n)
case AddExpr -> eval(left) + eval(right)
case MulExpr -> eval(left) * eval(right)
default -> throw new IllegalStateException()
}
}
}
@Newify(pattern=".*Expr")
def test() {
def exprs = [
IntExpr(4),
NegExpr(IntExpr(4)),
AddExpr(IntExpr(4), MulExpr(IntExpr(3), IntExpr(2))), // 4 + (3*2)
MulExpr(IntExpr(4), AddExpr(IntExpr(3), IntExpr(2))) // 4 * (3+2)
]
assert exprs.collect { eval(it) } == [4, -4, 10, 20]
}
test()
与 Java 的区别
-
目前,没有要求 switch 目标的所有可能值都必须由 case 分支完全覆盖。如果不存在
default
分支,则会添加一个隐式的null
分支。因此,在null
不希望出现的情况下,例如将结果存储在基本类型中,或构造一个不可为空的Optional
,则应提供一个明确的default
,例如// default branch avoids GroovyCastException int i = switch(s) { case 'one' -> 1 case 'two' -> 2 default -> 0 } // default branch avoids NullPointerException Optional.of(switch(i) { case 1 -> 'one' case 2 -> 'two' default -> 'buckle my shoe' })
在未来的 Groovy 版本中,或者可能是通过像 CodeNarc 这样的工具,我们希望支持与 Java 类似的更严格的穷举 case 分支检查。当使用 Groovy 的静态特性或通过额外的可选类型检查扩展时,这可能会自动实现。因此,开发人员可能希望不要依赖返回
null
的自动 default 分支,而是提供自己的 default 或穷举地覆盖所有分支。
密封类型
密封类、接口和特征限制了哪些其他类或接口可以扩展或实现它们。Groovy 支持在编写密封类型时使用 sealed
关键字或 @Sealed
注解。密封类型的允许子类可以明确给出(使用带有 sealed
关键字的 permits
子句或 @Sealed
的 permittedSubclasses
注解属性),或者如果同时编译所有相关类型,则自动检测。有关更多详细信息,请参见 (GEP-13) 和 Groovy 文档。
作为一个激励示例,密封层次结构在指定代数或抽象数据类型 (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
关键字的天气示例
sealed abstract class Weather { }
class Rainy extends Weather { Integer rainfall }
class Sunny extends Weather { Integer temp }
class Cloudy extends Weather { Integer uvIndex }
def threeDayForecast = [
new Rainy(rainfall: 12),
new Sunny(temp: 35),
new Cloudy(uvIndex: 6)
]
与 Java 的区别
-
non-sealed
关键字(或@NonSealed
注解)不是必需的,用来表明子类可以扩展。Codenarc 的未来版本可能有一条规则允许 Groovy 开发人员在需要时遵循 Java 的这种做法。话虽如此,保持对扩展的限制(通过使用final
或sealed
)将导致未来类型检查可以检查类型穷举使用的情况(例如 switch 表达式)的更多地方。 -
Groovy 使用
@Sealed
注解来支持 JDK8+ 的密封类。这些被称为模拟密封类。此类类会被 Groovy 编译器识别为密封类,但不会被 Java 编译器识别。对于 JDK17+,Groovy 会将密封类信息写入字节码。这些被称为原生密封类。请参见@SealedOptions
注解以进一步控制是否创建模拟或原生密封类。 -
Java 对密封层次结构中的类在同一个模块或同一个包中有一些要求。Groovy 目前没有强制执行此要求,但可能会在将来的版本中强制执行。特别是,原生密封类(请参见前一点)可能需要此要求。
|
|
记录和类似记录的类(孵化器)
Java 14 和 15 引入了记录作为预览特性,对于 Java 16,记录已从预览状态毕业。根据这篇文章 记录重点文章,记录“以更少的仪式对简单数据聚合进行建模”。
Groovy 具有 @Immutable
和 @Canonical
AST 变换等特性,这些特性已经支持以更少的仪式对数据聚合进行建模,尽管这些特性在一定程度上与记录的设计重叠,但它们不是直接等效的。记录最接近于 @Immutable
,只是在其中添加了一些变体。
Groovy 4 添加了对 JDK16+ 的原生记录以及早期 JDK 上类似记录的类的支持(也称为模拟记录)。类似记录的类具有原生记录的所有特性,但在字节码级别没有与原生记录相同的信息,因此在跨语言集成场景中不会被 Java 编译器识别为记录。请参见 @RecordOptions
注解以进一步控制是否创建模拟或原生记录。
类似记录的类看起来有点类似于使用 Groovy 的 @Immutable
AST 变换时生成的类。该变换本身就是一个元注解(也称为注解收集器),它结合了更细粒度的特性。提供这些特性的类似记录的重新组合相对简单,这就是 Groovy 4 在其记录实现中提供的内容。
您可以按照以下方式编写记录定义
record Cyclist(String firstName, String lastName) { }
或者以这种更长的形式(这或多或少是上面单行定义转换成的内容)
@groovy.transform.RecordType
class Cyclist {
String firstName
String lastName
}
您可以按照以下示例使用它
def richie = new Cyclist('Richie', 'Porte')
这将生成一个具有以下特征的类
-
它隐式地是 final 的
-
它有一个私有的 final 字段
firstName
,带有访问器方法firstName()
;lastName
也是如此 -
它有一个默认的
Cyclist(String, String)
构造函数 -
它有一个默认的
serialVersionUID
为 0L -
它具有隐式的
toString()
、equals()
和hashCode()
方法
@RecordType
注解结合了以下变换/标记注解
@RecordBase
@RecordOptions
@TupleConstructor(namedVariant = true, force = true, defaultsMode = DefaultsMode.AUTO)
@PropertyOptions
@KnownImmutable
@POJO
@CompileStatic
RecordBase
注解还提供 @ToString
和 @EqualsAndHashCode
功能,要么委托给这些变换,要么提供特殊的原生记录等效项。
我们热切地希望获得更多关于 Groovy 用户如何使用记录或类似记录的结构的反馈。
|
|
内置类型检查器
Groovy 的静态特性包括一个可扩展的类型检查机制。此机制允许用户
-
选择性地削弱类型检查,以允许更具动态风格的代码解析静态检查,或者
-
增强类型检查,允许 Groovy 在需要的情况下比 Java 更严格
到目前为止,我们知道此特性已被公司内部使用(例如类型检查的 DSL),但我们还没有看到广泛共享类型检查器扩展。从 Groovy 4 开始,我们在可选的 groovy-typecheckers
模块中捆绑了一些选定的类型检查器,以鼓励进一步使用此特性。
第一个包含的是正则表达式检查器。考虑以下代码
def newYearsEve = '2020-12-31'
def matcher = newYearsEve =~ /(\d{4})-(\d{1,2})-(\d{1,2}/
这段代码通过了编译,但在运行时失败,并出现 PatternSyntaxException
,因为我们“意外地”遗漏了最后的闭合括号。我们可以使用新的检查器在编译时获得此反馈,如下所示
import groovy.transform.TypeChecked
@TypeChecked(extensions = 'groovy.typecheckers.RegexChecker')
def whenIs2020Over() {
def newYearsEve = '2020-12-31'
def matcher = newYearsEve =~ /(\d{4})-(\d{1,2})-(\d{1,2}/
}
这会产生预期的编译错误
1 compilation error: [Static type checking] - Bad regex: Unclosed group near index 26 (\d{4})-(\d{1,2})-(\d{1,2} at line: 6, column: 19
与往常一样,Groovy 的编译器定制机制允许您简化此类检查器的应用,例如,以编译器配置脚本为例,使其全局应用。
我们欢迎您提供有关要在 Groovy 中包含的额外类型检查器扩展的更多反馈。
内置宏方法
Groovy 宏在 Groovy 2.5 中引入,旨在简化创建 AST 变换和其他操作编译器 AST 数据结构的代码。宏的一部分称为宏方法,允许在编译期间将看起来像是全局方法调用替换为转换后的代码。
与类型检查器扩展类似,我们知道此功能已在许多地方使用,但到目前为止,我们还没有看到宏方法的广泛共享。从 Groovy 4 开始,我们在可选的 groovy-macro-library
模块中捆绑了一些精选的宏方法,以鼓励进一步使用此功能。
第一个包含项有助于进行老式调试(穷人的序列化?)。假设在编码过程中,您定义了许多变量
def num = 42
def list = [1 ,2, 3]
def range = 0..5
def string = 'foo'
假设现在您想将它们打印出来以进行调试。您可以编写一些合适的 println
语句,并可能加入一些对 format()
的调用。您甚至可能使用 IDE 来帮助您完成此操作。或者,SV
和 NV
宏方法可以帮助您。
SV
宏方法创建一个字符串(实际上是 gapi:groovy.lang.GString),其中包含变量的名称和值。以下是一个示例
println SV(num, list, range, string)
输出结果为
num=42, list=[1, 2, 3], range=[0, 1, 2, 3, 4, 5], string=foo
在这里,SV
宏方法在编译过程中开始生效。编译器将明显的全局 SV
方法调用替换为一个表达式,该表达式组合了提供的变量的名称和 toString()
值。
还存在两种其他变体。SVI
调用 Groovy 的 inspect()
方法而不是 toString()
,而 SVD
调用 Groovy 的 dump()
方法。因此,以下代码
println SVI(range)
会产生以下输出
range=0..5
以及以下代码
println SVD(range)
会产生以下结果
range=<groovy.lang.IntRange@14 from=0 to=5 reverse=false inclusiveRight=true inclusiveLeft=true modCount=0>
NV
宏方法提供与 SV
类似的功能,但它不是创建“字符串”,而是创建 gapi:groovy.lang.NamedValue,允许您进一步处理名称和值信息。以下是一个示例
def r = NV(range)
assert r instanceof NamedValue
assert r.name == 'range' && r.val == 0..5
还存在一个 NVL
宏方法,它创建 NamedValue
实例列表。
def nsl = NVL(num, string)
assert nsl*.name == ['num', 'string']
assert nsl*.val == [42, 'foo']
我们欢迎您提供有关要在 Groovy 中包含的额外宏方法的更多反馈。如果您启用了此可选模块,但希望限制启用哪些宏方法,现在有一个机制可以禁用单个宏方法(和扩展方法)GROOVY-9675。
JavaShell(孵化中)
JavaShell 是 GroovyShell 的 Java 等效项,允许您更轻松地使用 Java 代码片段。例如,以下代码片段展示了编译记录(JDK14)并使用 Groovy 检查其 toString
import org.apache.groovy.util.JavaShell
def opts = ['--enable-preview', '--release', '14']
def src = 'record Coord(int x, int y) {}'
Class coordClass = new JavaShell().compile('Coord', opts, src)
assert coordClass.newInstance(5, 10).toString() == 'Coord[x=5, y=10]'
此功能在 Groovy 代码库的许多地方用于测试目的。使用 Java 和 Groovy 编译各种代码片段,以确保编译器按预期运行。我们还使用此功能为多语言开发人员提供生产力增强功能,允许从 Groovy 控制台(以 Java 形式)编译和/或运行 Java 代码。
POJO 注解(孵化中)
Groovy 支持动态和静态特性。动态 Groovy 的强大功能和灵活性源于对运行时(可能很广泛)的使用。静态 Groovy 对运行时库的依赖性要小得多。许多方法调用将具有对应于直接 JVM 方法调用的字节码(类似于 Java 字节码),而 Groovy 运行时通常会被完全绕过。但即使对于静态 Groovy,对 Groovy jar 的硬链接仍然存在。所有 Groovy 类都仍然实现了 GroovyObject
接口(因此具有诸如 getMetaClass
和 invokeMethod
之类的方法),并且还有一些其他地方会调用 Groovy 运行时。
@POJO
标记接口用于指示生成的类更像是普通的 Java 对象,而不是增强的 Groovy 对象。除非与 @CompileStatic
结合使用,否则该注解当前会被忽略。对于这样的类,编译器不会生成 Groovy 通常需要的诸如 getMetaClass()
之类的方法。此功能通常用于生成需要与 Java 或 Java 框架一起使用的类,在某些情况下,Java 可能会因 Groovy 的“管道”方法而感到困惑。
此功能处于孵化阶段。目前,应该将注解的存在视为对编译器的一种提示,以便在可能的情况下生成不依赖于 Groovy 运行时的字节码,但不能保证。
@CompileStatic
的用户会知道,当他们切换到静态 Groovy 时,某些动态功能将无法使用。他们可能会预期使用 @CompileStatic
和 @POJO
可能会导致更多限制。事实并非如此。添加 @POJO
会在某些地方生成更多类似 Java 的代码,但许多 Groovy 功能仍然有效。
考虑以下示例。首先是 Groovy Point
类
@CompileStatic
@POJO
@Canonical(includeNames = true)
class Point {
Integer x, y
}
现在是 Groovy PointList
类
@CompileStatic
@POJO
class PointList {
@Delegate
List<Point> points
}
我们可以使用 groovyc
以通常的方式编译这些类,并应该看到预期生成的 Point.class 和 PointList.class 文件。
然后我们可以编译以下 Java 代码。我们不需要将 Groovy jar 提供给 javac
或 java
,我们只需要上一步生成的类文件。
Predicate<Point> xNeqY = p -> p.getX() != p.getY(); // (1)
Point p13 = new Point(1, 3);
List<Point> pts = List.of(p13, new Point(2, 2), new Point(3, 1));
PointList list = new PointList();
list.setPoints(pts);
System.out.println(list.size());
System.out.println(list.contains(p13));
list.forEach(System.out::println);
long count = list.stream().filter(xNeqY).collect(counting()); // (2)
System.out.println(count);
-
检查 x 是否不等于 y
-
统计 x 不等于 y 的点的数量
请注意,虽然我们的 PointList
类具有许多列表方法可用(size
、contains
、forEach
、stream
等),这得益于 Groovy 的 @Delegate
变换,但这些方法都被烘焙到类文件中,生成的字节码不会调用任何 Groovy 库或依赖任何运行时代码。
运行后,会生成以下输出
3 true Point(x:1, y:3) Point(x:2, y:2) Point(x:3, y:1) 2
从本质上讲,这开辟了使用 Groovy 作为类似于 Lombok 的一种预处理器(但由 Groovy 语言支持)的可能性。
|
Groovy 合约(孵化中)
此可选模块支持按契约编程的风格。更具体地说,它提供合约注解,支持在 Groovy 类和接口上指定类不变式、先决条件和后置条件。以下是一个示例
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)
这会导致将对应于合约声明的检查逻辑按需注入到类的 методах 和构造函数中。检查逻辑将确保在方法执行之前满足任何先决条件,在任何方法执行之后满足任何后置条件,并且在调用方法之前和之后任何类不变式都为真。
此模块替换了以前外部的 gcontracts
项目,该项目现在已归档。
GINQ,也称为 Groovy 集成查询或 GQuery(孵化中)
GQuery 支持以类似 SQL 的风格查询集合。这可能涉及列表和/或映射,或者您自己的域对象,或者在处理例如 JSON、XML 和其他结构化数据时返回的那些对象。
from p in persons
leftjoin c in cities on p.city.name == c.name
where c.name == 'Shanghai'
select p.name, c.name as cityName
from p in persons
groupby p.gender
having p.gender == 'Male'
select p.gender, max(p.age)
from p in persons
orderby p.age in desc, p.name
select p.name
from n in numbers
where n > 0 && n <= 3
select n * 2
from n1 in nums1
innerjoin n2 in nums2 on n1 == n2
select n1 + 1, n2
让我们来看一个完整的示例。假设我们有关于水果、其价格(每 100 克)和维生素 C 浓度(每 100 克)的 JSON 格式的信息。我们可以按如下方式处理 JSON
import groovy.json.JsonSlurper
def json = new JsonSlurper().parseText('''
{
"prices": [
{"name": "Kakuda plum", "price": 13},
{"name": "Camu camu", "price": 25},
{"name": "Acerola cherries", "price": 39},
{"name": "Guava", "price": 2.5},
{"name": "Kiwifruit", "price": 0.4},
{"name": "Orange", "price": 0.4}
],
"vitC": [
{"name": "Kakuda plum", "conc": 5300},
{"name": "Camu camu", "conc": 2800},
{"name": "Acerola cherries", "conc": 1677},
{"name": "Guava", "conc": 228},
{"name": "Kiwifruit", "conc": 144},
{"name": "Orange", "conc": 53}
]
}
''')
现在,假设我们的预算有限,并且想要选择最具成本效益的水果来帮助我们满足每日维生素 C 的需求。我们将连接价格和维生素 C 信息,并按最具成本效益的水果进行排序。我们将选择前 2 名,以防我们去购物时我们的首选水果没有库存。我们可以看到,对于这些数据,卡卡杜李子紧随其后的是奇异果,是我们最好的选择
assert GQ {
from p in json.prices
join c in json.vitC on c.name == p.name
orderby c.conc / p.price in desc
limit 2
select p.name
}.toList() == ['Kakuda plum', 'Kiwifruit']
我们可以再次查看 XML 的相同示例。我们的 XML 处理代码可能类似于
import groovy.xml.XmlSlurper
def root = new XmlSlurper().parseText('''
<root>
<prices>
<price name="Kakuda plum">13</price>
<price name="Camu camu">25</price>
<price name="Acerola cherries">39</price>
<price name="Guava">2.5</price>
<price name="Kiwifruit">0.4</price>
<price name="Orange">0.4</price>
</prices>
<vitaminC>
<conc name="Kakuda plum">5300</conc>
<conc name="Camu camuum">2800</conc>
<conc name="Acerola cherries">1677</conc>
<conc name="Guava">228</conc>
<conc name="Kiwifruit">144</conc>
<conc name="Orange">53</conc>
</vitaminC>
</root>
''')
我们的 GQuery 代码可能类似于
assert GQ {
from p in root.prices.price
join c in root.vitaminC.conc on c.@name == p.@name
orderby c.toInteger() / p.toDouble() in desc
limit 2
select p.@name
}.toList() == ['Kakuda plum', 'Kiwifruit']
在未来的 Groovy 版本中,我们希望为 SQL 数据库提供 GQuery 支持,其中会根据 GQuery 表达式生成优化的 SQL 查询,这与 Groovy 的 DataSet 功能类似。与此同时,对于少量数据,您可以使用 Groovy 的标准 SQL 功能,该功能将从数据库返回的查询作为集合。以下是数据库具有 Price
和 VitaminC
表(两者都具有 name
和 per100g
列)的代码示例
// ... create sql connection ...
def price = sql.rows('SELECT * FROM Price')
def vitC = sql.rows('SELECT * FROM VitaminC')
assert GQ {
from p in price
join c in vitC on c.name == p.name
orderby c.per100g / p.per100g in desc
limit 2
select p.name
}.toList() == ['Kakuda plum', 'Kiwifruit']
// ... close connection ...
TOML 支持(孵化中)
现在支持处理基于 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'
}
}
}
以及解析
def ts = new TomlSlurper()
def toml = ts.parseText(builder.toString())
assert 'HSV Maloo' == toml.records.car.name
assert 'Holden' == toml.records.car.make
assert 2006 == toml.records.car.year
assert 'Australia' == toml.records.car.country
assert 'http://example.org' == toml.records.car.homepage
assert 'speed' == toml.records.car.record.type
assert 'production pickup truck with speed of 271kph' == toml.records.car.record.description
更多示例可以在 groovy-toml 文档中找到。
其他改进
GString 性能改进
GString 内部结构已进行改进以提高性能。在安全的情况下,GString 的 toString 值现在会自动缓存。虽然很少使用,但 GString 确实允许查看(甚至更改)其内部数据结构。在这种情况下,缓存会被禁用。如果您希望查看而不更改内部数据结构,可以在 GStringImpl
中调用 freeze()
方法,以禁止更改内部数据结构,从而允许缓存保持活动状态。 GROOVY-9637
例如,以下脚本在 Groovy 3 中大约需要 10 秒运行,而在 Groovy 4 中大约需要 0.1 秒运行
def now = java.time.LocalDateTime.now()
def gs = "integer: ${1}, double: ${1.2d}, string: ${'x'}, class: ${Map.class}, boolean: ${true}, date: ${now}"
long b = System.currentTimeMillis()
for (int i = 0; i < 10000000; i++) {
gs.toString()
}
long e = System.currentTimeMillis()
println "${e - b}ms"
增强的范围
Groovy 一直支持包含范围,例如 3..5
,以及排他范围(或在右侧开放),例如 4..<10
。从 Groovy 4 开始,范围可以是封闭的、在左侧开放的,例如 3<..5
,右侧或两侧开放,例如 0<..<3
。对于此类范围,范围将排除最左侧或最右侧的值。 GROOVY-9649
支持没有前导零的小数分数字面量
Groovy 以前要求小数值具有前导零,但现在也支持省略前导零。
def half = .5
def otherHalf = 0.5 // leading zero remains supported
double third = .333d
float quarter = .25f
def fractions = [.1, .2, .3]
// can be used for ranges too (with a rare edge case you might want to avoid)
def range1 = -1.5..<.5 // okay here
def range2 = -1.5.. .5 // space is okay but harder for humans (1)
def range3 = -1.5..0.5 // leading zero edge case (1)
assert range3 == [-1.5, -.5, .5]
-
没有前导零的小数值不能立即出现在范围
..
运算符之后。三个点会造成混淆,并且类似于可变参数表示法。您应该留出空格(对于人类读者来说可能仍然会造成混淆)或保留前导零(推荐这样做)。
JSR308 改进(孵化中)
Groovy 在最近的版本中一直在改进 JSR-308 支持。在 Groovy 4.0 中,添加了额外的支持。特别是,现在支持泛型类型上的类型注解。这对 Jqwik 属性化测试库和 Bean Validation 2 框架等工具的用户非常有用。以下是一个 Jqwik 测试示例
@Grab('net.jqwik:jqwik:1.5.5')
import net.jqwik.api.*
import net.jqwik.api.constraints.*
class PropertyBasedTests {
@Property
def uniqueInList(@ForAll @Size(5) @UniqueElements List<@IntRange(min = 0, max = 10) Integer> aList) {
assert aList.size() == aList.toSet().size()
assert aList.every{ anInt -> anInt >= 0 && anInt <= 10 }
}
}
在早期版本的 Groovy 中,处理了 @Forall
、@Size
和 @UniqueElements
注解,但 List
泛型类型上的 @IntRange
注解没有出现在生成的字节码中,现在它出现了。
以下是一个 Bean Validation 2 框架示例
@Grab('org.hibernate.validator:hibernate-validator:7.0.1.Final')
@Grab('org.hibernate.validator:hibernate-validator-cdi:7.0.1.Final')
@Grab('org.glassfish:jakarta.el:4.0.0')
import jakarta.validation.constraints.*
import jakarta.validation.*
import groovy.transform.*
@Canonical
class Car {
@NotNull @Size(min = 2, max = 14) String make
@Min(1L) int seats
List<@NotBlank String> owners
}
def validator = Validation.buildDefaultValidatorFactory().validator
def violations = validator.validate(new Car(make: 'T', seats: 1))
assert violations*.message == ['size must be between 2 and 14']
violations = validator.validate(new Car(make: 'Tesla', owners: ['']))
assert violations*.message.toSet() == ['must be greater than or equal to 1', 'must not be blank'] as Set
violations = validator.validate(new Car(make: 'Tesla', owners: ['Elon'], seats: 2))
assert !violations
同样,除了 List
泛型类型上的 @NonBlank
注解之外,所有其他注解以前都得到支持,现在 @NonBlank
也会出现在字节码中。
此功能被标记为孵化阶段。生成的字节码预计不会改变,但在该功能脱离孵化状态之前,注释在编译期间的 AST 表示的一些细微细节可能会略微改变。
此外,代码中出现的类型注释,例如局部变量类型、强制转换表达式类型、catch 块异常类型,仍在开发中。
AST 转换优先级
处理 AST 转换的顺序首先由转换中声明的 `phase` 确定,该 `phase` 在转换的 `@GroovyASTTransformation` 声明中声明。对于声明为同一阶段的转换,接下来使用相关转换注释在源代码中出现的顺序。
现在,转换编写者还可以为他们的转换指定优先级。为此,AST 转换必须实现 `TransformWithPriority` 接口,并在实现的 `priority()` 方法中返回他们的优先级作为整数。默认优先级为 `0`。具有最高正优先级的转换将首先被处理。负优先级将在所有优先级为零(默认值)的转换之后处理。
请注意,所有转换仍然一起处理。优先级只影响其他转换之间的排序。相应编译阶段的其他部分保持不变。
旧版整合
旧解析器删除
Groovy 3 引入了新的“鹦鹉”解析器,它支持 lambda 表达式、方法引用以及其他许多调整。在 Groovy 3 中,如果您愿意,您仍然可以恢复到旧解析器。在 Groovy 4 中,旧的基于 Antlr2 的解析器被删除。如果您需要旧解析器,请使用 Groovy 的旧版本。
经典字节码生成删除
在多个版本中,Groovy 可以生成经典的 *基于调用点的* 字节码或针对 JDK7+ invoke dynamic(“indy”)字节码指令的字节码。您可以使用编译器开关在它们之间切换,我们有两套 jar 包(“normal” 和 “-indy”)分别使用和不使用启用的开关构建。在 Groovy 4.0 中,只能生成使用后一种方法的字节码。现在只有一套 jar 包,它们恰好是 indy 风格的。
目前,Groovy 运行时仍然包含使用 Groovy 的旧版本编译的类所需的任何支持。如果您需要生成旧样式的字节码,请使用 Groovy 3.x 之前的版本。
这项工作最初计划在 Groovy 3.0 中进行,但有许多地方“indy”代码明显比“经典”字节码慢。我们已经进行了许多速度改进(从 GROOVY-8298 开始),并且具有一些调整内部阈值的能力(在代码库中搜索 `groovy.indy.optimize.threshold` 和 `groovy.indy.fallback.threshold`)。这项工作为我们带来了有用的速度改进,但我们欢迎进一步的反馈以帮助提高 indy 字节码的整体性能。
其他重大更改
-
Groovy 在其可选的 `groovy-jaxb` 模块中使用 JAXB 技术 时添加了一些非常小的增强功能。由于 JAXB 不再捆绑在 JDK 中,我们删除了此模块。希望使用该功能的用户可能能够将该模块的 Groovy 3 版本与 Groovy 4 一起使用,尽管我们不保证将来会这样做。(GROOVY-10005)。
-
可选的 `groovy-bsf` 模块为 BSF(又名 beanshell)框架的版本 2 提供了 Groovy BSF 引擎。此版本自 2005 年以来一直没有发布,并且已达到生命周期结束。在 Groovy 4 中,我们已删除此模块。希望使用该功能的用户可能能够将该模块的 Groovy 3 版本与 Groovy 4 一起使用,尽管我们不保证将来会这样做。(GROOVY-10023)。
-
以前许多类“泄漏”了 ASM 常量,这些常量本质上是通过实现 `Opcodes` 接口而产生的内部实现细节。这通常不会影响大多数 Groovy 脚本,但可能会影响操作 AST 节点的代码,例如 AST 转换。在使用 Groovy 4 编译之前,可能需要添加一个或多个适当的静态导入语句。扩展 `AbstractASTTransformation` 的 AST 转换是潜在受影响类的示例之一。(GROOVY-9736)。
-
`ASTTest` 以前具有 `RUNTIME` 保留,但现在具有 `SOURCE` 保留。我们不知道有用户使用旧保留,但知道有各种问题保留旧值。 GROOVY-9702
-
Groovy 的 `intersect` DGM 方法在提供投影闭包/比较器时,其语义与其他语言不同。其他语言通常在这种情况下具有 `intersectBy` 方法,而不是像 Groovy 那样重载 `intersect` 运算符。当没有投影函数起作用时,`a.intersect(b)` 应该始终等于 `b.intersect(a)`。当投影函数起作用时,大多数语言将 `a.intersect(b)` 定义为从 `a` 中的元素的子集,这些元素在投影后与来自 `b` 的投影值匹配。因此,结果值始终来自 `a`。可以反转所涉及的对象以从 `b` 中提取元素。Groovy 的语义过去与大多数其他语言相反,但现在已对齐。以下是一些使用新行为的示例
def abs = { a, b -> a.abs() <=> b.abs() } assert [1, 2].intersect([-2, -3], abs) == [2] assert [-2, -3].intersect([1, 2], abs) == [-2] def round = { a, b -> a.round() <=> b.round() } assert [1.1, 2.2].intersect([2.5, 3.5], round) == [2.2] assert [2.5, 3.5].intersect([1.1, 2.2], round) == [2.5]
只需反转对象的顺序即可获得之前的行为,例如,使用 `foo.intersect(bar)` 而不是 `bar.intersect(foo)`。 GROOVY-10275
-
对于各种边缘情况,JavaBean 属性命名约定存在一些不一致,例如,对于名称为单个大写 `X` 并且具有 `getX` 访问器的字段,该字段优先于访问器。 GROOVY-9618
-
许多大多数内部数据结构类,例如 AbstractConcurrentMapBase、AbstractConcurrentMap、ManagedConcurrentMap 已被弃用,它们的用法已替换为更好的替代方案。这应该基本上是不可见的,但某些更改可能会影响直接使用内部 Groovy 类的用户。 GROOVY-9631
-
我们升级了 Picocli 版本。这导致一些 CLI 帮助消息的格式略有变化。我们建议不要依赖此类消息的确切格式。 GROOVY-9627
-
我们目前正在尝试改进 Groovy 代码在某些情况下访问私有字段的方式,在这些情况下,这种访问是预期的,但存在问题,例如在涉及子类或内部类的闭包定义中 (GROOVY-5438)。在解决此问题之前,您可能会注意到 Groovy 4 代码在这些情况下发生中断。作为一种解决方法,您可以尝试在闭包之外使用局部变量来引用相关字段,然后在闭包中引用这些局部变量。
-
较早的 Groovy 版本无意中将常量 -0.0f 和 -0.0d 存储为与 0.0f 和 0.0d 相同。这仅适用于显式常量,即它不适用于导致正零或负零的计算。这也意味着,正零和负零的某些比较在应该不同的情况下返回真值,并且调用 `unique` 可能会导致一个集合仅包含正零,而不是正零和负零(根据 IEEE-745,这是正确答案)。根据您使用的是原始浮点数还是包装浮点数,您可能会或可能不会受到影响。如果您受到影响并希望使用旧的行为,请考虑使用 `equalsIgnoreZeroSign` 和布尔值 `ignoreZeroSign` 构造函数变体到 `NumberAwareComparator`。这些修改也已移植到 Groovy 3,因此请考虑在 Groovy 3 代码中使用它们,而不是依赖旧的行为,以便您的代码可以在不同版本之间正常工作。修复本身尚未移植,以避免破坏依赖于意外有缺陷行为的现有代码。
错误修复:GROOVY-9797
改进的文档和辅助方法:GROOVY-9981 -
各种 Groovy 测试类对 JUnit 3/4 类存在不必要的隐藏依赖关系。修改后,这些类现在可以与例如 JUnit 5(或 Spock)一起使用,而无需在类路径上使用 Junit 3/4。仅当代码明确查看抛出异常的类或通过反射检查类层次结构时,这才是破坏性变更。
`NotYetImplemented`:GROOVY-9492
`GroovyAssert`:GROOVY-9767 -
几个 `Sql#call` 变体错误地抛出 `Exception` 而不是 `SQLException`。这是一个二进制破坏性变更。在使用 Groovy 的旧版本编译代码,然后在 Groovy 4 上运行,反之亦然时,应小心谨慎。 GROOVY-9923
-
我们从公共 API 中删除了 `StaticTypeCheckingVisitor#collectAllInterfacesByName`,因为它存在错误,并且有许多可用的替代方案。我们不知道有任何框架使用此方法。即使它是公开的,它也被认为主要是内部的。 GROOVY-10123
-
两个 jar 文件(`servlet-api.jar` 和 `jsp-api.jar`)名义上是“提供”的依赖项,但以前被复制到 Groovy 二进制发行版中。现在不再是这样了。 GROOVY-9827
-
涉及数组上的 `plus` 的 Groovy 代码在某些上下文中破坏了引用透明性。表达式 `b + c`(其中 `b` 和 `c` 是数组)在 `a = b + c` 和 `b = b + c` 这两个表达式中可能给出不同的结果。后一个表达式(`b += c` 的简写)是类型保持的,但前一个是作为 Object[] 返回的。类型保持行为是预期的行为。 GROOVY-6837。
提示模拟旧行为:如果 `b` 不是 Object 数组,并且您想要 Object 数组结果,那么不要使用 `b + c`,而是使用以下之一
-
b.union(c)
-
new Object[0] + b + c
-
[] as Object[] + b + c
-
-
Groovy 的语法借鉴了 Eiffel 编程语言中的“信息隐藏原则”思想,即访问公共字段或属性(具有 getter 的私有字段)可以具有相同的语法形式。这种想法没有延续到对象元类中的 `getProperties()` 方法。现在 `getProperties()` 也返回公共字段。 GROOVY-10449。
JDK 要求
Groovy 4.0 需要 JDK16+ 才能构建,并且 JDK8 是我们支持的 JRE 的最低版本。Groovy 已在 JDK 8 到 17 版本上进行了测试。
更多信息
您可以浏览所有 在 JIRA 中为 Groovy 4.0 关闭的票证。
4.0.2 增补说明
-
Groovy 4 使用 Gradle 的模块元数据功能增强了为其依赖项存储的元数据。作为此更改的一部分,访问 `groovy-all` 依赖项的方式发生了变化,这对许多用户来说很令人困惑。特别是,需要使用 `platform`,而以前不需要。模块元数据已改进,不再需要使用 `platform`。 GROOVY-10543
-
已添加对 JDK19 的初步支持
-
Groovy 可选地支持使用安全策略文件来触发安全异常,如果执行了未经许可的操作(例如读取属性、退出 JVM 或访问文件等资源)。随着 Java 计划逐步淘汰安全策略框架 (JEP-411),未来的 Groovy 版本可能会逐步淘汰此可选支持。同时,用户可能会在使用此类功能时收到警告消息,并且在 JDK 18 或 19 中可能会出现异常。
-
关于安全异常尤其需要注意(见上一条),在 JDK 18 或 JDK 19 上使用 `groovysh` 时,用户应该将 `JAVA_OPTS` 设置为 `-Djava.security.manager=allow`。`groovysh` 工具使用安全管理器来禁止调用 `System::exit`。预计在某个时候会出现处理这种情况的替代 API,`groovysh` 将在可用时迁移到这些 API。