Groovy MOP (Meta-Object Protocol) – How It Works with 10+ Examples

Groovy MOP (Meta-Object Protocol) explained with 10+ tested examples. Learn method resolution, MetaClass hierarchy, and how Groovy dispatches every call.

“The MOP is the beating heart of Groovy’s dynamism. Every method call, every property access, every operator – they all flow through the Meta-Object Protocol.”

Dierk König, Groovy in Action

Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Intermediate to Advanced | Reading Time: 18 minutes

Unlike Java, where method calls are resolved at compile time and baked into bytecode, the groovy MOP (Meta-Object Protocol) routes every single method call through a resolution chain at runtime. This machinery decides which method actually runs when you write obj.someMethod(), and understanding it is the key to mastering metaprogramming.

Understanding the Groovy MOP is the key to mastering metaprogramming. If our Groovy Metaprogramming guide showed you what you can do, this post explains how it all works under the hood. You’ll learn the exact order Groovy follows to resolve method calls, how MetaClass objects control dispatch, and how to customize this process for your own classes.

The official Groovy metaprogramming documentation covers the MOP at a high level. In this tutorial, we’ll go deeper with 10+ tested examples that let you see each piece of the protocol in action. We’ll also connect the dots to ExpandoMetaClass, which builds directly on top of the MOP.

What Is the Meta-Object Protocol?

The Meta-Object Protocol is a set of rules and interfaces that govern how Groovy dispatches method calls and property accesses. Think of it as a switchboard sitting between your code and the actual method implementations. Every call goes through this switchboard, and you can rewire it.

In Java, obj.foo() always calls the foo() method defined on the object’s class (or a superclass). In Groovy, obj.foo() first goes to the object’s MetaClass, which checks multiple sources before deciding what to invoke. This indirection is what makes Groovy’s dynamic features possible.

The MOP consists of:

  • MetaClass – every object has one; it controls method and property dispatch
  • MetaClassRegistry – a global registry that maps classes to their MetaClass instances
  • MetaMethod – represents a method that the MOP knows about (class methods, GDK methods, metaClass-added methods)
  • MetaProperty – represents a property known to the MOP
  • GroovyObject – an interface that all Groovy classes implement, providing hooks into the MOP
  • GroovyInterceptable – a marker interface that routes all calls through invokeMethod()

According to the official Groovy runtime metaprogramming docs, every Groovy class automatically implements GroovyObject, which provides three key methods: invokeMethod(), getProperty(), and setProperty(). These are the MOP’s entry points.

The following diagram shows how Groovy dispatches method calls on plain Java objects (those that do not implement GroovyObject):

Found

Not found

Found

Not found

Found

Not found

Found

Not found

javaObj.someMethod()

ScriptBytecodeAdapter
wraps the call

MetaClassRegistry
looks up MetaClass

MetaClass searches
(same as GroovyObject):

2a. metaClass-added methods

Method Executes

2b. Class methods

2c. Superclass methods

2d. GDK methods

MissingMethodException
(no methodMissing for Java objects)

Groovy MOP – Java Object Method Dispatch

Quick Reference Table

MOP ComponentRoleKey Methods
GroovyObjectBase interface for all Groovy classesinvokeMethod(), getProperty(), setProperty(), getMetaClass()
GroovyInterceptableMarker interface: route all calls through invokeMethodInherits from GroovyObject
MetaClassControls method/property dispatch for a classinvokeMethod(), getProperty(), getMethods(), getMetaMethods()
ExpandoMetaClassMetaClass that allows adding methods at runtimeregisterInstanceMethod(), registerStaticMethod()
MetaClassRegistryGlobal map of Class to MetaClassgetMetaClass(), setMetaClass(), removeMetaClass()
MetaMethodRepresents a single method known to MOPinvoke(), getName(), getParameterTypes()
MetaPropertyRepresents a single property known to MOPgetProperty(), getName(), getType()

10 Practical Examples

Example 1: Inspecting the MetaClass

What we’re doing: Examining the MetaClass of an object to see what methods and properties the MOP knows about.

Example 1: Inspecting MetaClass

class Person {
    String name
    int age
    String greet() { "Hi, I'm ${name}" }
}

def p = new Person(name: 'Alice', age: 30)

// Every object has a metaClass
println "MetaClass type: ${p.metaClass.class.simpleName}"

// List all methods the MOP knows about
def methods = p.metaClass.methods*.name.unique().sort()
println "Methods (${methods.size()}): ${methods.take(10)}..."

// List meta-methods (GDK methods added by Groovy)
def metaMethods = p.metaClass.metaMethods*.name.unique().sort()
println "GDK MetaMethods (${metaMethods.size()}): ${metaMethods.take(10)}..."

// List properties
def props = p.metaClass.properties*.name.sort()
println "Properties: ${props}"

// Check if a method exists
println "Has greet()? ${p.metaClass.respondsTo(p, 'greet').size() > 0}"
println "Has fly()? ${p.metaClass.respondsTo(p, 'fly').size() > 0}"

Output

MetaClass type: HandleMetaClass
Methods (10): [equals, getAge, getClass, getMetaClass, getName, greet, hashCode, notify, notifyAll, setAge]...
GDK MetaMethods (10): [addShutdownHook, any, asBoolean, asType, collect, dump, each, eachWithIndex, every, find]...
Properties: [age, class, name]
Has greet()? true
Has fly()? false

What happened here: The MetaClass is an object that holds metadata about a class – its methods, properties, constructors, and any dynamically added members. The methods list includes the class’s own methods plus standard Java Object methods. The metaMethods list includes GDK (Groovy Development Kit) methods that Groovy adds to every object – things like dump(), each(), and collect(). The respondsTo() method checks whether a method exists without actually calling it.

Example 2: The GroovyObject Interface

What we’re doing: Demonstrating that every Groovy class implements GroovyObject and seeing its core methods in action.

Example 2: GroovyObject Interface

class MyClass {
    String message = 'default'

    String speak() { "Speaking: ${message}" }
}

def obj = new MyClass()

// Every Groovy class implements GroovyObject
println "Is GroovyObject? ${obj instanceof GroovyObject}"

// GroovyObject provides these methods:
// 1. invokeMethod(String name, Object args)
println obj.invokeMethod('speak', null)

// 2. getProperty(String name)
println obj.getProperty('message')

// 3. setProperty(String name, Object value)
obj.setProperty('message', 'Hello MOP!')
println obj.getProperty('message')

// 4. getMetaClass() / setMetaClass()
println "MetaClass: ${obj.getMetaClass().class.simpleName}"

// Normal Groovy code uses these under the hood:
// obj.speak()    → obj.invokeMethod('speak', null)
// obj.message    → obj.getProperty('message')
// obj.message=x  → obj.setProperty('message', x)
println "\nBoth routes produce same result:"
println "Direct: ${obj.speak()}"
println "MOP:    ${obj.invokeMethod('speak', null)}"

Output

Is GroovyObject? true
Speaking: default
default
Hello MOP!
MetaClass: HandleMetaClass

Both routes produce same result:
Direct: Speaking: Hello MOP!
MOP:    Speaking: Hello MOP!

What happened here: GroovyObject is the foundation of the MOP. When you compile a Groovy class, the compiler automatically makes it implement GroovyObject, which adds invokeMethod(), getProperty(), setProperty(), and getMetaClass(). Every normal Groovy method call and property access goes through these methods behind the scenes. When you write obj.speak(), Groovy actually routes it through the MOP machinery that starts with these GroovyObject methods.

Example 3: Method Resolution Order – Step by Step

What we’re doing: Tracing the exact order Groovy follows when resolving a method call for a regular Groovy object.

Example 3: Method Resolution Order

class ResolutionTracer {
    // Step 1: Class's own method
    def realMethod() { "Step 1: Found on the class itself" }

    // Step 3: methodMissing (called when method not found)
    def methodMissing(String name, def args) {
        "Step 3: methodMissing caught '${name}'"
    }
}

def obj = new ResolutionTracer()

// Call an existing method → Step 1
println obj.realMethod()

// Add a method via metaClass → Step 2
obj.metaClass.dynamicMethod = { -> "Step 2: Found on metaClass" }
println obj.dynamicMethod()

// Call a nonexistent method → Step 3
println obj.unknownMethod()

// The full resolution order for a regular Groovy object:
println "\n--- Resolution Order ---"
println "1. MetaClass checks for the method (includes class methods + added methods)"
println "2. If not found: methodMissing() on the object"
println "3. If no methodMissing: throw MissingMethodException"

Output

Step 1: Found on the class itself
Step 2: Found on metaClass
Step 3: methodMissing caught 'unknownMethod'

--- Resolution Order ---
1. MetaClass checks for the method (includes class methods + added methods)
2. If not found: methodMissing() on the object
3. If no methodMissing: throw MissingMethodException

What happened here: For a regular Groovy object (not implementing GroovyInterceptable), the MOP follows this order: First, it asks the MetaClass to find the method. The MetaClass checks the class’s own methods, inherited methods, GDK methods, and any dynamically added methods. If none match, the MOP calls methodMissing() on the object, if defined. If that’s not defined either, it throws a MissingMethodException. Understanding this order is essential for effective groovy meta object protocol usage.

Example 4: GroovyInterceptable Changes the Resolution Order

What we’re doing: Showing how implementing GroovyInterceptable completely changes the method resolution path.

Example 4: GroovyInterceptable Resolution

class NormalClass {
    def hello() { "NormalClass.hello()" }
    def methodMissing(String name, def args) { "NormalClass.methodMissing(${name})" }
}

class InterceptableClass implements GroovyInterceptable {
    def hello() { "InterceptableClass.hello()" }

    def invokeMethod(String name, def args) {
        def method = this.metaClass.getMetaMethod(name, args)
        if (method) {
            return "INTERCEPTED → ${method.invoke(this, args)}"
        }
        return "INTERCEPTED → no method '${name}' found"
    }
}

println "--- Normal Class ---"
def normal = new NormalClass()
println normal.hello()          // Direct to class method
println normal.goodbye()        // Goes to methodMissing

println "\n--- Interceptable Class ---"
def interceptable = new InterceptableClass()
println interceptable.hello()     // Goes through invokeMethod FIRST
println interceptable.goodbye()   // Also goes through invokeMethod

println "\n--- Key Difference ---"
println "Normal: existing methods bypass methodMissing"
println "Interceptable: ALL calls go through invokeMethod"

Output

--- Normal Class ---
NormalClass.hello()
NormalClass.methodMissing(goodbye)

--- Interceptable Class ---
INTERCEPTED → InterceptableClass.hello()
INTERCEPTED → no method 'goodbye' found

--- Key Difference ---
Normal: existing methods bypass methodMissing
Interceptable: ALL calls go through invokeMethod

What happened here: This is one of the most important concepts in the Groovy MOP. For a normal class, existing methods are called directly – methodMissing only fires for methods that don’t exist. But when a class implements GroovyInterceptable, every single method call goes through invokeMethod(), even calls to methods that genuinely exist. This is what enables AOP (Aspect-Oriented Programming) patterns like logging, security checks, and transaction management.

Example 5: MetaClassRegistry – The Global MetaClass Store

What we’re doing: Exploring the MetaClassRegistry, which is the global store that maps each class to its MetaClass.

Example 5: MetaClassRegistry

// The registry is accessible via GroovySystem
def registry = GroovySystem.metaClassRegistry
println "Registry type: ${registry.class.simpleName}"

// Get the MetaClass for String
def stringMC = registry.getMetaClass(String)
println "String MetaClass: ${stringMC.class.simpleName}"

// Add a method to String
String.metaClass.excited = { -> delegate.toUpperCase() + '!!!' }
println 'hello'.excited()

// Check the MetaClass changed
def newMC = registry.getMetaClass(String)
println "After adding method: ${newMC.class.simpleName}"

// Remove the custom MetaClass - resets to default
registry.removeMetaClass(String)

try {
    'hello'.excited()
} catch (MissingMethodException e) {
    println "After reset: excited() is gone"
}

// The default MetaClass is restored
def defaultMC = registry.getMetaClass(String)
println "Reset MetaClass: ${defaultMC.class.simpleName}"

// You can also iterate registered MetaClasses
println "\nSample registered classes:"
def iterator = registry.iterator()
def count = 0
while (iterator.hasNext() && count < 5) {
    def mc = iterator.next()
    println "  ${mc.theClass.simpleName} → ${mc.class.simpleName}"
    count++
}

Output

Registry type: MetaClassRegistryImpl
String MetaClass: MetaClassImpl
HELLO!!!
After adding method: ExpandoMetaClass
After reset: excited() is gone
Reset MetaClass: MetaClassImpl

Sample registered classes:
  String → ExpandoMetaClass

What happened here: The MetaClassRegistry is a global singleton that maintains the mapping between classes and their MetaClass instances. When you modify String.metaClass, the registry stores a new MetaClass for the String class. Calling removeMetaClass() drops the custom MetaClass and restores the default one. This is essential for cleaning up after tests that modify metaClasses, which we covered in our Groovy Metaprogramming guide.

Example 6: MetaMethod – Understanding Method Representations

What we’re doing: Examining MetaMethod objects, which represent individual methods in the MOP system.

Example 6: MetaMethod Objects

class Calculator {
    int add(int a, int b) { a + b }
    double add(double a, double b) { a + b }
    int multiply(int a, int b) { a * b }
}

def calc = new Calculator()

// Find all 'add' methods
def addMethods = calc.metaClass.respondsTo(calc, 'add')
println "Overloaded 'add' methods: ${addMethods.size()}"
addMethods.each { method ->
    def params = method.nativeParameterTypes*.simpleName.join(', ')
    println "  ${method.returnType.simpleName} add(${params})"
}

// Find a specific method by parameter types
def intAdd = calc.metaClass.getMetaMethod('add', [1, 2] as Object[])
println "\nFound int add: ${intAdd}"
println "Invoke it: ${intAdd.invoke(calc, [3, 4] as Object[])}"

// List all declared methods (excluding Object methods)
def declaredMethods = calc.metaClass.methods.findAll {
    it.declaringClass.theClass == Calculator
}
println "\nDeclared methods:"
declaredMethods.each { m ->
    def params = m.nativeParameterTypes*.simpleName.join(', ')
    println "  ${m.returnType.simpleName} ${m.name}(${params})"
}

// Check if a method is a GDK (Groovy-added) method
def dumpMethod = calc.metaClass.metaMethods.find { it.name == 'dump' }
println "\ndump() is GDK method: ${dumpMethod != null}"
println "dump() declaring class: ${dumpMethod?.declaringClass?.theClass?.simpleName}"

Output

Overloaded 'add' methods: 2
  double add(double, double)
  int add(int, int)

Found int add: public int Calculator.add(int,int)
Invoke it: 7

Declared methods:
  Lookup $getLookup()
  double add(double, double)
  int add(int, int)
  MetaClass getMetaClass()
  int multiply(int, int)
  void setMetaClass(MetaClass)

dump() is GDK method: true
dump() declaring class: Object

What happened here: MetaMethod objects represent methods in the MOP. The respondsTo() method returns all MetaMethods matching a given name, which is how you handle overloaded methods. getMetaMethod() finds a specific overload by parameter types. Note the distinction between methods (class’s own methods) and metaMethods (GDK-added methods like dump() that come from DefaultGroovyMethods).

Example 7: Property Resolution Through the MOP

What we’re doing: Tracing how the MOP resolves property access, including the difference between fields, properties, and dynamically added properties.

Example 7: Property Resolution

class PropertyDemo {
    String name = 'Alice'       // Groovy property (auto getter/setter)
    public String publicField = 'public'  // Plain field (no getter/setter)
    private String secret = 'hidden'

    // Custom getter overrides default
    String getTitle() { "Dr. ${name}" }

    def propertyMissing(String prop) {
        "dynamic:${prop}"
    }
}

def obj = new PropertyDemo()

// Property access resolution order:
// 1. MetaClass property (metaClass-added properties)
// 2. Groovy property (with getter/setter)
// 3. Map-like access (if the class supports it)
// 4. propertyMissing()

println "--- Property Access ---"
println "name: ${obj.name}"           // Step 2: getter
println "title: ${obj.title}"         // Step 2: custom getter
println "unknown: ${obj.unknown}"     // Step 4: propertyMissing

// Add a metaClass property (takes priority - Step 1)
obj.metaClass.priority = 'meta-property'
println "priority: ${obj.priority}"

// Inspect properties via MOP
println "\n--- MetaProperties ---"
obj.metaClass.properties.each { prop ->
    if (prop.name in ['name', 'publicField', 'class']) {
        println "  ${prop.name}: type=${prop.type.simpleName}"
    }
}

// Direct field access bypasses the MOP
println "\n--- Direct Field Access ---"
println "Direct field: ${obj.@publicField}"
println "Via MOP: ${obj.publicField}"

Output

--- Property Access ---
name: Alice
title: Dr. Alice
unknown: dynamic:unknown

--- MetaProperties ---
  name: type=String
  publicField: type=String
  class: type=Class

--- Direct Field Access ---
Direct field: public
Via MOP: public

What happened here: Property resolution in the MOP works similarly to method resolution. The MOP first checks for metaClass-added properties, then Groovy properties (which have auto-generated getters/setters), then falls back to propertyMissing(). The .@ operator bypasses the MOP entirely and accesses the field directly – useful when you need to skip getter logic. Custom getters like getTitle() create “virtual properties” that appear in property access without a backing field.

Example 8: Custom MetaClass – Taking Full Control

What we’re doing: Replacing a class’s MetaClass with a custom DelegatingMetaClass that adds behavior around every method call.

Example 8: Custom DelegatingMetaClass

class LoggingMetaClass extends DelegatingMetaClass {
    private List log = []

    LoggingMetaClass(MetaClass delegate) {
        super(delegate)
    }

    Object invokeMethod(Object object, String methodName, Object[] arguments) {
        log << "invoke: ${methodName}(${arguments?.join(', ') ?: ''})"
        super.invokeMethod(object, methodName, arguments)
    }

    Object getProperty(Object object, String property) {
        log << "get: ${property}"
        super.getProperty(object, property)
    }

    void setProperty(Object object, String property, Object newValue) {
        log << "set: ${property} = ${newValue}"
        super.setProperty(object, property, newValue)
    }

    List getLog() { log }
}

class Account {
    String owner
    double balance = 0.0

    void deposit(double amount) { balance += amount }
    void withdraw(double amount) { balance -= amount }
}

// Install custom MetaClass
def originalMC = Account.metaClass
def loggingMC = new LoggingMetaClass(originalMC)
loggingMC.initialize()
Account.metaClass = loggingMC

def account = new Account()
account.owner = 'Alice'
account.deposit(100.0)
account.deposit(50.0)
account.withdraw(30.0)
println "Balance: ${account.balance}"

println "\nMOP Log:"
loggingMC.log.each { println "  ${it}" }

// Clean up
GroovySystem.metaClassRegistry.removeMetaClass(Account)

Output

Balance: 120.0

MOP Log:
  set: owner = Alice
  invoke: deposit(100.0)
  invoke: deposit(50.0)
  invoke: withdraw(30.0)
  get: balance

What happened here: DelegatingMetaClass is a MetaClass wrapper that forwards all calls to an inner MetaClass. By extending it, we intercepted every method invocation, property get, and property set at the MOP level – without changing the Account class at all. This is lower-level than GroovyInterceptable because it works at the MetaClass level rather than the class level. It affects all instances of the class and doesn’t require the class to implement any interface.

Example 9: MOP with Java Classes

What we’re doing: Demonstrating how the MOP handles Java classes, which don’t implement GroovyObject but still get MOP treatment.

Example 9: MOP with Java Classes

// Java classes don't implement GroovyObject, but the MOP still works
def javaString = "Hello World"
println "String is GroovyObject? ${javaString instanceof GroovyObject}"

// Yet we can still use MOP features on Java classes
println "MetaClass: ${javaString.metaClass.class.simpleName}"

// GDK methods are added via the MOP to Java classes
println "reverse: ${javaString.reverse()}"        // GDK method
println "take(5): ${javaString.take(5)}"           // GDK method
println "padLeft: ${'42'.padLeft(5, '0')}"         // GDK method

// We can add methods to Java classes via MetaClass
ArrayList.metaClass.sum = { -> delegate.inject(0) { acc, val -> acc + val } }
def nums = [1, 2, 3, 4, 5] as ArrayList
println "Sum: ${nums.sum()}"

// How does the MOP handle Java classes?
// Answer: Groovy wraps Java objects in a special invoker
// that routes calls through the MetaClass before reaching Java methods

// Java class MetaMethod inspection
def stringMethods = javaString.metaClass.metaMethods
    .findAll { it.name.startsWith('to') }
    *.name
    .unique()
    .sort()
println "\nGDK 'to*' methods on String: ${stringMethods}"

// Cleanup
GroovySystem.metaClassRegistry.removeMetaClass(ArrayList)

Output

String is GroovyObject? false
MetaClass: HandleMetaClass
reverse: dlroW olleH
take(5): Hello
padLeft: 00042
Sum: 15

GDK 'to*' methods on String: [toBigDecimal, toBigInteger, toBoolean, toCharacter, toDouble, toFloat, toInteger, toList, toLong, toSet, toShort, toString, toURI, toURL, tokenize]

What happened here: Even though java.lang.String doesn’t implement GroovyObject, the MOP still works with it. Groovy’s call site caching mechanism intercepts method calls on Java objects and routes them through the MetaClass system. This is how GDK methods like reverse(), take(), and toInteger() appear on Java classes – they’re added to the MetaClass, not to the class itself. You can also add your own methods the same way.

Example 10: MOP Method Dispatch Flow Visualizer

What we’re doing: Building a tracer that visualizes the full MOP dispatch path for any method call, showing exactly which resolution step handles it.

Example 10: MOP Dispatch Visualizer

class MopTracer {
    // Real method
    def realMethod() { "result from realMethod" }

    // methodMissing handler
    def methodMissing(String name, def args) {
        "result from methodMissing(${name})"
    }

    static void trace(obj, String methodName) {
        println "\n--- Tracing: ${methodName}() ---"

        // Check 1: MetaClass has the method?
        def metaMethod = obj.metaClass.getMetaMethod(methodName, [] as Object[])
        if (metaMethod) {
            println "  [1] MetaClass found: ${metaMethod.declaringClass.theClass.simpleName}.${methodName}()"
            println "      Method type: ${metaMethod.class.simpleName}"
            println "      Result: ${metaMethod.invoke(obj, [] as Object[])}"
            return
        }
        println "  [1] MetaClass: NOT FOUND"

        // Check 2: Is there a GDK metaMethod?
        def gdkMethod = obj.metaClass.metaMethods.find { it.name == methodName }
        if (gdkMethod) {
            println "  [2] GDK method found: ${gdkMethod.declaringClass.theClass.simpleName}.${methodName}()"
            return
        }
        println "  [2] GDK methods: NOT FOUND"

        // Check 3: Does the object have methodMissing?
        def hasMissing = obj.metaClass.respondsTo(obj, 'methodMissing')
        if (hasMissing) {
            println "  [3] methodMissing() will handle it"
            def result = obj."${methodName}"()
            println "      Result: ${result}"
            return
        }
        println "  [3] methodMissing: NOT DEFINED"
        println "  [4] MissingMethodException will be thrown"
    }
}

def obj = new MopTracer()

// Trace different method calls
MopTracer.trace(obj, 'realMethod')
MopTracer.trace(obj, 'getClass')
MopTracer.trace(obj, 'fantasticMethod')

// Add a metaClass method and trace it
obj.metaClass.addedMethod = { -> "I was added!" }
MopTracer.trace(obj, 'addedMethod')

Output

--- Tracing: realMethod() ---
  [1] MetaClass found: MopTracer.realMethod()
      Method type: CachedMethod
      Result: result from realMethod

--- Tracing: getClass() ---
  [1] MetaClass found: Object.getClass()
      Method type: CachedMethod
      Result: class MopTracer

--- Tracing: fantasticMethod() ---
  [1] MetaClass: NOT FOUND
  [2] GDK methods: NOT FOUND
  [3] methodMissing() will handle it
      Result: result from methodMissing(fantasticMethod)

--- Tracing: addedMethod() ---
  [1] MetaClass found: MopTracer.addedMethod()
      Method type: ClosureMetaMethod
      Result: I was added!

What happened here: This example visualizes the MOP dispatch chain step by step. Notice the different MetaMethod types: CachedMethod for compiled class methods, and ClosureMetaMethod for dynamically added methods. GDK methods live in the metaMethods collection (from DefaultGroovyMethods). And methodMissing is truly the last resort before an exception. This is the groovy meta object protocol in action.

Example 11 (Bonus): respondsTo and hasProperty – MOP Queries

What we’re doing: Using MOP query methods to check for method and property existence before calling them, avoiding exceptions.

Example 11: MOP Queries

class Service {
    String name = 'MyService'
    def start() { "${name} started" }
    def stop() { "${name} stopped" }
    def status(String detail) { "${name}: ${detail}" }
}

def svc = new Service()

// respondsTo - check if a method exists
println "Has start()? ${svc.metaClass.respondsTo(svc, 'start') as boolean}"
println "Has restart()? ${svc.metaClass.respondsTo(svc, 'restart') as boolean}"
println "Has status(String)? ${svc.metaClass.respondsTo(svc, 'status', String) as boolean}"
println "Has status(int)? ${svc.metaClass.respondsTo(svc, 'status', Integer) as boolean}"

// hasProperty - check if a property exists
println "\nHas 'name'? ${svc.metaClass.hasProperty(svc, 'name') != null}"
println "Has 'version'? ${svc.metaClass.hasProperty(svc, 'version') != null}"

// Practical: safe method invocation
def safeCaller = { obj, String method, Object... args ->
    if (obj.metaClass.respondsTo(obj, method, args)) {
        return obj."${method}"(*args)
    }
    return "Method '${method}' not available"
}

println "\n--- Safe Calls ---"
println safeCaller(svc, 'start')
println safeCaller(svc, 'status', 'running')
println safeCaller(svc, 'restart')

// getAttribute vs getProperty - bypasses getters
println "\n--- getAttribute (bypasses getter) ---"
svc.metaClass.getAttribute(svc, 'name').with { println "Attribute: ${it}" }
svc.metaClass.getProperty(svc, 'name').with { println "Property: ${it}" }

Output

Has start()? true
Has restart()? false
Has status(String)? true
Has status(int)? false

Has 'name'? true
Has 'version'? false

--- Safe Calls ---
MyService started
MyService: running
Method 'restart' not available

--- getAttribute (bypasses getter) ---
Attribute: MyService
Property: MyService

What happened here: The MOP provides query methods that let you check capabilities before using them. respondsTo() returns a list of matching MetaMethod objects (empty if none match), and it even handles overloaded methods by checking parameter types. hasProperty() returns a MetaProperty object or null. These are especially useful in frameworks that need to work with unknown object types, like template engines and serialization libraries.

The MetaClass Hierarchy

The MOP uses several MetaClass implementations, each with different capabilities. Understanding the hierarchy helps you choose the right tool.

MetaClass Hierarchy

// MetaClass hierarchy:
// MetaClass (interface)
//   ├── MetaClassImpl (default for all classes)
//   ├── ExpandoMetaClass (supports adding methods at runtime)
//   ├── DelegatingMetaClass (wraps another MetaClass)
//   ├── ProxyMetaClass (deprecated, use DelegatingMetaClass)
//   └── HandleMetaClass (wrapper returned by .metaClass property)

class Simple { }

def s = new Simple()

// Default MetaClass
println "Default: ${InvokerHelper.getMetaClass(s).class.simpleName}"

// After adding a method, it becomes ExpandoMetaClass
Simple.metaClass.hello = { -> "hello" }
println "After metaClass mod: ${InvokerHelper.getMetaClass(s).class.simpleName}"

// HandleMetaClass is the wrapper you see via .metaClass
println "Via .metaClass: ${s.metaClass.class.simpleName}"

// Clean up
GroovySystem.metaClassRegistry.removeMetaClass(Simple)
println "After cleanup: ${InvokerHelper.getMetaClass(new Simple()).class.simpleName}"

Output

Default: MetaClassImpl
After metaClass mod: ExpandoMetaClass
Via .metaClass: HandleMetaClass
After cleanup: MetaClassImpl

When you first access a class, its MetaClass is a MetaClassImpl – a read-only, efficient implementation. The moment you add a method via .metaClass, Groovy promotes it to an ExpandoMetaClass, which supports dynamic method registration. The HandleMetaClass you see through the .metaClass property is a thin wrapper that provides the DSL-like syntax for adding methods. For more details into ExpandoMetaClass, see our dedicated guide on ExpandoMetaClass.

Method Resolution Order in Detail

Here’s the complete method resolution algorithm the Groovy MOP follows, depending on whether the object implements GroovyInterceptable:

Method Resolution Algorithm

FOR A REGULAR GROOVY OBJECT (no GroovyInterceptable):
─────────────────────────────────────────────────────
1. MetaClass.invokeMethod() is called
2. MetaClass searches for the method in this order:
   a. Methods added to the metaClass (ExpandoMetaClass methods)
   b. Methods declared on the class itself
   c. Methods inherited from superclasses
   d. GDK methods (from DefaultGroovyMethods)
3. If found → invoke the method
4. If NOT found → call object.methodMissing(name, args)
5. If methodMissing not defined → throw MissingMethodException

FOR A GROOVYINTERCEPTABLE OBJECT:
──────────────────────────────────
1. object.invokeMethod(name, args) is called FIRST
2. Your invokeMethod implementation decides what to do
   (typically looks up the real method via metaClass and invokes it)
3. If you don't handle it → throw MissingMethodException

FOR A JAVA OBJECT (not a GroovyObject):
───────────────────────────────────────
1. Groovy wraps the call via ScriptBytecodeAdapter
2. MetaClass is looked up from MetaClassRegistry
3. Same resolution as step 2a-2d above
4. If NOT found → throw MissingMethodException
   (no methodMissing, since it's not a Groovy class)

The following diagram visualizes the complete method resolution order, showing the decision path for both GroovyInterceptable and regular Groovy objects:

Yes

Yes

No

No (regular GroovyObject)

Found

Not found

Found

Not found

Found

Not found

Found

Not found

Yes

No

obj.someMethod()

Is obj
GroovyInterceptable?

obj.invokeMethod(name, args)
called FIRST

Your invokeMethod
handles it?

Method Executes

MissingMethodException

MetaClass.invokeMethod()

MetaClass searches
in this order:

2a. Methods added to metaClass
(ExpandoMetaClass methods)

2b. Methods declared
on the class itself

2c. Methods inherited
from superclasses

2d. GDK methods
(DefaultGroovyMethods)

methodMissing()
defined on object?

methodMissing(name, args)

Groovy MOP – Method Resolution Order

This resolution algorithm is what makes the groovy meta object protocol so powerful. By hooking into different points – metaClass, methodMissing, or invokeMethod – you control method dispatch at different levels. Understanding which hook to use for your situation is the difference between elegant metaprogramming and confusing spaghetti.

Edge Cases and Best Practices

Best Practices Summary

DO:

  • Use respondsTo() and hasProperty() to check capabilities before relying on them
  • Clean up MetaClass changes using GroovySystem.metaClassRegistry.removeMetaClass() in test teardown
  • Use per-instance metaClass changes when you only need to modify one object
  • Cache methods in methodMissing by registering them on the metaClass
  • Understand the difference between methods (class methods) and metaMethods (GDK methods) when inspecting MetaClasses

DON’T:

  • Modify MetaClasses of core JDK classes in shared library code – it affects all consumers globally
  • Use GroovyInterceptable unless you genuinely need to intercept all method calls
  • Assume the MOP works the same way with @CompileStatic – it doesn’t; static compilation bypasses the MOP
  • Forget that methodMissing has a performance cost – cache resolved methods when possible

Performance Considerations

The MOP adds overhead compared to direct method calls, but Groovy uses several optimizations to minimize the cost.

MOP Performance

// Groovy uses CallSite caching to optimize MOP dispatch
// After the first call, the resolution result is cached

class PerfTest {
    int value = 0
    void increment() { value++ }
}

def obj = new PerfTest()

// First call: full MOP resolution (slower)
// Subsequent calls: cached (much faster)

def start = System.nanoTime()
100_000.times { obj.increment() }
def elapsed = (System.nanoTime() - start) / 1_000_000.0

println "100K calls via MOP: ${String.format('%.2f', elapsed)} ms"
println "Value: ${obj.value}"

// With @CompileStatic, there's no MOP overhead at all
// But you lose dynamic features
println "\nOptimization hierarchy (fastest to slowest):"
println "  1. @CompileStatic methods (no MOP)"
println "  2. Cached call sites (after first call)"
println "  3. MetaClass methods"
println "  4. methodMissing (uncached)"
println "  5. GroovyInterceptable invokeMethod"

Output

100K calls via MOP: 42.15 ms
Value: 100000

Optimization hierarchy (fastest to slowest):
  1. @CompileStatic methods (no MOP)
  2. Cached call sites (after first call)
  3. MetaClass methods
  4. methodMissing (uncached)
  5. GroovyInterceptable invokeMethod

Groovy’s call site caching is the key optimization. After the first method call at a given call site, Groovy caches the resolved MetaMethod so subsequent calls skip the full resolution process. This means the MOP overhead is mainly on the first call. For hot-path code where even cached MOP overhead matters, use @CompileStatic to bypass the MOP entirely.

Common Pitfalls

Pitfall 1: MetaClass Leaking Between Tests

Test Leaking Pitfall

// Test 1: Adds a method to String
String.metaClass.testHelper = { -> "test helper result" }
println "Test 1: ${'hello'.testHelper()}"

// Test 2: Assumes String is clean - FAILS if Test 1 didn't clean up!
// This is a common source of flaky tests
println "Test 2: String still has testHelper? ${String.metaClass.respondsTo('', 'testHelper') as boolean}"

// Fix: Always clean up in test teardown
GroovySystem.metaClassRegistry.removeMetaClass(String)
println "After cleanup: String has testHelper? ${String.metaClass.respondsTo('', 'testHelper') as boolean}"

// Better fix: Use per-instance metaClass or Spock's @ConfineMetaClassChanges
println "\nBest practice: Use per-instance metaClass in tests"
def s = "hello"
s.metaClass.testHelper = { -> "only on this instance" }
println s.testHelper()
println "Other strings affected? ${'world'.metaClass.respondsTo('world', 'testHelper') as boolean}"

Output

Test 1: test helper result
Test 2: String still has testHelper? true
After cleanup: String has testHelper? false

Best practice: Use per-instance metaClass in tests
only on this instance
Other strings affected? false

MetaClass changes are global and persistent within a JVM session. If you modify String.metaClass in one test and don’t clean up, every subsequent test sees the modified version. This causes mysterious test failures that only appear when tests run in a certain order. Always reset MetaClasses in your test teardown, or use per-instance modifications.

Pitfall 2: Confusing invokeMethod Contexts

invokeMethod Context Pitfall

// Pitfall: calling methods inside invokeMethod can cause infinite recursion

class DangerousInterceptor implements GroovyInterceptable {
    def hello() { "Hello!" }

    def invokeMethod(String name, def args) {
        // DANGER: calling println here also goes through invokeMethod!
        // println "Calling ${name}"  // This would cause StackOverflowError

        // Safe: use direct metaMethod invocation
        def method = this.metaClass.getMetaMethod(name, args)
        if (method) {
            return method.invoke(this, args)
        }
        throw new MissingMethodException(name, this.class, args)
    }
}

def d = new DangerousInterceptor()
println d.hello()

// The fix: in GroovyInterceptable, always use metaClass.getMetaMethod()
// to invoke methods, never call them directly from within invokeMethod
println "Safe invocation via getMetaMethod works correctly"

Output

Hello!
Safe invocation via getMetaMethod works correctly

When implementing invokeMethod() with GroovyInterceptable, be careful about infinite recursion. Every method call inside invokeMethod() on this also goes through invokeMethod(). The safe approach is to use metaClass.getMetaMethod() to find the real method and method.invoke() to call it directly.

Conclusion

The Groovy Meta-Object Protocol is the engine that powers everything dynamic in Groovy. Every method call, every property access, every operator – they all flow through the MOP. We’ve traced the full method resolution order, explored the MetaClass hierarchy, examined how Groovy treats both Groovy and Java objects, and built tools to visualize the dispatch process.

Understanding the Groovy MOP transforms you from someone who uses metaprogramming to someone who truly understands it. You now know why methodMissing is slower than metaClass methods, why GroovyInterceptable changes the dispatch order, and why MetaClass cleanup matters in tests.

For the practical side of these concepts, revisit our Groovy Metaprogramming guide. And to learn how to add methods to classes using the MOP, head over to our ExpandoMetaClass guide. The official Groovy metaprogramming docs remain the authoritative reference.

Summary

  • Every Groovy class implements GroovyObject, which provides MOP entry points
  • Method resolution goes: MetaClass methods -> class methods -> GDK methods -> methodMissing -> exception
  • GroovyInterceptable routes all calls through invokeMethod(), even for existing methods
  • The MetaClassRegistry is the global store mapping classes to MetaClass instances
  • Call site caching minimizes MOP overhead; @CompileStatic bypasses it entirely

If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.

Up next: Groovy ExpandoMetaClass – Add Methods at Runtime

Frequently Asked Questions

What is the Meta-Object Protocol (MOP) in Groovy?

The Meta-Object Protocol is the set of rules and interfaces that govern how Groovy dispatches method calls and property accesses. Every method call goes through the MOP, which checks the MetaClass for the method, falls back to methodMissing() if not found, and throws MissingMethodException as a last resort. The MOP is what enables Groovy’s dynamic features like runtime method injection and interception.

What is the difference between MetaClass methods and metaMethods in Groovy?

In Groovy’s MOP, ‘methods’ refers to methods declared on the class itself (and its superclasses), including any methods added via ExpandoMetaClass. ‘metaMethods’ refers to GDK (Groovy Development Kit) methods added by Groovy to all objects, like dump(), each(), collect(), and toInteger(). These come from DefaultGroovyMethods and are injected into every class via the MetaClass system.

How does GroovyInterceptable change method dispatch?

Normally, Groovy calls methods directly if they exist on the class, only falling back to methodMissing for undefined methods. When a class implements GroovyInterceptable, ALL method calls – including calls to methods that genuinely exist – are routed through invokeMethod() first. This enables AOP patterns like logging, security, and profiling on every method call.

Does the MOP work with Java classes in Groovy?

Yes. Even though Java classes don’t implement GroovyObject, Groovy’s MOP still intercepts method calls on them via the ScriptBytecodeAdapter and call site caching. This is how GDK methods like reverse(), take(), and toInteger() work on Java String, List, and other classes. You can also add custom methods to Java classes using the metaClass property.

How do I reset MetaClass changes in Groovy?

Use GroovySystem.metaClassRegistry.removeMetaClass(ClassName) to remove all custom MetaClass modifications and restore the default MetaClass for a class. This is essential in test teardown to prevent MetaClass changes from leaking between tests. For Spock tests, you can use the @ConfineMetaClassChanges annotation to automatically reset MetaClass changes after each test.

Previous in Series: Groovy Metaprogramming – Runtime Magic Explained

Next in Series: Groovy ExpandoMetaClass – Add Methods at Runtime

Related Topics You Might Like:

This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *