轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

  • Spring

  • 其他语言

    • C语言指针二三事
    • 从Java到Kotlin
    • Groovy语言探索
      • 引言
        • 1. 简介
        • 2. Groovy与Java的关系
      • Groovy与Java的区别
        • 1. 默认导入
        • 2. 多态方法
        • 3. 数组初始化
        • 4. 包级可见性
        • 5. ARM块
        • 6. 内部类
        • a. 静态内部类
        • b. 匿名内部类
        • c. 创建非静态内部类的实例
        • 7. Lambda表达式和方法引用操作符
        • 8. GStrings
        • 9. 字符串和字符字面量
        • 10. == 的行为
        • 11. 原始类型和包装类
        • a. 使用@CompileStatic进行数字原始类型优化
        • b. 正零/负零边缘情况
        • 12. 类型转换
        • 13. 额外关键字
      • 现有Java项目集成Groovy
      • Groovy的新编程范式
        • 1. 动态类型系统
        • 2. GString(Groovy字符串)
        • 3. 操作符重载
        • 4. 类方法扩展
        • 5. 元数据编程
        • a 运行时元数据编程
        • 1. GroovyObject介绍
        • 2. 方法缺失处理
        • 3. 字段缺失处理
        • 4. 静态方法缺失处理
        • 5. 方法拦截
        • 6. 方法扩展
        • 7. 动态访问属性和方法
        • 8. GroovyInterceptable接口
        • 9. 类别
        • 10. MetaClass介绍
        • 11. 属性扩展
        • 12. 构造函数扩展
        • 13. 方法扩展
        • 14. 方法借用
        • 15. 动态方法名称
        • 16. 运行时方法和属性发现
        • 17. 方法拦截
        • 18. 属性拦截
        • b 编译时元数据编程
        • 1. 可用的AST转换
        • 2. 开发AST转换
        • 6. 生成器
        • 1. XML 生成器
        • 2. JSON 生成器
        • 3. 使用元编程定制生成器
        • 7. 脚本化编程
        • 1. 脚本文件和命令行执行
        • 2. DSL(领域特定语言)支持
        • 3. Groovy脚本在Java应用中的应用场景
      • 测试框架 Spock
        • 1. 参数化测试
        • 2. 异常测试
        • 3. Mocking 和 Stubbing
      • 总结
  • 工具

  • 后端
  • 其他语言
轩辕李
2023-05-24
目录

Groovy语言探索

# 引言

# 1. 简介

Groovy是一种运行在Java平台上的动态语言,它兼容Java的语法,同时引入了许多新的语法和特性,使得编写Java程序变得更加简单和高效。

Groovy的语法更加接近自然语言,使得代码更易读,同时,其强大的动态特性使得编写复杂的逻辑变得更简单。

# 2. Groovy与Java的关系

Groovy和Java有着密切的关系,Groovy本身是运行在Java平台上的,其底层依赖于Java的运行环境。

因此,Groovy可以无缝地与Java进行互操作,你可以在Groovy中调用Java的库和类,同时,也可以在Java中调用Groovy的代码。

Groovy的语法是兼容Java的,你可以在Groovy的程序中使用纯Java的语法,这使得Java程序员可以很快地学习和使用Groovy。

同时,Groovy也引入了许多新的语法和特性,使得编写Java程序变得更加简单和高效。

在实际的开发中,Groovy和Java经常被一起使用,Groovy通常用于编写测试、构建脚本、以及其他需要快速编写和修改的代码,而Java则用于编写性能要求更高的核心代码。

通过这种方式,Groovy和Java可以完美地配合,提供了一种高效、强大的开发工具。

# Groovy与Java的区别

Groovy力图使得对Java开发人员来说尽可能自然。在设计Groovy时,尽力遵循最少惊讶原则,特别是针对那些来自Java背景的开发人员学习Groovy的情况。

下面列出了Java和Groovy之间的主要区别:

# 1. 默认导入

以下这些包和类默认被导入,即您无需使用显式的导入语句来使用它们:

java.io.*
java.lang.*
java.math.BigDecimal
java.math.BigInteger
java.net.*
java.util.*
groovy.lang.*
groovy.util.*

# 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. 数组初始化

在Java中,数组初始化有两种形式:

int[] array = {1, 2, 3};             // Java数组初始化的简写语法
int[] array2 = new int[] {4, 5, 6};  // Java数组初始化的长写语法

而在Groovy中,{ ... }块被保留用于闭包。这意味着您不能使用Java的数组初始化的简写语法来创建数组字面量。而是可以借用Groovy的字面列表符号,如下所示:

int[] array = [1, 2, 3];

对于Groovy 3+,您还可以选择使用Java的数组初始化的长写语法:

def array2 = new int[] {1, 2, 3} // Groovy 3.0+ 支持Java风格的数组初始化长写语法

# 4. 包级可见性

在Groovy中,省略字段上的修饰符不会像Java那样创建包级私有字段:

class Person {
    String name
}

相反,它用于创建属性,也就是私有字段、相关的getter和setter。

您可以通过使用@PackageScope注解来创建包级私有字段:

class Person {
    @PackageScope String name
}

# 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
   }
}

# 6. 内部类

对于匿名内部类和嵌套类的实现,Groovy遵循Java的方式,但也存在一些差异,例如,不必将局部变量声明为final。在生成内部类字节码时利用了一些在生成groovy.lang.Closure时使用的实现细节。

# a. 静态内部类

下面是静态内部类的示例:

class A {
    static class B {}
}

new A.B()

对于静态内部类的使用是最受支持的一种。如果您确实需要内部类,请将其设置为静态内部类。

# b. 匿名内部类

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)

# c. 创建非静态内部类的实例

在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风格的语法来创建非静态内部类的实例。

# 7. Lambda表达式和方法引用操作符

Java 8+支持Lambda表达式和方法引用操作符(::):

Runnable run = () -> System.out.println("Run");  // Java
list.forEach(System.out::println);

Groovy 3及以上版本也支持这些特性。在Groovy的早期版本中,您应该使用闭包代替:

Runnable run = { println 'run' }
list.each { println it } // 或者 list.each(this.&println)

# 8. GStrings

由于双引号字符串字面量被解释为GString值,因此如果一个包含美元符号的字符串字面量的类使用Groovy和Java编译器进行编译,可能会出现编译错误或产生细微差异的代码。

尽管通常情况下,如果API声明了参数的类型,Groovy将在GString和String之间自动进行类型转换,但要注意接受Object参数的Java API,然后检查实际类型。

# 9. 字符串和字符字面量

在Groovy中,使用单引号字面量表示字符串,而使用双引号字面量表示String或GString,取决于字面量中是否有插值。

assert 'c'.class == String
assert "c".class == String
assert "c${1}".class in GString

当将单个字符的字符串赋值给char类型的变量时,Groovy将自动将其转换为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风格的转换将导致异常。

// 对于单个字符的字符串,两种方式是相同的
assert ((char) "c").class == Character
assert ("c" as char).class == Character

// 对于多个字符的字符串,它们是不同的
try {
  ((char) 'cx') == 'c'
  assert false: 'will fail - not castable'
} catch(GroovyCastException e) {
}
assert ('cx' as char) == 'c'
assert 'cx'.asType(char) == 'c'

# 10. == 的行为

在Java中,== 表示原始类型的相等性或对象的身份。在Groovy中,== 在所有情况下表示相等性。对于非原始类型,它

会将其转换为a.compareTo(b) == 0来评估Comparable对象的相等性,否则评估a.equals(b)。

要检查身份(引用相等性),请使用is方法:a.is(b)。从Groovy 3开始,您还可以使用===运算符(或否定版本):a === b(或c !== d)。

# 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); } 

    public static void main(String[] args) {
       Float calcResult = new Main().calc();
       System.out.println(calcResult); // => 3.0
    }
}

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); // 不能编译,错误:float无法被取消引用
    }
}

使用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) {           
    println "in m(long)"
}

void m(Integer i) {        
    println "in m(Integer)"
}

这是Java将调用的方法,因为扩展优先于拆箱。

# a. 使用@CompileStatic进行数字原始类型优化

由于Groovy在更多的地方转换为包装类,您可能会想知道它是否会为数字表达式生成更低效的字节码。Groovy具有一组高度优化的用于执行数学计算的类。在使用@CompileStatic时,仅涉及原始类型的表达式使用与Java相同的字节码。

# b. 正零/负零边缘情况

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         
assert (float) f3 == (float) f4

assert !f1.equals(f2)
assert !f3.equals(f4)

assert f1.equalsIgnoreZeroSign(f2)
assert f3.equalsIgnoreZeroSign(f4)

请注意,对于非原始类型,==映射到.equals()。

# 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 T - Y
double N C C C C T Y -
  • “Y”表示Java可以进行的转换
  • “C”表示 Java 在显式强制转换时可以进行的转换
  • “T”表示 Java 可以进行的转换,但数据被截断
  • “N”表示 Java 无法进行的转换

Groovy的类型转换规则:

<html>
<div class="box">
   <!-- 在这里插入需要具有水平滚动条的内容 -->
   <table>
    <thead>
      <tr>
        <th>Converts from</th>
        <th>boolean</th>
        <th>Boolean</th>
        <th>byte</th>
        <th>Byte</th>
        <th>short</th>
        <th>Short</th>
        <th>char</th>
        <th>Character</th>
        <th>int</th>
        <th>Integer</th>
        <th>long</th>
        <th>Long</th>
        <th>BigInteger</th>
        <th>float</th>
        <th>Float</th>
        <th>double</th>
        <th>Double</th>
        <th>BigDecimal</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>boolean</td>
        <td>-</td>
        <td>B</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
      </tr>
      <tr>
        <td>Boolean</td>
        <td>B</td>
        <td>-</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
        <td>N</td>
      </tr>
      <tr>
        <td>byte</td>
        <td>T</td>
        <td>T</td>
        <td>-</td>
        <td>B</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>D</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
      </tr>
      <tr>
        <td>Byte</td>
        <td>T</td>
        <td>T</td>
        <td>B</td>
        <td>-</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>D</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
      </tr>
      <tr>
        <td>short</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>D</td>
        <td>-</td>
        <td>B</td>
        <td>Y</td>
        <td>D</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
      </tr>
      <tr>
        <td>Short</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>T</td>
        <td>B</td>
        <td>-</td>
        <td>Y</td>
        <td>D</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
      </tr>
      <tr>
        <td>char</td>
        <td>T</td>
        <td>T</td>
        <td>Y</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
        <td>-</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
      </tr>
      <tr>
        <td>Character</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>-</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
      </tr>
      <tr>
        <td>int</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
        <td>-</td>
        <td>B</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
      </tr>
      <tr>
        <td>Integer</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
        <td>B</td>
        <td>-</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
      </tr>
      <tr>
        <td>long</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
        <td>-</td>
        <td>B</td>
        <td>Y</td>
        <td>T</td>
        <td>T</td>
        <td>T</td>
        <td>T</td>
        <td>Y</td>
      </tr>
      <tr>
        <td>Long</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>T</td>
        <td>Y</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
        <td>B</td>
        <td>-</td>
        <td>Y</td>
        <td>T</td>
        <td>T</td>
        <td>T</td>
        <td>T</td>
        <td>Y</td>
      </tr>
      <tr>
        <td>BigInteger</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>-</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>T</td>
      </tr>
      <tr>
        <td>float</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>T</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>-</td>
        <td>B</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
      </tr>
      <tr>
        <td>Float</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>T</td>
        <td>D</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>B</td>
        <td>-</td>
        <td>Y</td>
        <td>Y</td>
        <td>Y</td>
      </tr>
      <tr>
        <td>double</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>T</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>Y</td>
        <td>Y</td>
        <td>-</td>
        <td>B</td>
        <td>Y</td>
      </tr>
      <tr>
        <td>Double</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>T</td>
        <td>D</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>Y</td>
        <td>D</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>Y</td>
        <td>Y</td>
        <td>B</td>
        <td>-</td>
        <td>Y</td>
      </tr>
      <tr>
        <td>BigDecimal</td>
        <td>T</td>
        <td>T</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>D</td>
        <td>T</td>
        <td>D</td>
        <td>D</td>
        <td>Y</td>
        <td>Y</td>
        <td>-</td>
      </tr>
    </tbody>
  </table>
</div>
<style>
.box{
  overflow-x: auto;
  height: 100%;
}
</style>
</html>
  • 'Y' 表示 Groovy 可以进行转换。
  • 'D' 表示在动态编译或显式转换时,Groovy 可以进行转换。
  • 'T' 表示 Groovy 可以进行转换,但数据会被截断。
  • 'B' 表示装箱/拆箱操作。
  • 'N' 表示 Groovy 无法进行转换。

在转换为布尔值/Boolean 时,截断使用 Groovy Truth (opens new window) 进行转换。将数字转换为字符时,会将 Number.intvalue() 强制转换为 char。当从 Float 或 Double 进行转换时,Groovy 使用 Number.doubleValue() 构造 BigInteger 和 BigDecimal,否则会使用 toString() 进行构造。其他转换的行为由 java.lang.Number 定义。

# 13. 额外关键字

Groovy与Java有许多相同的关键字,并且Groovy 3及以上版本还具有与Java相同的保留类型var。此外,Groovy还具有以下关键字:

  • as
  • def
  • in
  • trait
  • it(在闭包中使用)

与Java相比,Groovy的限制较少,允许某些关键字出现在Java中非法的位置。例如,以下语句是有效的:var var = [def: 1, as: 2, in: 3, trait: 4]。然而,尽管编译器可能能够正常工作,但不建议在可能引起混淆的位置使用上述关键字。特别是避免将它们用作变量、方法和类名,因此,之前的var var示例将被视为不良风格。

关键字的详细文档可参考 (opens new window)。

# 现有Java项目集成Groovy

主要使用Groovy的Maven插件,将Groovy代码编译成Java字节码,然后打包到Java项目中。

这里用到Maven Group: Apache Groovy (opens new window)和gmavenplus-plugin (opens new window)

<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy</artifactId>
    <version>3.0.17</version>
</dependency>

<plugin>
    <groupId>org.codehaus.gmavenplus</groupId>
    <artifactId>gmavenplus-plugin</artifactId>
    <version>2.1.0</version>
    <executions>
        <execution>
            <goals>
                <goal>addSources</goal>
                <goal>compile</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <sources>
            <source>
                <directory>src/main/groovy</directory>
                <includes>
                    <include>**/*.groovy</include>
                </includes>
            </source>
        </sources>
    </configuration>
</plugin>

# Groovy的新编程范式

一个语言的强大主要体现在它的编程范式上,Groovy引入了许多新的编程范式,使得编写代码变得更加简单和高效。

# 1. 动态类型系统

Groovy的一个重要特性是其动态类型系统。在很多编程语言中,如Java,必须在变量声明时指定其类型,这个类型在后续的代码中不能改变。然而,Groovy采用的是动态类型系统,你可以在声明变量时不指定类型,Groovy会在运行时确定变量的类型。

动态类型的好处之一是编程灵活性增强。你不必提前知道或决定一个变量将包含什么类型的数据。这使得代码更加简洁,易于阅读和编写。

例如,下面是一个动态类型的Groovy例子:

def x = 123
println(x.getClass()) // 输出:class java.lang.Integer

x = "Hello, Groovy!"
println(x.getClass()) // 输出:class java.lang.String

在这个例子中,变量x最初被赋值为一个整数,后来被重新赋值为一个字符串。在每一步,Groovy都正确地处理了变量的类型。

动态类型的另一个优点是它使得Groovy更适合脚本编程和快速原型设计。你可以快速地写出一段代码,测试一个想法,而无需花费大量时间来定义和管理变量的类型。

# 2. GString(Groovy字符串)

在Groovy中,字符串是一个非常强大的数据类型,其中的GString(Groovy字符串)提供了许多强大的功能。

GString是Groovy中的字符串类型,它与Java的String有一些相似之处,但也有一些独特的特性。GString的一个主要特性是它支持字符串插值(String Interpolation)。

字符串插值是指在一个字符串中插入表达式,当字符串被求值时,这些表达式也会被求值,并将结果插入到字符串中。这使得构建复杂的字符串变得非常简单。例如:

def name = "Groovy"
def message = "Hello, ${name}!"
println(message)  // 输出:Hello, Groovy!

在这个例子中,${name}是一个表达式,它在字符串被打印时被求值,并将结果插入到字符串中。

GString还支持多行字符串,这使得编写包含多行文本的字符串变得更简单。例如:

def multiLineString = """
This is a
multi-line
string.
"""
println(multiLineString)

在这个例子中,"""被用来定义一个多行字符串。

另外,GString也支持使用+和*操作符进行字符串的拼接和重复。例如:

def hello = "Hello, " + "Groovy!"  // 字符串拼接
println(hello)

def repeat = "Groovy " * 3  // 字符串重复
println(repeat)  // 输出:Groovy Groovy Groovy 

# 3. 操作符重载

Groovy允许您重载各种操作符,以便可以在您自己的类中使用它们。考虑下面这个简单的类:

class Bucket {
    int size

    Bucket(int size) {
        this.size = size
    }

    Bucket plus(Bucket other) {
        return new Bucket(this.size + other.size)
    }
}

Bucket实现了一个名为plus()的特殊方法。通过实现plus()方法,Bucket类现在可以像这样使用+操作符:

def b1 = new Bucket(4)
def b2 = new Bucket(11)
assert (b1 + b2).size == 15

可以使用+操作符将两个Bucket对象相加。

所有(非比较器)Groovy操作符都有对应的方法,您可以在自己的类中实现这些方法。唯一的要求是您的方法必须是公共的,具有正确的名称和正确数量的参数。参数类型取决于您想要在操作符的右侧支持的类型。例如,通过使用以下签名的plus()方法:

Bucket plus(int capacity) {
    return new Bucket(this.size + capacity)
}

您可以支持以下语句:

assert (b1 + 11).size == 15

这是操作符及其对应方法的完整列表:

操作符 方法 操作符 方法
+ a.plus(b) [] a.getAt(b)
- a.minus(b) [] = c a.putAt(b, c)
* a.multiply(b) in a in b
/ a.div(b) << a.leftShift(b)
% a.mod(b) >> a.rightShift(b)
** a.power(b) >>> a.rightShiftUnsigned(b)
\ a.or(b) ++
& a.and(b) -- a.previous()
^ a.xor(b) +a a.positive()
as a.asType(b) -a a.negative()
a() a.call() ~a a.bitwiseNegate()

通过重载这些操作符中的方法,您可以自定义类的操作符行为,以便更自然地处理对象。

# 4. 类方法扩展

在Groovy中,您可以使用类方法扩展(Class Methods Extension)为现有的类添加新的方法,而无需修改原始类的定义。这使您能够在不改变类的源代码的情况下,向类添加自定义行为。

类方法扩展的一般步骤如下:

  1. 创建一个静态方法,并将要扩展的类作为第一个参数。方法可以定义在任何Groovy类中,例如Groovy脚本、Groovy类文件或Groovy的扩展模块。

  2. 在方法体内部,可以使用this关键字引用要扩展的对象实例。注意,扩展方法只能访问对象的公共成员。

  3. 调用扩展方法时,Groovy会自动将方法绑定到对应的对象上,使得该对象可以调用扩展方法。

以下是一个示例,展示了如何使用类方法扩展为String类添加一个startsWithIgnoreCase()方法:

class StringExtensions {
    static boolean startsWithIgnoreCase(String str, String prefix) {
        str.toLowerCase().startsWith(prefix.toLowerCase())
    }
}

use(StringExtensions) {
    def text = "Hello, world!"
    println text.startsWithIgnoreCase("hello")
}

输出结果:

true

在上述示例中,创建了一个名为StringExtensions的静态类,其中包含了一个名为startsWithIgnoreCase()的静态方法。该方法接收一个String类型的参数作为要检查的字符串,以及一个String类型的参数作为要比较的前缀。在方法体内部,将两个字符串都转换为小写,并使用startsWith()方法进行比较。然后,通过use方法将StringExtensions类应用为类方法扩展,使得可以直接在字符串对象上调用startsWithIgnoreCase()方法。

使用类方法扩展时,需要注意以下几点:

  • 类方法扩展只在应用的范围内生效,例如使用use方法指定的范围。
  • 避免在不同的模块中定义相同名称的类方法扩展,以避免冲突。
  • 类方法扩展应该遵循适当的命名约定,以确保代码的可读性和维护性。

类方法扩展是Groovy强大而灵活的特性之一,使您能够轻松地为现有的类添加新的方法,以满足特定的需求,提高代码的可重用性和扩展性。

# 5. 元数据编程

# a 运行时元数据编程

Groovy的运行时元数据编程主要依赖于MetaClass系统和GroovyObject接口。所有的Groovy对象都默认实现了GroovyObject接口,这个接口提供了动态添加方法和属性的能力。

# 1. GroovyObject介绍

GroovyObject是所有Groovy类的基类,它定义了几个用于动态方法和属性处理的方法,如 getProperty,setProperty,invokeMethod等。在实际编程中,你几乎不会直接使用GroovyObject,但是需要理解其作用。

class MyGroovyClass {
    def invokeMethod(String name, Object args) {
        println("Called ${name} with ${args}")
    }
}
def obj = new MyGroovyClass()
obj.someMethod("test") // 输出:Called someMethod with test

在这个例子中,MyGroovyClass重写了GroovyObject的invokeMethod方法,使得任何对未定义方法的调用都会被捕获,并打印出方法名和参数。

# 2. 方法缺失处理

当你尝试调用一个未定义的方法时,Groovy会调用methodMissing方法。你可以重写这个方法来处理未定义的方法调用。

class MethodMissingExample {
    def methodMissing(String name, args) {
        println("Attempted to call ${name} with ${args}")
    }
}

def example = new MethodMissingExample()
example.unknownMethod("test")  // 输出:Attempted to call unknownMethod with [test]

在这个例子中,调用了一个未定义的方法unknownMethod,Groovy捕获了这个调用并调用了重写的methodMissing方法。

# 3. 字段缺失处理

类似地,当你尝试访问一个未定义的字段时,Groovy会调用propertyMissing方法。你可以重写这个方法来处理未定义的字段访问。

class PropertyMissingExample {
    def propertyMissing(String name) {
        println("Attempted to access ${name}")
    }
}

def example = new PropertyMissingExample()
println(example.unknownProperty)  // 输出:Attempted to access unknownProperty

在这个例子中,尝试访问一个未定义的属性unknownProperty,Groovy捕获了这个访问并调用了重写的propertyMissing方法。

# 4. 静态方法缺失处理

你也可以处理对未定义的静态方法的调用,通过重写staticMethodMissing方法。

class StaticMethodMissingExample {
    static def staticMethodMissing(String name, args) {
        println("Attempted to call static ${name} with ${args}")
    }
}

StaticMethodMissingExample.unknownStaticMethod("test")  // 输出:Attempted to call static unknownStaticMethod with [test]

在这个例子中,尝试调用一个未定义的静态方法unknownStaticMethod,Groovy捕获了这个调用并调用了重写的staticMethodMissing方法。

# 5. 方法拦截

你可以通过重写invokeMethod方法来拦截所有的方法调用。

class InvokeMethodExample {
    def invokeMethod(String name, args) {
        println("Attempted to call ${name} with ${args}")
    }
}

def example = new InvokeMethodExample()
example.anyMethod("test")  // 输出:Attempted to call anyMethod with [test]

在这个例子中,调用了一个任意的方法anyMethod,Groovy捕获了这个调用并调用了重写的invokeMethod方法。

# 6. 方法扩展

你可以向任何对象添加新的方法。这是通过修改对象的metaClass来实现的。

String.metaClass.shout = { -> delegate.toUpperCase() + "!" }
println("hello".shout())  // 输出:HELLO!

在这个例子中,向String类添加了一个shout方法,这个方法将字符串转换为大写并添加一个感叹号。

# 7. 动态访问属性和方法

通过getProperty和setProperty方法,Groovy支持动态地访问和修改属性。类似地,invokeMethod方法允许动态地调用方法。

class DynamicAccessExample {
    def getProperty(String name) {
        println("Getting ${name}")
    }

    def setProperty(String name, value) {
        println("Setting ${name} to ${value}")
    }

    def invokeMethod(String name, args) {
        println("Calling ${name} with ${args}")
    }
}

def example = new DynamicAccessExample()
example.someProperty = "test"  // 输出:Setting someProperty to test
println(example.someProperty)  // 输出:Getting someProperty
example.someMethod("test")  // 输出:Calling someMethod with [test]

在这个例子中,动态地访问和修改了一个属性,并动态地调用了一个方法。

# 8. GroovyInterceptable接口

GroovyInterceptable接口提供了一种更灵活的方法拦截机制。当一个类实现了这个接口,所有对这个类的方法的调用都会被拦截,即使这个方法已经在类中定义。

class InterceptableExample implements GroovyInterceptable {
    def invokeMethod(String name, args) {
        println("Intercepted call to ${name} with ${args}")
    }
}

def example = new InterceptableExample()
example.someMethod("test")  // 输出:Intercepted call to someMethod with [test]

在这个例子中,调用了一个任意的方法someMethod,尽管这个方法并未在类中定义,但是由于类实现了GroovyInterceptable接口,这个调用仍然被拦截。

# 9. 类别

Groovy的类别(Categories)允许你向一个类添加新的方法,但是这些方法只在类别的作用范围内有效。这是一种局部的方法扩展。

class StringCategory {
    static String shout(String self) {
        return self.toUpperCase() + "!"
    }
}

use(StringCategory) {
    println("hello".shout())  // 输出:HELLO!
}
println("hello".shout())  // 抛出异常:No signature of method...

在这个例子中,向String类添加了一个shout方法,但是这个方法只在use(StringCategory)的作用范围内有效。

也可以使用@Category注解来定义类别。

@Category(String)
class StringUtilCategory {
    static String shout(String self) {
        return self.toUpperCase() + "!"
    }
}
# 10. MetaClass介绍

每个Groovy对象都有一个元类(MetaClass),元类包含了这个对象的所有方法和属性。你可以通过修改元类来添加、修改或删除方法和属性。

String.metaClass.shout = { -> delegate.toUpperCase() + "!" }
println("hello".shout())  // 输出:HELLO!

在这个例子中,向String类的元类添加了一个shout方法,这个方法将字符串转换为大写并添加一个感叹号。

ExpandoMetaClass是Groovy的一个内置类,它允许你更灵活地修改元类。例如,你可以向一个类的所有实例添加新的方法,而不仅仅是一个对象。

String.metaClass = new ExpandoMetaClass(String, false, true)
String.metaClass.shout = { -> delegate.toUpperCase() + "!" }
println("hello".shout())  // 输出:HELLO!
println("world".shout())  // 输出:WORLD!

在这个例子中,向String类的所有实例添加了一个shout方法,而不仅仅是"hello"字符串对象。

# 11. 属性扩展

你可以通过修改元类来向任何对象添加新的属性。

String.metaClass.category = "Text"
println("hello".category)  // 输出:Text

在这个例子中,向String类添加了一个category属性,这个属性对所有的String对象都有效。

# 12. 构造函数扩展

你可以通过修改元类来向任何对象添加新的构造函数。

String.metaClass.constructor = { int count, char c -> String.valueOf(Character.toChars(c)).multiply(count) }
println(new String(5, 'a'))  // 输出:aaaaa

在这个例子中,向String类添加了一个新的构造函数,这个构造函数接收一个数字和一个字符,然后生成一个包含指定数量字符的字符串。

# 13. 方法扩展

你可以通过修改元类来向任何类添加新的静态方法。

String.metaClass.static.fromNumber = { int number -> number.toString() }
println(String.fromNumber(123))  // 输出:123

在这个例子中,向String类添加了一个fromNumber静态方法,这个方法将一个数字转换为字符串。

如果是实例方法扩展,则需要去掉static关键字。

class Book {
   String title
}

Book.metaClass.titleInUpperCase << {-> title.toUpperCase() }

def b = new Book(title:"The Stand")

assert "THE STAND" == b.titleInUpperCase()
# 14. 方法借用

你可以方法指针语法“借用”其他类的方法,使得这些方法在当前类中可用。

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()

在这个例子中,“借用”了BorrowMethodsExample类的所有方法,并在MyGroovyClass中使用这些方法。

# 15. 动态方法名称

你可以动态地生成方法名称,然后调用这些方法。

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

在这个例子中,动态地生成了一个方法名称,然后调用了这个方法。

# 16. 运行时方法和属性发现

你可以在运行时发现一个对象的所有方法和属性。

class MethodAndPropertyDiscoveryExample {
    def exampleMethod() { }
    def exampleProperty
}

def example = new MethodAndPropertyDiscoveryExample()
println(example.metaClass.methods*.name)  // 输出所有方法的名称
println(example.metaClass.properties*.name)  // 输出所有属性的名称

在这个例子中,打印出了一个对象的所有方法和属性的名称。

# 17. 方法拦截

你可以拦截对静态方法的调用,通过重写invokeStaticMethod方法。

class StaticMethodInterceptionExample {
    static def exampleMethod() { }
}

StaticMethodInterceptionExample.metaClass.invokeStaticMethod = { Object object, String method, Object[] arguments ->
    println("Attempted to call static ${method} on ${object.name} with ${arguments}")
}

StaticMethodInterceptionExample.exampleMethod()  // 输出:Attempted to call static exampleMethod on StaticMethodInterceptionExample with []

在这个例子中,尝试调用一个静态方法,Groovy捕获了这个调用并调用了重写的invokeStaticMethod方法。

拦截示例方法调用,通过重写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()
# 18. 属性拦截
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

# b 编译时元数据编程

# 1. 可用的AST转换

Groovy提供了一些内置的AST转换,你可以通过注解来使用这些转换。

例如,@ToString注解会自动生成toString方法。

import groovy.transform.ToString

@ToString
class ToStringExample {
    def name
    def value
}

def example = new ToStringExample(name: "test", value: 123)
println(example)  // 输出:ToStringExample(test, 123)

在这个例子中,使用@ToString注解生成了一个toString方法,这个方法返回对象的名称和值。

同样,Groovy还提供了一些其他的AST转换,例如@TupleConstructor(自动生成一个元组构造函数)、@Canonical(自动生成equals、hashCode和toString方法)、@Lazy(延迟初始化属性)、@Sortable(自动生成比较方法)和@Delegate(委托方法的实现)。

# 2. 开发AST转换

这里只介绍本地AST转换,关于全局AST转换请参考:Global transformations (opens new window)。

创建注解和对应的ASTTransformation:

import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target

@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(classes = WithLoggingASTTransformation)
@interface WithLogging {
}
import groovy.transform.CompileStatic
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.expr.ArgumentListExpression
import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.codehaus.groovy.ast.expr.VariableExpression
import org.codehaus.groovy.ast.stmt.BlockStatement
import org.codehaus.groovy.ast.stmt.ExpressionStatement
import org.codehaus.groovy.ast.stmt.Statement
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation

@CompileStatic
@GroovyASTTransformation(phase= CompilePhase.SEMANTIC_ANALYSIS)
class WithLoggingASTTransformation implements ASTTransformation {

    @Override
    void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
        MethodNode method = (MethodNode) nodes[1]

        def startMessage = createPrintlnAst("Starting $method.name")
        def endMessage = createPrintlnAst("Ending $method.name")

        def existingStatements = ((BlockStatement)method.code).statements
        existingStatements.add(0, startMessage)
        existingStatements.add(endMessage)

    }

    private static Statement createPrintlnAst(String message) {
        new ExpressionStatement(
                new MethodCallExpression(
                        new VariableExpression("this"),
                        new ConstantExpression("println"),
                        new ArgumentListExpression(
                                new ConstantExpression(message)
                        )
                )
        )
    }
}

测试:

@WithLogging
def greet() {
    println "Hello World"
}
greet()

输出:

Starting greet
Hello World
Ending greet

# 6. 生成器

Groovy 中的生成器是用于简化复杂对象创建的强大工具。这些对象可以是任何复杂的数据结构,例如 XML 或 JSON 数据,或者 UI 组件等。

# 1. XML 生成器

XML 生成器可以让你以更自然、更易读的方式生成 XML。以下是一个简单的例子:

import groovy.xml.MarkupBuilder

def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.person(id: '1') {
    name("John Doe")
    email("john.doe@example.com")
}
println writer.toString()

输出结果:

<person id='1'>
    <name>John Doe</name>
    <email>john.doe@example.com</email>
</person>

# 2. JSON 生成器

同样,Groovy 也提供了 JSON 生成器:

import groovy.json.JsonBuilder

def json = new JsonBuilder()
json.person {
    id 1
    name "John Doe"
    email "john.doe@example.com"
}
println json.toPrettyString()

输出结果:

{
    "person": {
        "id": 1,
        "name": "John Doe",
        "email": "john.doe@example.com"
    }
}

# 3. 使用元编程定制生成器

除了使用Groovy内置的生成器,你也可以使用元编程技术来创建自定义的生成器。这通常通过实现methodMissing或者修改MetaClass来完成。

下面是一个简单的例子,展示了如何创建一个HTML生成器:

class HtmlBuilder {
    def out = new StringWriter()

    def methodMissing(String name, args) {
        out << "<$name>"
        if (args.length > 0 && args[0] instanceof Closure) {
            args[0].delegate = this
            args[0].call()
        }
        out << "</$name>\n"
    }

    String toString() {
        out.toString()
    }
}

def html = new HtmlBuilder()
html.html {
    head {
        title { 'Hello, World!' }
    }
    body {
        h1 { 'Welcome to my website' }
        p { 'This is a paragraph.' }
    }
}
println html

在这个例子中,创建了一个HtmlBuilder类,这个类重写了methodMissing方法,使得可以使用任何方法来表示HTML标签。使用闭包来表示标签的内容,闭包的委托对象就是HTML生成器,所以可以在闭包中使用任何方法。

输出的HTML将会是:

<html>
<head>
<title>Hello, World!</title>
</head>
<body>
<h1>Welcome to my website</h1>
<p>This is a paragraph.</p>
</body>
</html>

这只是一个基础的例子,实际的HTML生成器会更复杂。例如,你可能需要处理属性、自闭合标签,或者使用缩进来改善输出的可读性。

通过元编程,你可以创建任何类型的生成器。你只需要了解你想要生成的结构的语法,然后使用Groovy的动态特性来模拟这个语法。

# 7. 脚本化编程

# 1. 脚本文件和命令行执行

Groovy 的一个强大特性是它支持脚本化编程。你可以将 Groovy 代码放入 .groovy 文件中,然后像运行其他脚本语言(如 Python 或 Shell 脚本)一样来运行它。例如,假设你有一个文件名为 script.groovy 的 Groovy 脚本,你可以使用以下命令来运行它:

groovy script.groovy

此外,你可以在命令行中直接执行 Groovy 代码。例如:

groovy -e "println 'Hello, World!'"

这将在控制台打印出 "Hello, World!"。

# 2. DSL(领域特定语言)支持

Groovy 提供了创建 DSL(领域特定语言)的强大支持。DSL 是为特定问题领域设计的计算机语言,它使用特定领域的术语和概念,从而使领域专家能够直接与程序交互,而不需要了解编程知识。

通过 Groovy 的动态特性和闭包,你可以创建流畅易读的 DSL。以下是一个简单的例子,定义了一个 DSL 用于描述饮料:

class Beverage {
    String name
    float price

    static Beverage drink(Closure closure) {
        Beverage beverage = new Beverage()
        closure.delegate = beverage
        closure.call()
        return beverage
    }

    def named(String name) { this.name = name }
    def costs(float price) { this.price = price }
}

Beverage beverage = Beverage.drink {
    named 'Coffee'
    costs 5.00
}

println "${beverage.name} costs \$${beverage.price}"

在这个例子中,定义了一个 Beverage 类,它有两个方法:named 和 costs。使用这两个方法在 drink 方法的闭包中描述一个饮料。输出将是 "Coffee costs $5.0"。

典型的DSL应用就是Gradle构建脚本。Gradle使用Groovy作为构建脚本的语言,这使得Gradle构建脚本非常易读。

# 3. Groovy脚本在Java应用中的应用场景

这里主要介绍GroovyScriptEngine的使用。它提供了一种在运行时动态加载和执行Groovy脚本的机制。

以下是一个简单的示例,展示了GroovyScriptEngine的使用:

import groovy.util.GroovyScriptEngine;
import groovy.lang.Binding;

public class GroovyScriptExample {
    public static void main(String[] args) throws Exception {
        // 创建GroovyScriptEngine对象,指定脚本存储位置
        String[] roots = { "scripts/" }; // 脚本存储在scripts目录下
        GroovyScriptEngine scriptEngine = new GroovyScriptEngine(roots);
        
        // 创建Binding对象,用于传递变量给脚本
        Binding binding = new Binding();
        binding.setVariable("name", "Alice");
        
        // 执行脚本
        scriptEngine.run("greeting.groovy", binding);
        
        // 从脚本中获取结果
        String result = (String) binding.getVariable("result");
        System.out.println(result);
    }
}

假设在scripts/目录下有一个名为greeting.groovy的脚本文件,其内容如下:

def greeting = "Hello, $name!"
result = greeting.toUpperCase()

运行上述Java代码,将会输出:

HELLO, ALICE!

# 测试框架 Spock

Spock 是一个基于 Groovy 的测试框架,它提供了一种更清晰、更直观的方式来编写测试,尤其是单元测试。

与 JUnit 相比,Spock 提供了更高级的功能,例如参数化测试、异常测试和强大的 mock 框架。

下面是一个 Spock 测试的基本结构:

import spock.lang.Specification

class SimpleTest extends Specification {
    def "length of Spock's and his friends' names"() {
        expect:
        "Spock".length() == 5
        "Kirk".length() == 4
        "Scotty".length() == 6
    }
}

在这个例子中,创建了一个 Spock 测试,测试了几个字符串的长度。注意,使用了描述性的字符串作为测试方法的名称,这使得测试的目的更明确。

# 1. 参数化测试

Spock 支持参数化测试,你可以使用数据表格来为测试方法提供不同的参数:

class ParameterizedTest extends Specification {
    def "maximum of #a and #b is #c"() {
        expect:
        Math.max(a, b) == c

        where:
        a | b || c
        3 | 7 || 7
        5 | 4 || 5
        9 | 9 || 9
    }
}

在这个例子中,测试了 Math.max 方法。每一行数据都会创建一个新的测试,a、b 和 c 的值都会从数据表格中取得。

# 2. 异常测试

Spock 可以方便地测试异常。例如:

class ExceptionTest extends Specification {
    def "should throw NullPointerException when doSomething is called"() {
        when:
        doSomething(null)

        then:
        thrown(NullPointerException)
    }
}

在这个例子中,测试了 doSomething 方法当传入 null 参数时是否抛出 NullPointerException。

# 3. Mocking 和 Stubbing

Spock 提供了强大的 mocking 和 stubbing 功能。你可以创建 mock 对象,然后定义这些对象的行为,或者验证它们是否被正确地调用。

class MockingTest extends Specification {
    def "should call the save method of the repository"() {
        given:
        def repository = Mock(Repository)
        def service = new Service(repository)

        when:
        service.save("data")

        then:
        1 * repository.save("data")
    }
}

在这个例子中,创建了一个 Repository 的 mock 对象,然后验证了 Service.save 方法是否调用了 repository.save。

Spock利用Groovy的AST转换机制将易读的测试规范转换为标准的JUnit测试方法,然后使用自定义的运行时拦截器来解释和执行这些测试方法。

这种组合使得Spock能够提供更具可读性和表达力的测试语法,并与其他测试工具和框架无缝集成。

# 总结

Groovy是一种功能强大的动态编程语言,具有丰富的元编程特性和灵活的语法。

本文着重对其新编程范式,也就是Java中不支持的功能进行了介绍。

通过掌握这些Groovy的特性,您可以更好地利用Groovy的灵活性和强大的元编程能力,提高代码的可读性、表达力和可维护性。

同时,您也可以通过DSL和Spock等工具,编写清晰、可维护的单元测试和领域特定语言,提升开发效率和代码质量。

祝你变得更强!

编辑 (opens new window)
#Groovy
上次更新: 2023/06/14
从Java到Kotlin
从Eclipse到IDEA,金字塔到太空堡垒

← 从Java到Kotlin 从Eclipse到IDEA,金字塔到太空堡垒→

最近更新
01
Spring Boot版本新特性
09-15
02
Spring框架版本新特性
09-01
03
Spring Boot开发初体验
08-15
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式