Skip to content

Groovy MOP — Meta-Object Protocol and Metaprogramming

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Groovy MOP. We cover key concepts, practical examples, and best practices to help you master this topic.

Groovy's Meta-Object Protocol enables runtime interception of method calls, property access, and class behavior modification through its powerful Metaprogramming API without bytecode manipulation.

What You'll Learn

  • The Meta-Object Protocol architecture
  • methodMissing and propertyMissing
  • ExpandoMetaClass
  • Category and use blocks

Why It Matters

Metaprogramming allows frameworks to provide clean DSLs and reduce boilerplate. Durga Antivirus Pro uses Groovy's MOP to intercept and log plugin API calls for security auditing without modifying plugin source code.

Real-World Use

Grails uses MOP for dynamic finders, Gradle uses it for task DSL, and testing frameworks use it for mocking. Any Groovy framework that offers a clean DSL relies on the MOP.

flowchart LR
    A["MOP"] --> B["methodMissing"]
    A --> C["propertyMissing"]
    A --> D["MetaClass"]
    A --> E["ExpandoMetaClass"]
    B --> F["Dynamic Methods"]
    D --> G["Interception"]
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#dbeafe,stroke:#2563eb,color:#1e40af

How the MOP Works

Every Groovy object has a MetaClass that intercepts method calls:

def str = "hello"
println str.metaClass          // org.codehaus.groovy.runtime.HandleMetaClass
println str.metaClass.methods  // list of methods

The MetaClass decides what happens when a method is called. By default it invokes the real method, but you can override this behavior.

methodMissing

When a method does not exist, Groovy calls methodMissing:

class DynamicPerson {
    def properties = [:]

    def methodMissing(String name, args) {
        if (name.startsWith('get')) {
            def prop = name[3].toLowerCase() + name[4..-1]
            return properties[prop]
        }
        if (name.startsWith('set')) {
            def prop = name[3].toLowerCase() + name[4..-1]
            properties[prop] = args[0]
            return
        }
        throw new MissingMethodException(name, this.getClass(), args)
    }
}

def p = new DynamicPerson()
p.setName('Alice')
p.setAge(30)
println p.getName()  // Alice
println p.getAge()   // 30

This enables dynamic property access without declaring fields.

propertyMissing

Intercept property access similarly:

class ConfigProxy {
    private Map config = [:]

    void load() {
        config.serverUrl = 'https://api.example.com'
        config.timeout = 5000
    }

    def propertyMissing(String name) {
        config[name]
    }

    void propertyMissing(String name, value) {
        config[name] = value
    }
}

def cp = new ConfigProxy()
cp.load()
println cp.serverUrl  // https://api.example.com
cp.timeout = 10000

ExpandoMetaClass

Add methods to existing classes at runtime:

String.metaClass.reverseWords = { ->
    delegate.split(' ').reverse().join(' ')
}

println "hello world".reverseWords()  // world hello

Integer.metaClass.square = { ->
    delegate * delegate
}

println 5.square()  // 25

This is useful for monkey-patching and adding DSL methods.

Method Interception

Intercept all method calls on a class:

class LoggerInterceptor {
    static void install() {
        Object.metaClass.invokeMethod = { String name, args ->
            println "Before: $name with $args"
            def result = delegate.metaClass.getMetaMethod(name, args)?.invoke(delegate, args)
            println "After: $name returned $result"
            result
        }
    }
}

LoggerInterceptor.install()
println "hello".toUpperCase()
// Before: toUpperCase with []
// After: toUpperCase returned HELLO
// HELLO

Category Classes

Use categories to add methods temporarily within a use block:

class StringUtils {
    static String shout(String self) {
        self.toUpperCase() + '!'
    }

    static String reverseWords(String self) {
        self.split(' ').reverse().join(' ')
    }
}

use(StringUtils) {
    println "hello world".shout()        // HELLO WORLD!
    println "hello world".reverseWords() // world hello
}
// Outside use block, methods are gone

Mixin with @Mixin

class LoggingMixin {
    def log(String msg) {
        println "[${new Date()}] $msg"
    }
}

class Service {
    @Mixin(LoggingMixin)
    def process() {
        log "Processing started"
        // business logic
        log "Processing finished"
    }
}

new Service().process()

Common Mistakes

1. Modifying metaClass globally

// Wrong — affects ALL strings everywhere
String.metaClass.shout = { -> delegate.toUpperCase() + '!' }
// Use categories or local MetaClass modifications

2. Forgetting delegate in closures

Inside a metaclass closure, delegate refers to the target object, not this.

3. Performance overhead

Dynamic method resolution is slower. Use @CompileStatic for performance-critical code.

4. Method clashes with existing methods

Adding a method that already exists causes subtle bugs. Check with metaClass.respondsTo(obj, methodName) first.

5. Not handling missing methods gracefully

Always provide fallback behavior in methodMissing instead of throwing raw exceptions.

Practice Questions

1. What is the difference between methodMissing and invokeMethod?

methodMissing is called when no method is found. invokeMethod intercepts every call regardless of existence.

2. How does ExpandoMetaClass differ from categories?

ExpandoMetaClass permanently modifies the metaclass; categories are scoped to a use block.

3. What is the delegate in a metaclass closure?

The object the method was called on, accessed via delegate keyword.

4. Why does @CompileStatic disable MOP features?

Static compilation bypasses the metaclass dispatch mechanism, making method calls direct.

Challenge: Create a Groovy class that supports dynamic attribute access similar to Groovy's Expando.

FAQ

{{< faq question="Is the MOP unique to Groovy?" >}} No, but Groovy has the most accessible MOP among JVM languages. Ruby and Python have similar metaprogramming capabilities. {{< /faq >}}

{{< faq question="Does MOP work with Java classes?" >}} Yes, Groovy can add methods to Java classes via ExpandoMetaClass. The changes affect Groovy code only, not Java callers. {{< /faq >}}

{{< faq question="What is the performance cost of MOP?" >}} Dynamic dispatch is ~10x slower than direct calls. Use @CompileStatic on hot paths to restore native performance. {{< /faq >}}

{{< faq question="Can I remove metaclass modifications?" >}} Yes, by replacing the metaClass: SomeClass.metaClass = new MetaClassImpl(SomeClass). {{< /faq >}}

{{< faq question="What is the difference between Mixin and Trait?" >}} Mixins (deprecated) inject behavior at runtime. Traits are compile-time and type-checkable. {{< /faq >}}

Mini Project

Build a simple request validation framework that uses methodMissing to automatically validate parameters before delegating to real methods.

class ValidatedService {
    private validations = [:]

    def methodMissing(String name, args) {
        def validator = validations[name]
        if (validator && !validator(args)) {
            throw new IllegalArgumentException("Validation failed for $name")
        }
        println "Executing $name with $args"
    }

    void addValidation(String method, Closure validator) {
        validations[method] = validator
    }
}

def svc = new ValidatedService()
svc.addValidation('saveUser') { args -> args[0] != null && args[0].length() > 0 }
svc.saveUser('Alice')  // OK
// svc.saveUser('')     // throws

What's Next

Now that you understand the Meta-Object Protocol, proceed to Groovy categories.

Topic Description Link
Category Scoped method injection {{< ref "19-category" >}}
Traits Compile-time behavior composition {{< ref "22-trait" >}}
Grails Web framework using MOP Grails

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro