Skip to content
AgileLogics
7 min read

Runtime Metaprogramming

Prerequisites: Classes and objects, Closures in depth

In dynamic Groovy, every method call and property access goes through the Meta-Object Protocol (MOP). Each class has a metaclass that decides what a call means — and you can change it while the program runs. This is how Grails adds save() and findAllByName() to plain classes, and how Groovy itself adds methods to String and List.

Adding methods to existing classes

String.metaClass.shout = { -> delegate.toUpperCase() + '!' }
Integer.metaClass.isEven = { -> delegate % 2 == 0 }
List.metaClass.static.of3 = { a, b, c -> [a, b, c] }

println 'hello'.shout()
println 4.isEven()
println List.of3(1, 2, 3)
println 'hello'.respondsTo('shout') ? 'String can shout' : 'no'
HELLO!
true
[1, 2, 3]
String can shout
  • Class.metaClass.name = closure adds a method; inside the closure, delegate is the object the method was called on.
  • metaClass.static adds static methods.
  • This is global for the whole JVM — convenient in scripts, risky in libraries. Prefer the scoped techniques below in shared code.

Per-instance changes

class Robot { String name }

def r2 = new Robot(name: 'R2')
def c3 = new Robot(name: 'C3')
r2.metaClass.beep = { -> "$name says beep" }

println r2.beep()
println c3.respondsTo('beep').isEmpty()
R2 says beep
true

methodMissing and propertyMissing

A class can handle calls to methods and properties that don't exist:

class Config {
    private Map values = [:]

    def propertyMissing(String name) { values[name] }
    def propertyMissing(String name, value) { values[name] = value }

    def methodMissing(String name, args) {
        if (name.startsWith('with') && args.size() == 1) {
            def key = name.substring(4).uncapitalize()
            values[key] = args[0]
            return this
        }
        throw new MissingMethodException(name, Config, args)
    }

    String toString() { values.toString() }
}

def cfg = new Config()
cfg.host = 'localhost'
cfg.withPort(8080).withDebug(true)
println cfg.host
println cfg
try {
    cfg.explode()
} catch (MissingMethodException e) {
    println "no method: $e.method"
}
localhost
[host:localhost, port:8080, debug:true]
no method: explode

Grails' dynamic finders (Book.findByTitle('Dune')) are built exactly like this: parse the method name, then do the work.

Intercepting every call

class Calculator implements GroovyInterceptable {
    int add(int a, int b) { a + b }
    int mul(int a, int b) { a * b }

    def invokeMethod(String name, args) {
        System.out.println "-> $name$args"
        def result = metaClass.getMetaMethod(name, args).invoke(this, args)
        System.out.println "<- $result"
        result
    }
}

def calc = new Calculator()
calc.mul(calc.add(2, 3), 4)
-> add[2, 3]
<- 5
-> mul[5, 4]
<- 20

Implementing GroovyInterceptable routes every call through invokeMethod — useful for logging, tracing, or access checks. (System.out.println is used because println itself is a method call and would be intercepted too.)

Expando

def point = new Expando(x: 3, y: 4)
point.length = { -> Math.sqrt(x * x + y * y) }
point.z = 0
println point.length()
println point.properties.keySet().sort()
5.0
[length, x, y, z]

An Expando is an object you build property by property — handy for quick stubs in tests.

When not to use it

Runtime metaprogramming is powerful but invisible: IDEs can't complete the methods, static compilation ignores them, and global metaclass changes leak across a whole application. Use it for DSLs, frameworks, and tests; prefer traits, categories, or extension modules (next lesson) everywhere else.