Перейти к основному содержимому

Метапрограммирование в Groovy

· 8 мин. чтения

1. Обзор

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

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

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

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

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

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

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

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

3.1. свойствоОтсутствует

Когда мы пытаемся получить доступ к неопределенному свойству класса 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.foreach.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 похож на propertyMissing . Однако methodMissing перехватывает вызов любого отсутствующего метода, тем самым избегая MissingMethodException .

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

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

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

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

3.3. ExpandoMetaClass

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

staticExtensionClasses=com.foreach.metaprogramming.extension.StaticEmployeeExtension

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

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

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

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

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

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

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

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

4.1. @Нанизывать

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

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

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

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

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

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

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

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

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

4.2. @TupleConstructor

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

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

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

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

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

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

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

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

4.3. @EqualsAndHashCode

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

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

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

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

4.4. @канонический

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

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

4.5. @AutoClone

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

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

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

4.6. Поддержка ведения журналов с помощью @Log, @Commons, @Log4j, @Log4j2 и @Slf4j

Чтобы добавить поддержку ведения журнала в любой класс Groovy, все, что нам нужно, это добавить аннотации, доступные в пакете groovy.util.logging .

Давайте включим ведение журнала, предоставляемое JDK, добавив аннотацию @Log к классу Employee . После этого мы добавим метод logEmp :

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

Вызов метода logEmp для объекта Employee покажет журналы на консоли:

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

Точно так же доступна аннотация @Commons для добавления поддержки ведения журналов Apache Commons. @Log4j доступен для поддержки ведения журнала Apache Log4j 1.x, а @Log4j2 — для Apache Log4j 2.x. Наконец, используйте @Slf4j , чтобы добавить поддержку Simple Logging Facade для Java .

5. Вывод

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

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

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

Как обычно, реализации кода для этой статьи доступны на GitHub .