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 = closureadds a method; inside the closure,delegateis the object the method was called on.metaClass.staticadds 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.