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 等工具,我们期望支持更严格的穷尽 case 分支检查,类似于 Java。这可以在使用 Groovy 的静态特性时自动实现,或通过附加的可选类型检查扩展实现。因此,开发人员可能希望不依赖自动默认分支返回 null,而是提供自己的默认值或穷尽覆盖所有分支。

密封类型

密封类、接口和特征限制了哪些其他类或接口可以扩展或实现它们。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 的未来版本可能有一个规则,允许希望遵循 Java 实践的 Groovy 开发人员。话虽如此,保持扩展限制(通过使用 finalsealed)将导致更多地方可以对类型的穷尽使用进行未来类型检查(例如 switch 表达式)。

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

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

注意
提示: 密封类可以使用 sealed(及相关)关键字或 @Sealed(及相关)注解。关键字样式通常更简洁,但是如果您的编辑器或其他工具尚未提供对新关键字的支持,您可能更喜欢注解样式。用于编写密封类的语法不会影响是创建原生密封类还是模拟密封类。这完全由字节码版本和 @SealedOptions 中给出的选项决定。请注意,在使用关键字样式定义的类上使用 @SealedOptions 注解是没问题的。
注意
警告: 密封类是孵化中的特性。虽然我们不期望有大的变化,但一些次要细节可能会在未来的 Groovy 版本中发生变化。

记录和类记录(孵化中)

Java 14 和 15 引入了 records 作为预览特性,并且在 Java 16 中,records 从预览状态毕业。根据这篇 records 聚焦文章,records“以更少的繁琐建模简单数据聚合”。

Groovy 具有 @Immutable@Canonical AST 转换等特性,它们已经支持以更少的繁琐建模数据聚合,虽然这些特性在一定程度上与 records 的设计重叠,但它们不是直接等效的。Records 最接近 @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')

这将生成一个具有以下特征的类

  • 它是隐式最终的

  • 它有一个私有最终字段 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(孵化中)

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 代码。我们不需要 javacjava 可用的 Groovy jar,我们只需要上一步生成的类文件。

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 类借助 Groovy 的 @Delegate 转换提供了许多列表方法(sizecontainsforEachstream 等),但这些方法都内置在类文件中,并且生成的字节码不调用任何 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-Integrated Query 或 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信息连接起来,并按最具成本效益的水果排序。我们将选择前两种,以防我们购物时首选没有库存。我们可以看到,对于这些数据,卡卡杜李子和猕猴桃是我们的最佳选择

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 转换的处理顺序首先由转换的 @GroovyASTTransformation 声明中声明的 phase 决定。对于声明在同一阶段的转换,然后使用关联转换注解在源代码中出现的顺序。

现在,转换编写者还可以为其转换指定优先级。为此,AST 转换必须实现 TransformWithPriority 接口,并在实现的 priority() 方法中返回其优先级作为整数。默认优先级为 0。具有最高正优先级的转换将首先处理。负优先级将在所有优先级为零(默认值)的转换之后处理。

请注意,转换仍然会一起处理。优先级只影响其他转换之间的顺序。各自编译器阶段的其他部分保持不变。

旧版本整合

旧解析器移除

Groovy 3 引入了新的“Parrot”解析器,支持 lambda、方法引用和许多其他调整。在 Groovy 3 中,您仍然可以回溯到旧解析器(如果需要)。在 Groovy 4 中,基于旧版 Antlr2 的解析器已被移除。如果需要旧解析器,请使用旧版 Groovy。

经典字节码生成移除

在许多版本中,Groovy 可以生成经典的基于调用站点的字节码或针对 JDK7+ 调用动态(“indy”)字节码指令的字节码。您可以通过编译器开关在它们之间切换,并且我们有两组 jar(“normal”和“-indy”),分别在启用和未启用开关的情况下构建。在 Groovy 4.0 中,只能生成使用后一种方法的字节码。现在只有一组 jar,它们恰好是 indy 风格的。

目前,Groovy 运行时仍然包含使用 Groovy 旧版本编译的类所需的任何必要支持。如果您需要生成旧式字节码,请使用 Groovy 3.x 及以下版本。

这项工作最初计划在 Groovy 3.0 中完成,但在许多地方,“indy”代码明显慢于“经典”字节码。我们已经进行了许多速度改进(从 GROOVY-8298 开始),并且能够调整内部阈值(在代码库中搜索 groovy.indy.optimize.thresholdgroovy.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 相同。这仅适用于显式常量,即不适用于导致正负零的计算。这也意味着正负零的某些比较在它们应该不同时返回 true,并且调用 unique 可能会导致集合只包含正零,而不是正负零(根据 IEEE-745 的正确答案)。根据您使用的是基本浮点类型还是包装浮点类型,您可能会或可能不会受到影响。如果您受到影响并希望保留旧行为,请考虑使用 equalsIgnoreZeroSignNumberAwareComparator 的布尔 ignoreZeroSign 构造函数变体。这些修改也已回溯到 Groovy 3,因此考虑在 Groovy 3 代码中使用它们,而不是依赖旧行为,以便您的代码可以在不同版本之间正确运行。修复本身并未回溯,以避免破坏依赖于意外缺陷行为的现有代码。
    错误修复:GROOVY-9797
    改进的文档和辅助方法:GROOVY-9981

  • 各种 Groovy 测试类对 JUnit 3/4 类有不必要的隐藏依赖。修改后,这些类现在可以在没有 Junit 3/4 在类路径上的情况下与例如 JUnit 5(或 Spock)一起使用。这只在代码明确查看抛出异常的类或通过反射检查类层次结构时才是一个重大更改。
    NotYetImplemented: GROOVY-9492
    GroovyAssert: GROOVY-9767

  • 几个 Sql#call 变体错误地抛出 Exception 而不是 SQLException。这是一个二进制重大更改。使用旧版 Groovy 编译依赖这些方法的代码,然后在 Groovy 4 上运行,反之亦然时,应谨慎。 GROOVY-9923

  • 我们从公共 API 中删除了 StaticTypeCheckingVisitor#collectAllInterfacesByName,因为它存在错误,并且有许多替代方案可用。我们不知道有任何框架使用此方法。尽管它是公共的,但它主要被认为是内部的。GROOVY-10123

  • 两个 jar 文件(servlet-api.jarjsp-api.jar)名义上是“provided”依赖项,但之前被复制到 Groovy 二进制发行版中。现在不再是这种情况。GROOVY-9827

  • 涉及数组 plus 的 Groovy 代码在某些情况下破坏了引用透明性。表达式 b + c,其中 bc 是数组,可能在两个表达式 a = b + cb = 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,现在却需要。模块元数据已得到改进,不再需要使用 platformGROOVY-10543

  • 已添加对 JDK19 的初步支持

  • Groovy 可选地支持使用安全策略文件来触发安全异常,如果执行不允许的操作(例如读取属性、退出 JVM 或访问文件等资源)。随着 Java 计划逐步淘汰安全策略框架(JEP-411),未来的 Groovy 版本可能会逐步淘汰此可选支持。在此期间,用户如果在 JDK 18 或 19 中使用此类功能,可能会收到警告消息,并可能遇到异常。

  • 特别值得注意的是,关于安全异常(见上一点),在 JDK18 或 JDK19 上使用 groovysh 时,用户应将 JAVA_OPTS 设置为 -Djava.security.manager=allowgroovysh 工具使用安全管理器来禁止调用 System::exit。预计在某个时候会出现处理此场景的替代 API,groovysh 将在可用时转向这些 API。