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)     // reset

25.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.StaticStringExtensions

25.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.