Metaprogrammazione in Groovy

1. Panoramica

Groovy è un linguaggio JVM dinamico e potente che ha numerose funzionalità come chiusure e tratti.

In questo tutorial, esploreremo il concetto di metaprogrammazione in Groovy.

2. Che cos'è la metaprogrammazione?

La metaprogrammazione è una tecnica di programmazione per scrivere un programma per modificare se stesso o un altro programma utilizzando i metadati.

In Groovy, è possibile eseguire la metaprogrammazione sia in fase di esecuzione che in fase di compilazione. Andando avanti, esploreremo alcune caratteristiche notevoli di entrambe le tecniche.

3. Metaprogrammazione runtime

La metaprogrammazione runtime ci consente di alterare le proprietà ei metodi esistenti di una classe. Inoltre, possiamo allegare nuove proprietà e metodi; tutto in fase di esecuzione.

Groovy fornisce alcuni metodi e proprietà che aiutano a modificare il comportamento di una classe in fase di esecuzione.

3.1. propertyMissing

Quando proviamo ad accedere a una proprietà non definita di una classe Groovy, viene generata un'eccezione MissingPropertyException. Per evitare l'eccezione, Groovy fornisce il metodo propertyMissing .

Per prima cosa, scriviamo una classe Employee con alcune proprietà:

class Employee { String firstName String lastName int age }

In secondo luogo, creeremo un oggetto Employee e proveremo a visualizzare un indirizzo di proprietà non definito . Di conseguenza, genererà 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 fornisce il metodo propertyMissing per rilevare la richiesta di proprietà mancante. Pertanto, possiamo evitare una MissingPropertyException in fase di esecuzione.

Per catturare la chiamata al metodo getter di una proprietà mancante, la definiremo con un singolo argomento per il nome della proprietà:

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

Inoltre, lo stesso metodo può avere il secondo argomento come valore della proprietà, per catturare la chiamata al metodo setter di una proprietà mancante:

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

3.2. methodMissing

Il metodo methodMissing è simile a propertyMissing . Tuttavia, methodMissing intercetta una chiamata per qualsiasi metodo mancante, evitando così l' eccezione MissingMethodException .

Proviamo a chiamare il metodo getFullName su un oggetto Employee . Poiché getFullName è mancante, l'esecuzione genererà MissingMethodException in fase di esecuzione:

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

Quindi, invece di racchiudere una chiamata al metodo in un try-catch , possiamo definire methodMissing :

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

3.3. ExpandoMetaClass

Groovy fornisce una proprietà metaClass in tutte le sue classi. La proprietà metaClass fa riferimento a un'istanza di ExpandoMetaClass .

La classe ExpandoMetaClass offre numerosi modi per trasformare una classe esistente in fase di esecuzione. Ad esempio, possiamo aggiungere proprietà, metodi o costruttori.

Innanzitutto, aggiungiamo la proprietà address mancante alla classe Employee usando la proprietà metaClass :

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

Andando oltre, aggiungiamo il metodo getFullName mancante all'oggetto classe Employee in fase di esecuzione:

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

Allo stesso modo, possiamo aggiungere un costruttore alla classe Employee in fase di esecuzione:

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

Allo stesso modo, possiamo aggiungere metodi statici usando metaClass.static.

La proprietà metaClass non è solo utile per modificare classi definite dall'utente, ma anche classi Java esistenti in fase di runtime.

Ad esempio, aggiungiamo un metodo capitalize alla classe String :

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

3.4. Estensioni

Un'estensione può aggiungere un metodo a una classe in fase di esecuzione e renderlo accessibile a livello globale.

The methods defined in an extension should always be static, with the self class object as the first argument.

For example, let's write a BasicExtension class to add a getYearOfBirth method to the Employee class:

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

To enable the BasicExtensions, we'll need to add the configuration file in the META-INF/services directory of our project.

So, let's add the org.codehaus.groovy.runtime.ExtensionModule file with the following configuration:

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

Let's verify the getYearOfBirth method added in the Employee class:

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

Similarly, to add static methods in a class, we'll need to define a separate extension class.

For instance, let's add a static method getDefaultObj to our Employee class by defining StaticEmployeeExtension class:

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

Then, we enable the StaticEmployeeExtension by adding the following configuration to the ExtensionModule file:

staticExtensionClasses=com.baeldung.metaprogramming.extension.StaticEmployeeExtension

Now, all we need is to test our staticgetDefaultObj method on the Employee class:

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

Similarly, using extensions, we can add a method to pre-compiled Java classes like Integer and 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. Compile-time Metaprogramming

Using specific annotations, we can effortlessly alter the class structure at compile-time. In other words, we can use annotations to modify the abstract syntax tree of the class at the compilation.

Let's discuss some of the annotations which are quite handy in Groovy to reduce boilerplate code. Many of them are available in the groovy.transform package.

If we carefully analyze, we'll realize a few annotations provides features similar to Java's Project Lombok.

4.1. @ToString

The @ToString annotation adds a default implementation of the toString method to a class at compile-time. All we need is to add the annotation to the class.

For instance, let's add the @ToString annotation to our Employee class:

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

Now, we'll create an object of the Employee class and verify the string returned by the toString method:

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

We can also declare parameters such as excludes, includes, includePackage and ignoreNulls with @ToString to modify the output string.

For example, let's exclude id and package from the string of the Employee object:

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

4.2. @TupleConstructor

Use @TupleConstructor in Groovy to add a parameterized constructor in the class. This annotation creates a constructor with a parameter for each property.

For example, let's add @TupleConstructor to the Employee class:

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

Now, we can create Employee object passing parameters in the order of properties defined in the class.

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

If we don't provide values to the properties while creating objects, Groovy will consider default values:

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

Similar to @ToString, we can declare parameters such as excludes, includes and includeSuperProperties with @TupleConstructor to alter the behavior of its associated constructor as needed.

4.3. @EqualsAndHashCode

We can use @EqualsAndHashCode to generate the default implementation of equals and hashCode methods at compile time.

Let's verify the behavior of @EqualsAndHashCode by adding it to the Employee class:

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

4.4. @Canonical

@Canonical is a combination of @ToString, @TupleConstructor, and @EqualsAndHashCode annotations.

Just by adding it, we can easily include all three to a Groovy class. Also, we can declare @Canonical with any of the specific parameters of all three annotations.

4.5. @AutoClone

A quick and reliable way to implement Cloneable interface is by adding the @AutoClone annotation.

Let's verify the clone method after adding @AutoClone to the Employee class:

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

4.6. Logging Support With @Log, @Commons, @Log4j, @Log4j2, and @Slf4j

To add logging support to any Groovy class, all we need is to add annotations available in groovy.util.logging package.

Let's enable the logging provided by JDK by adding the @Log annotation to the Employee class. Afterward, we'll add the logEmp method:

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

Calling the logEmp method on an Employee object will show the logs on the console:

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

Similarly, the @Commons annotation is available to add Apache Commons logging support. @Log4j is available for Apache Log4j 1.x logging support and @Log4j2 for Apache Log4j 2.x. Finally, use @Slf4j to add Simple Logging Facade for Java support.

5. Conclusion

In this tutorial, we've explored the concept of metaprogramming in Groovy.

Along the way, we've seen a few notable metaprogramming features both for runtime and compile-time.

At the same time, we've explored additional handy annotations available in Groovy for cleaner and dynamic code.

As usual, the code implementations for this article are available over on GitHub.