Groovy 4.0 版本发布说明

Groovy 4 在早期版本的 Groovy 的现有特性的基础上进行了构建。此外,它还包含了许多新特性,并简化了 Groovy 代码库的各个方面。

注意
警告:Groovy 4 的某些特性被指定为“孵化器”。在适当的情况下,这些特性的相关类或 API 可能使用 @Incubating 注解进行注释。
在使用孵化器特性时应谨慎,因为其细节可能会在后续版本的 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.XmlParsergroovy.ant.AntBuildergroovy.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 子句或 @SealedpermittedSubclasses 注解属性),或者如果同时编译所有相关类型,则自动检测。有关更多详细信息,请参见 (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 的这种做法。话虽如此,保持对扩展的限制(通过使用 finalsealed)将导致未来类型检查可以检查类型穷举使用的情况(例如 switch 表达式)的更多地方。

  • Groovy 使用 @Sealed 注解来支持 JDK8+ 的密封类。这些被称为模拟密封类。此类类会被 Groovy 编译器识别为密封类,但不会被 Java 编译器识别。对于 JDK17+,Groovy 会将密封类信息写入字节码。这些被称为原生密封类。请参见 @SealedOptions 注解以进一步控制是否创建模拟或原生密封类。

  • Java 对密封层次结构中的类在同一个模块或同一个包中有一些要求。Groovy 目前没有强制执行此要求,但可能会在将来的版本中强制执行。特别是,原生密封类(请参见前一点)可能需要此要求。

注意
提示:密封类可以使用 sealed(和相关)关键字或 @Sealed(和相关)注解。关键字样式通常更简洁,但是,如果您拥有尚未提供对新关键字的支持的编辑器或其他工具,那么您可能更喜欢注解样式。用于编写密封类的语法不会影响是否创建原生模拟密封类。这完全由字节码版本和 @SealedOptions 中给出的选项决定。请注意,在使用关键字样式定义的类上使用 @SealedOptions 注解是可以的。
注意
警告:密封类是一个孵化器特性。虽然我们预计不会有大的变化,但一些小的细节可能会在未来的 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 用户如何使用记录或类似记录的结构的反馈。

注意
提示:您可以使用 record 关键字或 @RecordType 注解来创建原生模拟记录。关键字样式通常更简洁,但是,如果您拥有尚未提供对新关键字和紧凑语法的支持的编辑器或其他工具,那么您可能更喜欢注解样式。用于编写记录的语法不会影响是否创建原生模拟记录。这完全由字节码版本和 @RecordOptions 中给出的选项决定。请注意,在使用关键字样式定义的记录上使用 @RecordOptions 注解是可以的。
注意
警告:记录是一个孵化器特性。虽然我们预计不会有大的变化,但一些小的细节可能会在未来的 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 来帮助您完成此操作。或者,SVNV 宏方法可以帮助您。

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 代码。

image

POJO 注解(孵化中)

Groovy 支持动态和静态特性。动态 Groovy 的强大功能和灵活性源于对运行时(可能很广泛)的使用。静态 Groovy 对运行时库的依赖性要小得多。许多方法调用将具有对应于直接 JVM 方法调用的字节码(类似于 Java 字节码),而 Groovy 运行时通常会被完全绕过。但即使对于静态 Groovy,对 Groovy jar 的硬链接仍然存在。所有 Groovy 类都仍然实现了 GroovyObject 接口(因此具有诸如 getMetaClassinvokeMethod 之类的方法),并且还有一些其他地方会调用 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.classPointList.class 文件。

然后我们可以编译以下 Java 代码。我们不需要将 Groovy jar 提供给 javacjava,我们只需要上一步生成的类文件。

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);
  1. 检查 x 是否不等于 y

  2. 统计 x 不等于 y 的点的数量

请注意,虽然我们的 PointList 类具有许多列表方法可用(sizecontainsforEachstream 等),这得益于 Groovy 的 @Delegate 变换,但这些方法都被烘焙到类文件中,生成的字节码不会调用任何 Groovy 库或依赖任何运行时代码。

运行后,会生成以下输出

3
true
Point(x:1, y:3)
Point(x:2, y:2)
Point(x:3, y:1)
2

从本质上讲,这开辟了使用 Groovy 作为类似于 Lombok 的一种预处理器(但由 Groovy 语言支持)的可能性。

注意
警告:并非编译器的所有部分和并非所有 AST 变换都了解 POJO。使用这种方法是否需要 Groovy jar 位于类路径中,结果可能会有所不同。虽然我们预计随着时间的推移,会进行一些改进,允许更多 Groovy 结构与 @POJO 一起使用,但我们目前不保证最终会支持所有结构。因此它处于孵化状态。

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 功能,该功能将从数据库返回的查询作为集合。以下是数据库具有 PriceVitaminC 表(两者都具有 nameper100g 列)的代码示例

// ... 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 ...

更多示例可以在 GQuery 文档 中找到(或直接在 源代码库 中找到)。

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]
  1. 没有前导零的小数值不能立即出现在范围 .. 运算符之后。三个点会造成混淆,并且类似于可变参数表示法。您应该留出空格(对于人类读者来说可能仍然会造成混淆)或保留前导零(推荐这样做)。

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 版本上进行了测试。

更多信息

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。