Runtime Metaprogramming
25.1 The Meta-Object Protocol (MOP)
In dynamic Groovy, method calls go through the MetaClass. This allows interception, injection, and synthesis of methods and properties at runtime.
Call flow (simplified): invokeMethod (if GroovyInterceptable) → MetaClass methods (including injected ones) → declared methods → methodMissing → invokeMethod fallback → MissingMethodException.
25.2 Adding Methods with ExpandoMetaClass
String.metaClass.shout = { -> delegate.toUpperCase() + "!" }
println "hello".shout() // HELLO!
Integer.metaClass.isEven = { -> delegate % 2 == 0 }
println 4.isEven()
String.metaClass.static.random = { int len -> (1..len).collect { ('a'..'z')[new Random().nextInt(26)] }.join() }
println String.random(5)
class Person { String name }
Person.metaClass.greet = { "Hi, I'm $name" } // affects all instances
Person.metaClass.constructor = { String n -> new Person(name: n.capitalize()) }
def p = new Person("ana")
println p.greet()
def single = new Person(name: "Solo")
single.metaClass.secret = { "only me" } // per-instance metaclass
println single.secret()
GroovySystem.metaClassRegistry.removeMetaClass(String) // reset25.3 Intercepting and Synthesizing Methods
class DynamicFinder {
def methodMissing(String name, args) {
if (name.startsWith("findBy")) {
def field = name - "findBy"
return "Finding by ${field.uncapitalize()} = ${args[0]}"
}
throw new MissingMethodException(name, this.class, args)
}
def propertyMissing(String name) { "virtual property: $name" }
def propertyMissing(String name, value) { println "setting $name = $value" }
}
def f = new DynamicFinder()
println f.findByEmail("[email protected]")
println f.anything
f.color = "red"
class Traced implements GroovyInterceptable {
def invokeMethod(String name, args) {
System.out.println "→ $name(${args.join(', ')})"
def m = metaClass.getMetaMethod(name, args)
m ? m.invoke(this, args) : "no such method"
}
def work(x) { x * 2 }
}
println new Traced().work(21)
// Method caching pattern: synthesize once, then register it on the metaClass
class Cacher {
def methodMissing(String name, args) {
def impl = { Object[] a -> "generated $name ${a.toList()}" }
Cacher.metaClass."$name" = impl
impl(*args)
}
}25.4 Categories and Extension Modules
Extension modules (the cleanest approach for libraries): write static methods whose first parameter is the extended type, then register them in META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule. They work with @CompileStatic too.
class StringUtils {
static String reverseWords(String self) { self.split(' ').reverse().join(' ') }
}
use(StringUtils) { // scoped, thread-local method injection
println "hello big world".reverseWords()
}
import groovy.time.TimeCategory
use(TimeCategory) {
println 2.days.from.now
println new Date() + 3.hours
}moduleName=my-extensions
moduleVersion=1.0
extensionClasses=com.example.StringExtensions
staticExtensionClasses=com.example.StaticStringExtensions25.5 Expando and Dynamic Objects
def obj = new Expando(name: "Dynamic")
obj.greet = { "Hello from $name" }
obj.age = 5
println obj.greet(), obj.age
// respondsTo / hasProperty introspection
println "text".respondsTo("toUpperCase"), new Person().hasProperty("name")
println "text".metaClass.methods*.name.unique().take(5)
def m = Person.metaClass.getMetaMethod("greet")25.6 Runtime Mixins (Legacy) and Trait Alternatives
@Mixin / mixin() are deprecated. Use traits (withTraits, as), categories, or extension modules.