Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 4535

Metaprogramming in Groovy

$
0
0

1. Overview

Groovy is a dynamic and powerful JVM language which has numerous features like closures and traits.

In this tutorial, we’ll explore the concept of Metaprogramming in Groovy.

2. What is Metaprogramming?

Metaprogramming is a programming technique of writing a program to modify itself or another program using metadata.

In Groovy, it’s possible to perform metaprogramming at both runtime and compile-time. Going forward, we’ll explore a few notable features of both techniques.

3. Runtime Metaprogramming

Runtime metaprogramming enables us to alter the existing properties and methods of a class. Also, we can attach new properties and methods; all at runtime.

Groovy provides a few methods and properties that help to alter the behavior of a class at runtime.

3.1. propertyMissing

When we try to access an undefined property of a Groovy class, it throws a MissingPropertyException. To avoid the exception, Groovy provides the propertyMissing method.

First, let’s write an Employee class with some properties:

class Employee {
    String firstName
    String lastName  
    int age
}

Second, we’ll create an Employee object and try to display an undefined property address. Consequently, it will throw the 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 provides the propertyMissing method to catch the missing property request. Therefore, we can avoid a MissingPropertyException at runtime.

To catch a missing property’s getter method call, we’ll define it with a single argument for the property name:

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

Also, the same method can have the second argument as the value of the property, to catch a missing property’s setter method call:

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

3.2. methodMissing

The methodMissing method is similar to propertyMissing. However, methodMissing intercepts a call for any missing method, thereby avoiding the MissingMethodException.

Let’s try to call the getFullName method on an Employee object. As getFullName is missing, execution will throw the MissingMethodException at runtime:

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

So, instead of wrapping a method call in a try-catch, we can define methodMissing:

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

3.3. ExpandoMetaClass

Groovy provides a metaClass property in all its classes. The metaClass property refers to an instance of the ExpandoMetaClass.

The ExpandoMetaClass class provides numerous ways to transform an existing class at runtime. For example, we can add properties, methods, or constructors.

First, let’s add the missing address property to the Employee class using metaClass property:

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

Moving further, let’s add the missing getFullName method to the Employee class object at runtime:

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

Similarly, we can add a constructor to the Employee class at runtime:

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

Likewise, we can add static methods using metaClass.static.

The metaClass property is not only handy to modify user-defined classes, but also existing Java classes at runtime.

For example, let’s add a capitalize method to the String class:

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

3.4. Extensions

An extension can add a method to a class at runtime and make it accessible globally.

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 (new Date().getYear() + 1900) - 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:

Employee emp = new Employee(age: 28)
assert emp.getYearOfBirth() == 1991

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 static getDefaultObj 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 in the GitHub project.


Viewing all articles
Browse latest Browse all 4535

Trending Articles