Groovy MOP — Meta-Object Protocol and Metaprogramming
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