1. 概述

Groovy 是一种运行在 JVM 上的动态语言,具备强大的元编程能力。它支持运行时和编译时两种元编程方式,可以极大地提升代码的灵活性和开发效率。

本文将带你深入了解 Groovy 的元编程机制,包括运行时方法拦截、动态修改类结构、以及编译时注解处理等内容。文中会结合具体示例,帮助你快速掌握 Groovy 元编程的核心技巧。


2. 什么是元编程?

元编程(Metaprogramming)是一种通过程序自身来操作、修改程序结构或行为的编程技术。

在 Groovy 中,元编程可以分为两类:

  • 运行时元编程(Runtime Metaprogramming):在程序运行过程中动态修改类或对象的行为。
  • 编译时元编程(Compile-time Metaprogramming):在编译阶段通过注解修改类结构,生成额外代码。

这两种方式各有所长,结合使用可以写出更灵活、更简洁的代码。


3. 运行时元编程

Groovy 提供了多种机制,允许你在运行时动态修改类或对象的行为,包括拦截缺失属性、方法、使用 metaClass 扩展类等。

3.1. propertyMissing

当访问一个类中未定义的属性时,Groovy 会抛出 MissingPropertyException。你可以通过定义 propertyMissing 方法来捕获这种访问,避免异常。

✅ 示例:

class Employee {
    String firstName
    String lastName
    int age
}

尝试访问未定义的 address 属性:

Employee emp = new Employee(firstName: "Norman", lastName: "Lewis")
println emp.address

输出:

groovy.lang.MissingPropertyException: No such property: address for class: Employee

解决方法:

def propertyMissing(String propertyName) {
    "property '$propertyName' is not available"
}

调用:

assert emp.address == "property 'address' is not available"

你也可以为 setter 提供实现:

def propertyMissing(String propertyName, propertyValue) {
    "cannot set $propertyValue - property '$propertyName' is not available"
}

3.2. methodMissing

propertyMissing 类似,methodMissing 用于捕获未定义方法的调用,避免 MissingMethodException

✅ 示例:

def methodMissing(String methodName, def methodArgs) {
    "method '$methodName' is not defined"
}

调用:

assert emp.getFullName() == "method 'getFullName' is not defined"

3.3. ExpandoMetaClass

metaClass 是 Groovy 类中自带的属性,指向 ExpandoMetaClass 实例。通过它,可以在运行时动态添加属性、方法甚至构造函数。

✅ 添加属性:

Employee.metaClass.address = ""
Employee emp = new Employee(firstName: "Norman", lastName: "Lewis", address: "US")
assert emp.address == "US"

✅ 添加方法:

emp.metaClass.getFullName = {
    "$lastName, $firstName"
}
assert emp.getFullName() == "Lewis, Norman"

✅ 添加构造函数:

Employee.metaClass.constructor = { String firstName ->
    new Employee(firstName: firstName)
}
Employee norman = new Employee("Norman")
assert norman.firstName == "Norman"

⚠️ 你甚至可以扩展 Java 原生类,比如给 String 添加方法:

String.metaClass.capitalize = { String str ->
    str.substring(0, 1).toUpperCase() + str.substring(1)
}
assert "norman".capitalize() == "Norman"

3.4. 扩展(Extensions)

扩展允许你在运行时为类添加全局方法。方法必须是静态的,且第一个参数是当前类的实例。

✅ 示例:为 Employee 添加 getYearOfBirth 方法

class BasicExtensions {
    static int getYearOfBirth(Employee self) {
        return Year.now().value - self.age
    }
}

META-INF/services/org.codehaus.groovy.runtime.ExtensionModule 中配置:

moduleName=core-groovy-2
moduleVersion=1.0-SNAPSHOT
extensionClasses=com.baeldung.metaprogramming.extension.BasicExtensions

调用:

def age = 28
Employee emp = new Employee(age: age)
assert emp.getYearOfBirth() == Year.now().value - age

✅ 添加静态方法:

class StaticEmployeeExtension {
    static Employee getDefaultObj(Employee self) {
        return new Employee(firstName: "firstName", lastName: "lastName", age: 20)
    }
}

配置:

staticExtensionClasses=com.baeldung.metaprogramming.extension.StaticEmployeeExtension

调用:

assert Employee.getDefaultObj().firstName == "firstName"

✅ 扩展 Java 类:

public static void printCounter(Integer self) {
    while (self > 0) {
        println self
        self--
    }
}
assert 5.printCounter() == 0
public static Long square(Long self) {
    return self * self
}
assert 40l.square() == 1600l

4. 编译时元编程

Groovy 支持通过注解在编译阶段修改类结构,这些注解通常位于 groovy.transform 包中,功能类似于 Java 的 Lombok。

4.1. @ToString

@ToString 会为类自动生成 toString() 方法。

✅ 示例:

@ToString
class Employee {
    long id
    String firstName
    String lastName
    int age
}

调用:

Employee employee = new Employee(id: 1, firstName: "norman", lastName: "lewis", age: 28)
assert employee.toString() == "com.baeldung.metaprogramming.Employee(1, norman, lewis, 28)"

控制输出内容:

@ToString(includePackage = false, excludes = ['id'])
assert employee.toString() == "Employee(norman, lewis, 28)"

4.2. @TupleConstructor

@TupleConstructor 自动生成按属性顺序的构造函数。

✅ 示例:

@TupleConstructor
class Employee {
    long id
    String firstName
    String lastName
    int age
}

调用:

Employee norman = new Employee(1, "norman", "lewis", 28)
assert norman.toString() == "Employee(norman, lewis, 28)"

默认值处理:

Employee snape = new Employee(2, "snape")
assert snape.toString() == "Employee(snape, null, 0)"

4.3. @EqualsAndHashCode

@EqualsAndHashCode 自动生成 equals()hashCode() 方法。

✅ 示例:

Employee norman = new Employee(1, "norman", "lewis", 28)
Employee normanCopy = new Employee(1, "norman", "lewis", 28)
assert norman == normanCopy
assert norman.hashCode() == normanCopy.hashCode()

4.4. @Canonical

@Canonical@ToString@TupleConstructor@EqualsAndHashCode 的组合注解。

✅ 示例:

@Canonical
class Employee {
    long id
    String firstName
    String lastName
    int age
}

4.5. @AutoClone

@AutoClone 用于自动生成 clone() 方法,避免手动实现 Cloneable 接口。

✅ 示例:

Employee norman = new Employee(1, "norman", "lewis", 28)
def normanCopy = norman.clone()
assert norman == normanCopy

4.6. 日志支持注解

Groovy 提供多种日志注解,简化日志引入过程。

  • @Log:JDK 自带日志
  • @Commons:Apache Commons Logging
  • @Log4j / @Log4j2:Log4j 系列
  • @Slf4j:SLF4J + Logback

✅ 示例:

@Log
class Employee {
    String lastName
    String firstName
    int age

    def logEmp() {
        log.info "Employee: $lastName, $firstName is of $age years age"
    }
}

调用:

Employee employee = new Employee(1, "Norman", "Lewis", 28)
employee.logEmp()

输出:

INFO: Employee: Lewis, Norman is of 28 years age

5. 总结

Groovy 的元编程能力非常强大,无论是运行时动态修改类行为,还是编译时通过注解减少样板代码,都极大地提升了开发效率和代码灵活性。

本文介绍了以下核心内容:

技术 说明
propertyMissing 拦截未定义属性访问
methodMissing 拦截未定义方法调用
ExpandoMetaClass 动态扩展类行为
Extensions 全局扩展方法
@ToString / @TupleConstructor / @EqualsAndHashCode / @Canonical / @AutoClone 编译时生成方法
日志注解 快速引入日志功能

掌握这些技巧,能让你写出更简洁、更灵活、更具表现力的 Groovy 代码。


原始标题:Metaprogramming in Groovy