轩辕李的博客 轩辕李的博客
首页
  • 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、内部类
        • 6.1、静态内部类
        • 6.2、匿名内部类
        • 6.3、创建非静态内部类的实例
        • 7、Lambda表达式和方法引用操作符
        • 8、GStrings
        • 9、字符串和字符字面量
        • 10、相等性判断的差异
        • 11、原始类型和包装类
        • 11.1、使用@CompileStatic进行数字原始类型优化
        • 11.2、正零/负零边缘情况
        • 12、类型转换
        • 13、额外关键字
      • 三、现有Java项目集成Groovy
      • 四、Groovy的新编程范式
        • 1、动态类型系统
        • 2、GString(Groovy字符串)
        • 3、操作符重载
        • 4、类方法扩展
        • 5、元编程
        • 5.1、运行时元编程
        • a、核心概念
        • 5.2、编译时元编程
        • 6、生成器与DSL
        • 6.1、内置生成器
        • 6.2、DSL实战示例
        • 7、脚本化编程与Java集成
        • 7.1、Groovy作为脚本语言
        • 7.2、在Java应用中嵌入Groovy
        • 7.3、实际应用场景
      • 五、测试框架 Spock
        • 1、核心特性
        • 2、高级特性
        • 3、与JUnit的对比优势
      • 六、最佳实践建议
        • 1、何时使用Groovy
        • 2、性能优化技巧
      • 七、总结
      • 八、参考资源
  • 工具

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

Groovy语言探索

# 一、引言

Groovy是一种运行在Java虚拟机(JVM)上的动态编程语言,它不仅完全兼容Java语法,还融入了Python、Ruby等动态语言的优秀特性。

作为Apache软件基金会的顶级项目,Groovy在构建工具(Gradle)、测试框架(Spock)、Web开发(Grails)等领域得到了广泛应用。

# 1、核心优势

  • 无缝Java集成:可以直接调用Java类库,Java代码也能调用Groovy代码
  • 动态特性:支持运行时元编程、动态类型、闭包等高级特性
  • 简洁语法:减少样板代码,提高开发效率
  • DSL支持:强大的领域特定语言创建能力

# 2、Groovy与Java的关系

Groovy与Java的关系可以概括为"源于Java,高于Java":

  1. 完全兼容:任何有效的Java代码都是有效的Groovy代码
  2. 双向互操作:Groovy和Java可以在同一项目中混合使用
  3. 共享生态:可以使用所有Java库和框架
  4. 性能权衡:Groovy提供了@CompileStatic注解来获得接近Java的性能

在实际项目中,典型的应用模式是:

  • Java:编写核心业务逻辑和性能敏感的代码
  • Groovy:编写测试代码、构建脚本、配置文件和快速原型

# 二、Groovy与Java的主要区别

虽然Groovy兼容Java语法,但它引入了许多增强特性来提高开发效率。以下是需要重点掌握的核心区别:

# 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时使用的实现细节。

# 6.1、静态内部类

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

class A {
    static class B {}
}

new A.B()

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

# 6.2、匿名内部类

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)

# 6.3、创建非静态内部类的实例

在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程序员最容易混淆的特性之一:

Java中:

  • == 对于原始类型比较值,对于对象比较引用
  • .equals() 比较对象内容

Groovy中:

  • == 相当于Java的.equals()(比较内容)
  • .is() 相当于Java的==(比较引用)
  • === (Groovy 3+) 等同于.is()
def str1 = new String("hello")
def str2 = new String("hello")

assert str1 == str2        // true (内容相等)
assert !str1.is(str2)      // false (不同对象)
assert str1 === str1       // true (同一对象)

# 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将调用m(long)方法,因为扩展优先于装箱。

# 11.1、使用@CompileStatic进行数字原始类型优化

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

# 11.2、正零/负零边缘情况

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、类型转换

Groovy的类型转换比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 - 自动转换(无需显式转换)
  • C - 需要显式强制转换
  • T - 可转换但数据可能被截断
  • N - 无法转换
  • D - 动态编译时可转换
  • B - 装箱/拆箱操作

关键差异:

  1. Groovy支持更多的自动类型转换
  2. 布尔值可以从任何类型转换(使用Groovy Truth)
  3. 数字类型之间的转换更加灵活
  4. BigInteger和BigDecimal的支持更加完善

示例:Groovy Truth的应用

// 任何非null、非空、非零的值都被视为true
assert !null
assert !""
assert !0
assert ![]
assert ![:]

assert "text"
assert 1
assert [1, 2, 3]
assert [key: "value"]

在转换为布尔值时,Groovy使用Groovy Truth (opens new window)规则。数字类型之间的转换更加灵活,支持BigInteger和BigDecimal的无缝转换。

# 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>4.0.28</version>
</dependency>

<plugin>
    <groupId>org.codehaus.gmavenplus</groupId>
    <artifactId>gmavenplus-plugin</artifactId>
    <version>4.2.1</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.isCase(b)
/ a.div(b) << a.leftShift(b)
% a.mod(b) >> a.rightShift(b)
** a.power(b) >>> a.rightShiftUnsigned(b)
| a.or(b) ++ a.next()
& a.and(b) -- a.previous()
^ a.xor(b) +a 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、元编程

Groovy的元编程能力是其最强大的特性之一,允许在运行时和编译时修改程序行为。

# 5.1、运行时元编程

# a、核心概念

运行时元编程依赖于MetaClass系统,每个Groovy对象都有一个元类,控制着对象的行为。

1. 方法拦截与处理

class DynamicHandler {
    // 处理未定义的方法调用
    def methodMissing(String name, args) {
        if (name.startsWith("find")) {
            return "Searching for ${name[4..-1]}"
        }
        throw new MissingMethodException(name, this.class, args)
    }
    
    // 处理未定义的属性访问
    def propertyMissing(String name) {
        return "Property $name not found"
    }
}

def handler = new DynamicHandler()
println handler.findUser()  // 输出: Searching for User
println handler.unknownProp  // 输出: Property unknownProp not found

2. 动态方法和属性扩展

// 向现有类添加方法
String.metaClass.shout = { -> delegate.toUpperCase() + "!" }
println("hello".shout())  // 输出:HELLO!

// 向现有类添加属性
String.metaClass.category = "Text"
println("hello".category)  // 输出:Text

// 添加静态方法
String.metaClass.static.fromNumber = { int n -> n.toString() }
println(String.fromNumber(123))  // 输出:123

3. 类别(Categories)- 局部扩展

@Category(String)
class StringUtils {
    String reverseString() {
        this.toCharArray().reverse().join('')
    }
    
    boolean isPalindrome() {
        def clean = this.toLowerCase().replaceAll(/\W/, '')
        clean == clean.reverse()
    }
}

use(StringUtils) {
    println "hello".reverseString()  // olleh
    println "racecar".isPalindrome()  // true
}
// 在use块外部,这些方法不可用

4. 实际应用示例:构建器模式

class HtmlBuilder {
    def writer = new StringWriter()
    
    def methodMissing(String name, args) {
        writer << "<$name"
        if (args[0] instanceof Map) {
            args[0].each { k, v -> writer << " $k='$v'" }
            writer << ">"
            if (args.size() > 1) {
                if (args[1] instanceof Closure) {
                    args[1].delegate = this
                    args[1]()
                } else {
                    writer << args[1]
                }
            }
        } else if (args[0] instanceof Closure) {
            writer << ">"
            args[0].delegate = this
            args[0]()
        } else {
            writer << ">$args[0]"
        }
        writer << "</$name>"
    }
    
    String toString() { writer.toString() }
}

def html = new HtmlBuilder()
html.div(class: 'container') {
    h1('Welcome')
    p('This is a paragraph')
}
println html  // 输出格式化的HTML

# 5.2、编译时元编程

编译时元编程通过AST(抽象语法树)转换在编译阶段修改代码,提供更好的性能和类型安全。

常用的AST转换注解

import groovy.transform.*

// 自动生成构造函数、toString、equals和hashCode
@Canonical
class Person {
    String name
    int age
}

// 委托模式实现
class Manager {
    @Delegate
    Worker worker = new Worker()
}

// 不可变类
@Immutable
class Point {
    int x, y
}

// 编译时类型检查
@CompileStatic
class Calculator {
    int add(int a, int b) {
        return a + b  // 编译时验证类型
    }
}

// 懒加载
class DataService {
    @Lazy
    List<String> expensiveData = loadData()
    
    private List<String> loadData() {
        // 模拟耗时操作
        Thread.sleep(1000)
        return ["data1", "data2"]
    }
}

自定义AST转换示例

自定义AST转换允许你在编译时修改代码结构。例如,创建一个自动记录方法执行时间的注解:

import groovy.transform.*

// 使用内置的@TimedInterrupt注解
@TimedInterrupt(value = 5, unit = TimeUnit.SECONDS)
class LongRunningTask {
    void process() {
        // 如果执行超过5秒,将抛出异常
        while(true) {
            // 处理逻辑
        }
    }
}

// 或使用@Memoized缓存方法结果
class FibonacciCalculator {
    @Memoized
    long fibonacci(int n) {
        if (n <= 1) return n
        return fibonacci(n - 1) + fibonacci(n - 2)
    }
}

更多AST转换开发细节请参考官方文档 (opens new window)。

# 6、生成器与DSL

Groovy的生成器模式和DSL支持使得创建领域特定语言变得简单直观。

# 6.1、内置生成器

XML生成器

import groovy.xml.MarkupBuilder

def xml = new MarkupBuilder()
xml.users {
    user(id: 1, active: true) {
        name('Alice')
        email('alice@example.com')
        roles {
            role('admin')
            role('developer')
        }
    }
}
// 生成格式化的XML结构

JSON生成器

import groovy.json.JsonBuilder

def json = new JsonBuilder()
json.config {
    database {
        host 'localhost'
        port 5432
        credentials {
            username 'admin'
            password 'secret'
        }
    }
}
println json.toPrettyString()

# 6.2、DSL实战示例

构建配置DSL

class ConfigDSL {
    Map config = [:]
    
    def database(Closure cl) {
        def db = new DatabaseConfig()
        cl.delegate = db
        cl.resolveStrategy = Closure.DELEGATE_FIRST
        cl()
        config.database = db.settings
    }
    
    class DatabaseConfig {
        Map settings = [:]
        
        def host(String h) { settings.host = h }
        def port(int p) { settings.port = p }
        def credentials(Closure cl) {
            cl.delegate = this
            cl()
        }
        def username(String u) { settings.username = u }
        def password(String p) { settings.password = p }
    }
}

// 使用DSL
def config = new ConfigDSL()
config.database {
    host 'localhost'
    port 5432
    credentials {
        username 'admin'
        password 'secret'
    }
}

这种DSL在Gradle构建脚本中得到了完美应用,使得构建配置变得简洁易读。

# 7、脚本化编程与Java集成

# 7.1、Groovy作为脚本语言

Groovy可以像Python或Shell一样作为脚本语言使用:

# 直接执行脚本文件
groovy script.groovy

# 命令行执行代码
groovy -e "println 'Hello, World!'"

# 作为Unix脚本(添加shebang)
#!/usr/bin/env groovy
println "This is a Groovy script"

# 7.2、在Java应用中嵌入Groovy

使用GroovyScriptEngine动态执行脚本

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

public class ScriptRunner {
    public static void main(String[] args) throws Exception {
        // 创建脚本引擎
        GroovyScriptEngine engine = new GroovyScriptEngine("scripts/");
        
        // 创建变量绑定
        Binding binding = new Binding();
        binding.setVariable("input", "World");
        
        // 执行脚本并获取结果
        Object result = engine.run("hello.groovy", binding);
        System.out.println("Result: " + result);
    }
}

脚本文件 (scripts/hello.groovy)

// 访问Java传入的变量
def message = "Hello, $input!"

// 执行业务逻辑
def process() {
    return message.toUpperCase()
}

// 返回结果给Java
return process()

使用GroovyShell进行简单求值

import groovy.lang.GroovyShell;
import groovy.lang.Binding;

Binding binding = new Binding();
binding.setVariable("x", 10);
binding.setVariable("y", 20);

GroovyShell shell = new GroovyShell(binding);
Object result = shell.evaluate("x + y");
System.out.println(result); // 输出: 30

# 7.3、实际应用场景

  1. 规则引擎:动态加载业务规则脚本
  2. 配置管理:使用Groovy DSL替代XML/JSON配置
  3. 自动化测试:编写灵活的测试脚本
  4. 插件系统:实现可热加载的插件机制
  5. 数据处理:编写ETL转换脚本

# 五、测试框架 Spock

Spock是基于Groovy的BDD(行为驱动开发)测试框架,它让测试代码像自然语言一样易读。

# 1、核心特性

基础测试结构

import spock.lang.*

class UserServiceSpec extends Specification {
    
    def setup() {
        // 每个测试方法执行前运行
    }
    
    def "用户注册应该返回新创建的用户ID"() {
        given: "准备测试数据"
        def username = "alice"
        def email = "alice@example.com"
        
        when: "执行注册操作"
        def userId = userService.register(username, email)
        
        then: "验证结果"
        userId != null
        userId > 0
    }
}

# 2、高级特性

1. 数据驱动测试

@Unroll
def "计算 #a + #b 应该等于 #expected"() {
    expect:
    calculator.add(a, b) == expected
    
    where:
    a  | b  || expected
    1  | 2  || 3
    5  | 7  || 12
    -1 | 1  || 0
}

2. Mock和Stub

def "发送邮件时应该调用邮件服务"() {
    given:
    def emailService = Mock(EmailService)
    def userService = new UserService(emailService)
    
    when:
    userService.resetPassword("user@example.com")
    
    then:
    1 * emailService.sendEmail("user@example.com", _) >> true
    // 1次调用,第二个参数任意,返回true
}

3. 异常测试

def "无效输入应该抛出异常"() {
    when:
    service.process(null)
    
    then:
    def e = thrown(IllegalArgumentException)
    e.message == "Input cannot be null"
}

4. 超时测试

@Timeout(value = 2, unit = TimeUnit.SECONDS)
def "操作应该在2秒内完成"() {
    expect:
    service.longRunningOperation() != null
}

# 3、与JUnit的对比优势

特性 JUnit Spock
语法 注解驱动 BDD风格块
数据驱动 需要@ParameterizedTest 内置where块
Mock框架 需要Mockito等 内置Mock支持
断言 assertEquals等方法 直接使用==
可读性 一般 优秀

Spock通过Groovy的强大特性,让测试代码变得更加简洁、易读和维护。

# 六、最佳实践建议

# 1、何时使用Groovy

✅ 适合场景:

  • 构建脚本(Gradle)
  • 自动化测试(Spock)
  • DSL开发
  • 快速原型开发
  • 脚本和配置文件
  • 与Java项目混合使用

❌ 不适合场景:

  • 性能关键的核心业务逻辑
  • 需要严格类型检查的金融系统
  • 大型团队协作(除非有严格规范)

# 2、性能优化技巧

  1. 使用@CompileStatic:在性能敏感的代码中使用静态编译
  2. 避免过度元编程:运行时元编程会影响性能
  3. 合理使用闭包:闭包虽然灵活但有性能开销
  4. 缓存计算结果:使用@Memoized注解缓存昂贵的计算

# 七、总结

Groovy作为JVM生态系统中的重要一员,它不是要替代Java,而是作为Java的有力补充。通过本文介绍的特性——动态类型、元编程、DSL支持、简洁语法等,Groovy在特定领域展现出了独特的价值。

掌握Groovy不仅能提高开发效率,还能拓展编程思维。无论是使用Gradle构建项目、用Spock编写测试,还是开发DSL、编写脚本,Groovy都能让你的代码更加优雅和高效。

建议读者从实际项目中的小任务开始,逐步将Groovy融入到日常开发中。记住,选择合适的工具解决合适的问题,才是优秀工程师的标志。

# 八、参考资源

  • Groovy官方文档 (opens new window)
  • Spock框架文档 (opens new window)
  • Gradle用户指南 (opens new window)
  • 《Groovy in Action》 (opens new window)

祝你变得更强!

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

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

最近更新
01
AI时代的编程心得
09-11
02
Claude Code与Codex的协同工作
09-01
03
Claude Code实战之供应商切换工具
08-18
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式