Runtime Metaprogramming & the MOP

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Groovy 6.0 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26 (checked on 2026-10-14). Runtime metaprogramming is the ability to inspect and change how an object answers method calls and property accesses while the program runs: intercepting calls, answering calls for methods that do not exist, and adding methods to classes. All of it hangs on one idea, the Meta-Object Protocol (MOP): every Groovy call goes through a MetaClass that decides what actually happens. Here the MOP means the GroovyObject hooks and the MetaClass they delegate to; changing the metaclass itself is on Metaclasses, ExpandoMetaClass & Categories, adding methods to library classes from a jar is on Extension Modules, and the compile-time counterpart is on AST Transformations.

Runtime metaprogramming relies on dynamic dispatch. Code compiled with @CompileStatic, and in the stricter parts of @TypeChecked, resolves calls at compile time and does not see hooks such as methodMissing unless a type checking extension cooperates; see Type Checking Extensions.

The Meta-Object Protocol

A class compiled by Groovy implements GroovyObject, an interface with five methods. Every Groovy class gets them automatically; a class written in Java can extend GroovyObjectSupport to get them too.

println GroovyObject.declaredMethods*.name.sort()
class Fan { def speed = 1; def rpm() { speed * 600 } }
def f = new Fan()
println f instanceof GroovyObject
println 'abc' instanceof GroovyObject
println f.metaClass.getClass().simpleName
println GroovySystem.metaClassRegistry.getMetaClass(Fan).getClass().simpleName
[getMetaClass, getProperty, invokeMethod, setMetaClass, setProperty]
true
false
HandleMetaClass
MetaClassImpl

Plain Java classes such as String are not GroovyObject`s, yet Groovy can call them dynamically all the same: the call goes to the metaclass registered for the class in the `MetaClassRegistry (here MetaClassImpl). HandleMetaClass is a thin wrapper that obj.metaClass returns so that obj.metaClass.something = ... changes only that object; the wrapper is the topic of the metaclass page.

The hooks fall in three groups:

  • Calls: invokeMethod(String, Object) runs when a method is called and cannot be found (or for every call on a GroovyInterceptable), and methodMissing(String, Object) runs when a method cannot be found.

  • Properties: getProperty(String) and setProperty(String, Object) run for every property access, and propertyMissing(String) / propertyMissing(String, Object) when the property does not exist.

  • Attributes: getAttribute and setAttribute live on the MetaClass, not on GroovyObject; they read and write the field itself, bypassing getters and setters.

Everything the compiler does for obj.foo(), obj.foo and obj.foo = 1 can also be done by hand through the metaclass:

class Fan {
    def speed = 1
    def rpm() { speed * 600 }
}
def f = new Fan()
println f.invokeMethod('rpm', null)
println f.metaClass.invokeMethod(f, 'rpm', [] as Object[])
println f.getProperty('speed')
f.setProperty('speed', 3)
println f.rpm()
println f.metaClass.getMetaMethod('rpm')?.name
println f.metaClass.getMetaProperty('speed').type.simpleName
println f.metaClass.properties*.name.sort()
600
600
1
1800
rpm
Object
[class, speed]

speed is declared with def, so its type is Object. Note that properties includes class, which every object has.

How a method call is resolved

When Groovy evaluates obj.name(args) it follows the steps in the flowchart below. The first step that applies decides the result.

flowchart TD call["obj.name(args)"] --> q1{"obj implements GroovyInterceptable?"} q1 -- yes --> intercept["obj.invokeMethod(name, args) -- handles every call, including toString and getClass"] q1 -- no --> q2{"metaclass finds a method? -- (class methods, ExpandoMetaClass additions, categories in scope)"} q2 -- yes --> run["invoke that method"] q2 -- no --> q3{"property name holds a Closure?"} q3 -- yes --> closure["call the closure"] q3 -- no --> q4{"methodMissing defined?"} q4 -- yes --> mm["methodMissing(name, args)"] q4 -- no --> q5{"invokeMethod overridden in the class?"} q5 -- yes --> im["invokeMethod(name, args)"] q5 -- no --> err["MissingMethodException"]

The next sections show each branch. This order was checked on Groovy 6.0.0 with a class that has a real method, a closure-valued property, methodMissing and invokeMethod all at once:

class Order {
    def hook = { x -> "closure property ${x}" }
    def real() { 'real' }
    def methodMissing(String n, Object a) { "methodMissing:${n}" }
    def invokeMethod(String n, Object a) { "invokeMethod:${n}" }
}
def o = new Order()
println o.real()
println o.hook(1)
println o.ghost()
class OnlyInvoke { def invokeMethod(String n, Object a) { "invokeMethod:${n}" } }
println new OnlyInvoke().ghost()
class Neither {}
try { new Neither().ghost() } catch (MissingMethodException e) { println e.class.simpleName }
real
closure property 1
methodMissing:ghost
invokeMethod:ghost
MissingMethodException

Prefer methodMissing for "answer calls that do not exist": it cannot recurse into itself the way invokeMethod can, and it composes with metaclass-added methods. Reach for invokeMethod mainly through GroovyInterceptable, when every call must pass through one place.

invokeMethod

If a class defines invokeMethod(String, Object) it is called whenever the requested method does not exist on the class (and there is no methodMissing). Methods that do exist run normally and invokeMethod is not involved.

class Zone {
    String name
    def invokeMethod(String name, Object args) {
        "invokeMethod(${name}, ${args as List})"
    }
}
def z = new Zone(name: 'north')
println z.water(5)
println z.toString().startsWith('Zone@')
println z.name
invokeMethod(water, [5])
true
north

Here water does not exist, so invokeMethod answered; toString() exists (inherited from Object), so it ran normally.

GroovyInterceptable

A class that implements the marker interface GroovyInterceptable has every method call routed to its invokeMethod, whether the method exists or not, so it is the tool for cross-cutting behaviour such as tracing. Inside invokeMethod call the real method through the metaclass, not through this, otherwise the call is intercepted again.

class Probe implements GroovyInterceptable {
    def reading() { 21 }
    def invokeMethod(String name, Object args) {
        def m = metaClass.getMetaMethod(name, args)
        System.out.println "before ${name}"
        def r = m ? m.invoke(this, args) : "(no ${name})"
        System.out.println "after ${name}"
        r
    }
}
def p = new Probe()
println p.reading()
println p.nothing()
before reading
after reading
21
before nothing
after nothing
(no nothing)

The calls to System.out.println are deliberate. A plain println inside the class is a method call on this, and this is interceptable, so the interceptor calls itself until the stack overflows:

class Naive implements GroovyInterceptable {
    def work() { 1 }
    def invokeMethod(String name, Object args) {
        println "calling ${name}"      // println is a call on this: intercepted again
        1
    }
}
try { new Naive().work() } catch (StackOverflowError e) { println 'StackOverflowError' }
StackOverflowError

Interception covers calls made from outside and from inside the object, toString() and getClass() included:

class Mix implements GroovyInterceptable {
    def foo() { 'foo' }
    def invokeMethod(String n, Object a) { "intercepted:${n}" }
}
println new Mix().foo()
println new Mix().toString()
println new Mix().getClass()
intercepted:foo
intercepted:toString
intercepted:getClass

methodMissing

methodMissing(String name, Object args) is the more targeted hook: it runs only when no method (and no closure property) of that name exists. A common pattern is to synthesise the method the first time it is requested and register it on the metaclass, so later calls find a real method and skip methodMissing altogether.

class Sensor {
    def methodMissing(String name, args) {
        System.out.println "synthesising ${name}"
        Sensor.metaClass."${name}" = { Object... a -> "value of ${name}" }
        "value of ${name}"
    }
}
def s = new Sensor()
println s.readTemp()
println s.readTemp()
println new Sensor().readTemp()
println new Sensor().readTemp()
synthesising readTemp
value of readTemp
synthesising readTemp
value of readTemp
value of readTemp
value of readTemp

The registered method is found by every instance created afterwards (the last two lines print without "synthesising"). The instance s that triggered it had already cached its previous metaclass, so on Groovy 6.0.0 it kept calling methodMissing. When it matters, register the method before creating instances, or compare with the per-instance approach on the metaclass page.

To refuse names that do not follow the convention, throw MissingMethodException:

class Strict {
    def methodMissing(String name, args) {
        if (name.startsWith('read')) return "reading ${name - 'read'}"
        throw new MissingMethodException(name, Strict, args)
    }
}
def s = new Strict()
println s.readHumidity()
try { s.explode() } catch (MissingMethodException e) { println e.message.readLines()[0] }
reading Humidity
No signature of method: explode for class: Strict is applicable for argument types: () values: []

Properties: getProperty and setProperty

getProperty(String) and setProperty(String, Object) run for every property access on the object, existing or not, unless the property is read as an attribute with .@. Delegate to the metaclass for the default behaviour.

class Bean {
    String name = 'fern'
    def getProperty(String p) {
        if (p == 'shout') return name.toUpperCase()
        metaClass.getProperty(this, p)
    }
    void setProperty(String p, Object v) {
        if (p == 'name') v = v.trim()
        metaClass.setProperty(this, p, v)
    }
}
def b = new Bean()
println b.shout
b.name = '  moss  '
println "[${b.name}]"
println b.@name
FERN
[moss]
moss

A side effect to remember: obj.metaClass is itself a property read, so it goes through your getProperty. In a class that answers every name, u.metaClass is no longer the metaclass:

class Unknown {
    def getProperty(String name) { "prop:${name}" }
}
def u = new Unknown()
println u.anything
println u.metaClass
println u.getMetaClass().getClass().simpleName
prop:anything
prop:metaClass
HandleMetaClass

Inside the class’s own code metaClass refers to the field directly, which is why metaClass.getProperty(this, p) above works. Outside, call getMetaClass().

Attributes: getAttribute and setAttribute

An attribute is the raw field. obj.@name reads it directly without calling getName(). The same access is available as a MOP operation through the metaclass:

class Gauge {
    private String unit = 'C'
    String getUnit() { 'unit is ' + unit }
}
def g = new Gauge()
println g.unit
println g.@unit
println g.metaClass.getAttribute(g, 'unit')
g.metaClass.setAttribute(g, 'unit', 'F')
println g.@unit
unit is C
C
C
F

getAttribute and setAttribute are not part of the GroovyObject interface (the list earlier has no such method); they are MetaClass operations, so a custom metaclass can override them.

propertyMissing

propertyMissing(String name) answers reads and propertyMissing(String name, Object value) answers writes of properties that do not exist. Like methodMissing, it is only called when normal lookup fails, so it is cheaper than overriding getProperty.

class Readings {
    def values = [:]
    def propertyMissing(String name) { values[name] }
    def propertyMissing(String name, value) { values[name] = value }
}
def r = new Readings()
r.temp = 22
r.humidity = 61
println r.temp + r.humidity
println r.values
println r.nothing
83
[temp:22, humidity:61]
null

Static variants

methodMissing and propertyMissing also exist for calls on the class itself. They are static methods named with the prefix $static_: $static_methodMissing(String, Object) and $static_propertyMissing(String).

class Vent {
    static def $static_methodMissing(String name, Object args) { "static ${name}${args.toList()}" }
    static def $static_propertyMissing(String name) {
        if (name == 'limit') return 80
        throw new MissingPropertyException(name, Vent)
    }
}
println Vent.dispatch(3)
println Vent.limit
static dispatch[3]
80

$static_propertyMissing throws for names it does not know on purpose. When a class answers every static property name, a call such as Vent.dispatch(3) is read first as the property dispatch and then called, and fails with "No signature of method: call". Keep the property hook selective.

respondsTo and hasProperty

Because methods and properties can appear at run time, Groovy lets you ask the metaclass what a receiver supports. Both are defined on MetaClass and exposed as GDK methods on every object. respondsTo(name) returns the list of matching MetaMethod`s (empty if none), `respondsTo(name, argTypes) narrows by argument types, and hasProperty(name) returns the MetaProperty, or null.

class Vent {
    String open() { 'open' }
    String open(int percent) { "open ${percent}%" }
}
def v = new Vent()
println v.respondsTo('open').size()
println v.respondsTo('open', [] as Object[]).size()
println v.respondsTo('open', 50).size()
println v.respondsTo('open', 'x').size()
println v.respondsTo('close').isEmpty()
println v.hasProperty('class')?.name
println v.hasProperty('nothing')
2
1
1
0
true
class
null

Use these before calling into an object whose shape you do not control: if (v.respondsTo('open', 50)) v.open(50). A method that a class would answer only through methodMissing is not reported by respondsTo, which looks at what is registered, not at what the hook would accept; that is why Groovy code that synthesises methods usually registers them on the metaclass.

Where the book is outdated

Older Groovy books describe invokeMethod as the way to build dynamic objects and give less weight to methodMissing and propertyMissing, which arrived in Groovy 1.5 and are the preferred hooks today. The GroovyObject interface, the MetaClass operations and the order of resolution shown here are the same on Groovy 6.0 as in the 2.x series; what changed around them is the runtime underneath (Groovy 6.0 needs JDK 17 or later) and the growing use of @CompileStatic, which bypasses this machinery.