Métaprogrammation dans Groovy

1. Vue d'ensemble

Groovy est un langage JVM dynamique et puissant qui possède de nombreuses fonctionnalités telles que des fermetures et des traits.

Dans ce didacticiel, nous explorerons le concept de la métaprogrammation dans Groovy.

2. Qu'est-ce que la métaprogrammation?

La métaprogrammation est une technique de programmation consistant à écrire un programme pour se modifier lui-même ou un autre programme en utilisant des métadonnées.

Dans Groovy, il est possible d'effectuer une métaprogrammation à la fois à l'exécution et à la compilation. À l'avenir, nous explorerons quelques caractéristiques notables des deux techniques.

3. Métaprogrammation d'exécution

La métaprogrammation d'exécution nous permet de modifier les propriétés et méthodes existantes d'une classe. En outre, nous pouvons attacher de nouvelles propriétés et méthodes; tout à l'exécution.

Groovy fournit quelques méthodes et propriétés qui aident à modifier le comportement d'une classe au moment de l'exécution.

3.1. propriété

Lorsque nous essayons d'accéder à une propriété non définie d'une classe Groovy, cela lève une MissingPropertyException. Pour éviter l'exception, Groovy fournit la méthode propertyMissing .

Tout d'abord, écrivons une classe Employee avec quelques propriétés:

class Employee { String firstName String lastName int age }

Deuxièmement, nous allons créer un objet Employee et essayer d'afficher une adresse de propriété non définie . Par conséquent, il lancera l' exception 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 fournit la méthode propertyMissing pour intercepter la demande de propriété manquante. Par conséquent, nous pouvons éviter une MissingPropertyException au moment de l'exécution.

Pour intercepter l'appel de la méthode getter d'une propriété manquante, nous la définirons avec un seul argument pour le nom de la propriété:

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

En outre, la même méthode peut avoir le deuxième argument comme valeur de la propriété, pour intercepter l'appel de méthode setter d'une propriété manquante:

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

3.2. méthode manquante

La méthode methodMissing est similaire à propertyMissing . Cependant, methodMissing intercepte un appel pour toute méthode manquante, évitant ainsi l' exception MissingMethodException .

Essayons d'appeler la méthode getFullName sur un objet Employee . Comme getFullName est manquant, l'exécution lèvera l'exception MissingMethodException à l'exécution:

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

Ainsi, au lieu d'encapsuler un appel de méthode dans un try-catch , nous pouvons définir methodMissing :

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

3.3. ExpandoMetaClass

Groovy fournit une propriété metaClass dans toutes ses classes. La propriété metaClass fait référence à une instance de ExpandoMetaClass .

La classe ExpandoMetaClass fournit de nombreuses façons de transformer une classe existante au moment de l'exécution. Par exemple, nous pouvons ajouter des propriétés, des méthodes ou des constructeurs.

Tout d'abord, ajoutons la propriété d' adresse manquante à la classe Employee à l' aide de la propriété metaClass :

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

En allant plus loin, ajoutons la méthode getFullName manquante à l' objet de classe Employee au moment de l'exécution:

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

De même, nous pouvons ajouter un constructeur à la classe Employee au moment de l'exécution:

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

De même, nous pouvons ajouter des méthodes statiques en utilisant metaClass.static.

La propriété metaClass est non seulement pratique pour modifier les classes définies par l'utilisateur, mais également les classes Java existantes au moment de l'exécution.

Par exemple, ajoutons une méthode capitalize à la classe String :

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

3.4. Extensions

Une extension peut ajouter une méthode à une classe lors de l'exécution et la rendre accessible globalement.

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.