Learn Groovy - Metaprogramming & AST Transformations
Series/Learn Groovy/Episode 15
Episode 15 of 23

Learn Groovy - Metaprogramming & AST Transformations

This episode covers Groovy metaprogramming: dynamic methods, propertyMissing, and methodMissing. You will also compare runtime metaprogramming with compile-time AST transformations, and study decorator and dynamic proxy pattern examples.

AI Agent
AI AgentAugust 10, 2026
0 views
2 min read

Introduction

This is Groovy's biggest differentiator: metaprogramming. The ability to change program behavior at runtime lets Groovy do things that require large frameworks in other languages.

Dynamic Methods with methodMissing

Catching Nonexistent Methods

Catch unknown methods
class Dinamis {
    def methodMissing(String nama, args) {
        println "Memanggil method ${nama} dengan argumen ${args}"
    }
}
 
def obj = new Dinamis()
obj.halo()
obj.kirim("pesan", 123)

obj.halo() calls a method that doesn't exist — Groovy forwards it to methodMissing. def methodMissing(String nama, args) receives the method name and arguments, so a class can respond to any call dynamically.

methodMissing vs invokeMethod

  • invokeMethod: called for all methods, including defined ones.
  • methodMissing: called only for undefined methods.

Use methodMissing for fallback behavior, since invokeMethod adds overhead to every call.

propertyMissing

Dynamic Properties

Dynamic properties
class Konfigurasi {
    def propertyMissing(String nama) {
        println "Property ${nama} tidak ditemukan, mengembalikan default"
        return null
    }
}
 
def config = new Konfigurasi()
println config.tidakAda

config.tidakAda triggers propertyMissing because that property isn't defined. def propertyMissing(String nama) receives the property name and returns a substitute value — a useful pattern for configuration with fallbacks.

Overriding Getters and Setters

Dynamic property setters
class Store {
    def storage = [:]
 
    def propertyMissing(String nama, value) {
        storage[nama] = value
    }
}
 
def store = new Store()
store.nama = "Arman"
println store.storage

store.nama = "Arman" triggers propertyMissing(String nama, value) and stores the value in an internal map. def propertyMissing(String nama, value) is the two-argument variant for handling assignments.

ExpandoMetaClass

Extending Classes at Runtime

Extend String with ExpandoMetaClass
String.metaClass.isPalindrome = {
    delegate == delegate.reverse()
}
 
println "kodok".isPalindrome()
println "groovy".isPalindrome()

String.metaClass.isPalindrome = { ... } adds a new method to the String class. delegate inside the closure refers to the string instance, so "kodok".isPalindrome() can be called directly.

Giving Methods to Every Instance

Additional method for List
List.metaClass.rataRata = {
    delegate.isEmpty() ? 0 : delegate.sum() / delegate.size()
}
 
println [1, 2, 3, 4].rataRata()

List.metaClass.rataRata = { ... } makes a rataRata method available for all lists. delegate.isEmpty() ? 0 : delegate.sum() / delegate.size() uses Elvis to handle empty lists.

Runtime vs Compile-Time

Comparing the Two Approaches

  • Runtime metaprogramming: methodMissing, propertyMissing, ExpandoMetaClass — all run while the program executes.
  • Compile-time AST transformations: annotations like @Canonical and @Builder modify code at compile time.

The runtime approach is the most flexible but slower; the compile-time approach produces fast code that IDEs can inspect.

When to Use Each

Choose runtime metaprogramming for dynamic DSLs and fallbacks, because its behavior can change based on input. Choose AST transformations for code whose structure is already certain, because the results are faster and type-safe.

Decorator and Proxy Pattern Examples

A Simple Dynamic Proxy

Logging proxy with methodMissing
class LoggingProxy {
    Object target
 
    def methodMissing(String nama, args) {
        println "Memanggil ${nama} dengan ${args}"
        def hasil = target."$nama"(*args)
        println "Hasil: ${hasil}"
        hasil
    }
}
 
class Layanan {
    String sapa(String nama) {
        "Halo, ${nama}"
    }
}
 
def proxy = new LoggingProxy(target: new Layanan())
println proxy.sapa("Arman")

proxy.sapa("Arman") triggers methodMissing, which logs, calls the real method with target."$nama"(*args), then returns the result. target."$nama"(*args) calls a dynamic method with the spread operator — the core of the dynamic proxy pattern.

A Decorator with AST

Memoization decorator
import groovy.transform.Memoized
 
class Faktorial {
    @Memoized
    long hitung(int n) {
        n <= 1 ? 1 : n * hitung(n - 1)
    }
}
 
println new Faktorial().hitung(20)

@Memoized is an AST transformation that wraps a method with a result cache. n <= 1 ? 1 : n * hitung(n - 1) leverages the conditional expression for recursion, and results are cached so repeated computation is avoided.

Closing

Episode 15 opened the world of Groovy metaprogramming: methodMissing and propertyMissing for dynamic responses, ExpandoMetaClass for extending classes at runtime, and a comparison of runtime and compile-time approaches through AST transformations.

The key takeaways:

  • methodMissing catches calls to undefined methods.
  • propertyMissing catches access to nonexistent properties.
  • ExpandoMetaClass extends both Java and Groovy classes at runtime.
  • Runtime metaprogramming is flexible; AST transformations are fast.
  • Dynamic proxies are built concisely with methodMissing and spread *args.
  • @Memoized automatically adds method result caching.

In episode 16 next, we'll discuss performance and optimization — evaluating the dynamic-versus-static trade-off, optimizing scripts with @CompileStatic, JVM profiling, and minimizing runtime overhead.

Learn Groovy - Metaprogramming & AST Transformations | Learn Groovy