«1. Обзор

Groovy — это динамичный и мощный язык JVM, который имеет множество функций, таких как замыкания и трейты.

В этом уроке мы рассмотрим концепцию метапрограммирования в Groovy.

2. Что такое метапрограммирование?

Метапрограммирование — это метод программирования, при котором программа модифицирует себя или другую программу с использованием метаданных.

В Groovy можно выполнять метапрограммирование как во время выполнения, так и во время компиляции. В дальнейшем мы рассмотрим несколько примечательных особенностей обоих методов.

3. Метапрограммирование во время выполнения

Метапрограммирование во время выполнения позволяет нам изменять существующие свойства и методы класса. Также мы можем добавлять новые свойства и методы; все во время выполнения.

Groovy предоставляет несколько методов и свойств, которые помогают изменить поведение класса во время выполнения.

3.1. propertyMissing

Когда мы пытаемся получить доступ к неопределенному свойству класса Groovy, возникает исключение MissingPropertyException. Чтобы избежать исключения, Groovy предоставляет метод propertyMissing.

Во-первых, давайте напишем класс Employee с некоторыми свойствами:

class Employee {
    String firstName
    String lastName  
    int age
}

Во-вторых, мы создадим объект Employee и попробуем отобразить неопределенный адрес свойства. Следовательно, будет выдано исключение MissingPropertyException:

Employee emp = new Employee(firstName: "Norman", lastName: "Lewis")
println emp.address
groovy.lang.MissingPropertyException: No such property: 
address for class: com.baeldung.metaprogramming.Employee

Groovy предоставляет метод propertyMissing для перехвата запроса об отсутствующем свойстве. Таким образом, мы можем избежать MissingPropertyException во время выполнения.

def propertyMissing(String propertyName) {
    "property '$propertyName' is not available"
}
assert emp.address == "property 'address' is not available"

Чтобы поймать вызов метода получения отсутствующего свойства, мы определим его с одним аргументом для имени свойства:

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

Кроме того, тот же метод может иметь второй аргумент в качестве значения свойства свойство, чтобы поймать вызов метода установки отсутствующего свойства:

3.2. methodMissing

try {
    emp.getFullName()
} catch (MissingMethodException e) {
    println "method is not defined"
}

Метод methodMissing похож на propertyMissing. Однако methodMissing перехватывает вызов любого отсутствующего метода, тем самым избегая MissingMethodException.

def methodMissing(String methodName, def methodArgs) {
    "method '$methodName' is not defined"
}
assert emp.getFullName() == "method 'getFullName' is not defined"

Попробуем вызвать метод getFullName для объекта Employee. Так как getFullName отсутствует, выполнение вызовет исключение MissingMethodException во время выполнения:

Итак, вместо того, чтобы оборачивать вызов метода в try-catch, мы можем определить methodMissing:

3.3. ExpandoMetaClass

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

Groovy предоставляет свойство metaClass во всех своих классах. Свойство metaClass ссылается на экземпляр ExpandoMetaClass.

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

Класс ExpandoMetaClass предоставляет множество способов преобразования существующего класса во время выполнения. Например, мы можем добавить свойства, методы или конструкторы.

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

Во-первых, давайте добавим отсутствующее свойство адреса в класс Employee, используя свойство metaClass:

Двигаясь дальше, давайте добавим отсутствующий метод getFullName в объект класса Employee во время выполнения:

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

~~ ~

Точно так же мы можем добавить конструктор к классу Employee во время выполнения:

Точно так же мы можем добавить статические методы, используя metaClass.static.

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

Свойство metaClass удобно не только для изменения пользовательских классов, но и для существующих классов Java во время выполнения.

Например, добавим метод capitalize в класс String:

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

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

3.4. Расширения

Расширение может добавить метод к классу во время выполнения и сделать его доступным глобально.

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

Методы, определенные в расширении, всегда должны быть статическими, с объектом класса self в качестве первого аргумента.

staticExtensionClasses=com.baeldung.metaprogramming.extension.StaticEmployeeExtension

Например, давайте напишем класс BasicExtension, чтобы добавить метод getYearOfBirth в класс Employee:

assert Employee.getDefaultObj().firstName == "firstName"
assert Employee.getDefaultObj().lastName == "lastName"
assert Employee.getDefaultObj().age == 20

Чтобы включить BasicExtensions, нам нужно добавить файл конфигурации в каталог META-INF/services наш проект.

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

Итак, добавим файл org.codehaus.groovy.runtime.ExtensionModule со следующей конфигурацией:

Проверим метод getYearOfBirth, добавленный в класс Employee:

Аналогично, чтобы добавить статические методы в классе, нам нужно определить отдельный класс расширения.

Например, давайте добавим статический метод getDefaultObj в наш класс Employee, определив класс StaticEmployeeExtension:

Затем мы включим StaticEmployeeExtension, добавив следующую конфигурацию в файл ExtensionModule:

«

«Теперь все, что нам нужно, это протестировать наш статический метод getDefaultObj в классе Employee:

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

Точно так же, используя расширения, мы можем добавить метод к предварительно скомпилированным классам Java, таким как Integer и Long:

Employee employee = new Employee()
employee.id = 1
employee.firstName = "norman"
employee.lastName = "lewis"
employee.age = 28

assert employee.toString() == "com.baeldung.metaprogramming.Employee(1, norman, lewis, 28)"

4. Метапрограммирование во время компиляции

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

Используя специальные аннотации, мы можем легко изменить структуру класса во время компиляции. Другими словами, мы можем использовать аннотации для изменения абстрактного синтаксического дерева класса при компиляции.

Давайте обсудим некоторые аннотации, которые очень удобны в Groovy для сокращения шаблонного кода. Многие из них доступны в пакете groovy.transform.

Если мы внимательно проанализируем, мы поймем, что несколько аннотаций предоставляют функции, аналогичные Java Project Lombok.

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

4.1. @ToString

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

Аннотация @ToString добавляет реализацию метода toString по умолчанию в класс во время компиляции. Все, что нам нужно, это добавить аннотацию к классу.

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

Например, добавим аннотацию @ToString к нашему классу Employee:

Теперь мы создадим объект класса Employee и проверим строку, возвращаемую методом toString:

~~ ~ Мы также можем объявить такие параметры, как excludes, include, includePackage и ignoreNulls с помощью @ToString для изменения выходной строки.

Например, исключим id и package из строки объекта Employee:

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

assert norman == normanCopy
assert norman.hashCode() == normanCopy.hashCode()

4.2. @TupleConstructor

Используйте @TupleConstructor в Groovy, чтобы добавить в класс параметризованный конструктор. Эта аннотация создает конструктор с параметром для каждого свойства.

Например, добавим @TupleConstructor в класс Employee:

Теперь мы можем создать объект Employee, передавая параметры в том порядке, в котором свойства определены в классе.

Если мы не указываем значения свойств при создании объектов, Groovy примет значения по умолчанию:

try {
    Employee norman = new Employee(1, "norman", "lewis", 28)
    def normanCopy = norman.clone()
    assert norman == normanCopy
} catch (CloneNotSupportedException e) {
    e.printStackTrace()
}

Подобно @ToString, мы можем объявить такие параметры, как excludes, include и includeSuperProperties с помощью @ TupleConstructor для изменения поведения связанного с ним конструктора по мере необходимости.

4.3. @EqualsAndHashCode

Мы можем использовать @EqualsAndHashCode для создания стандартной реализации методов equals и hashCode во время компиляции.

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

Давайте проверим поведение @EqualsAndHashCode, добавив его в класс Employee:

Employee employee = new Employee(1, "Norman", "Lewis", 28)
employee.logEmp()
INFO: Employee: Lewis, Norman is of 28 years age

4.4. @Canonical

@Canonical представляет собой комбинацию аннотаций @ToString, @TupleConstructor и @EqualsAndHashCode.

Просто добавив его, мы можем легко включить все три в класс Groovy. Кроме того, мы можем объявить @Canonical с любым из конкретных параметров всех трех аннотаций.

4.5. @AutoClone

Быстрый и надежный способ реализовать интерфейс Cloneable — добавить аннотацию @AutoClone.

Проверим метод клонирования после добавления @AutoClone в класс Employee: