Metaclasses, ExpandoMetaClass & Categories
|
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). The Meta-Object Protocol of
Runtime Metaprogramming & the MOP sends every call to a
MetaClass. This page is about that object: what it is, how to change it for a class or for a single instance, and the two
standard ways of adding methods to existing classes at run time, ExpandoMetaClass (permanent, global to the class) and
categories (temporary, scoped to a block and a thread). Adding methods that must be visible to every user of a jar, and to the
type checker, is the job of Extension Modules.
All of these change behaviour only for dynamic code. A call compiled with @CompileStatic is bound at compile time and
ignores metaclass changes and categories; Type Checking Extensions
covers how static code can still be told about such methods.
Metaclasses and the registry
Every class has exactly one metaclass in the MetaClassRegistry, which maps Class to MetaClass. By default the entry is a
MetaClassImpl: it knows the class’s methods, properties and fields (through reflection, plus the GDK) and carries out
invokeMethod, getProperty and the other MOP operations. The registry is GroovySystem.metaClassRegistry.
class Zone { String name = 'north' }
def z = new Zone()
def registry = GroovySystem.metaClassRegistry
println registry.getMetaClass(Zone).getClass().simpleName
println registry.getMetaClass(String).getClass().simpleName
println registry.metaClassCreationHandle.getClass().simpleName
println Zone.metaClass.getClass().simpleName
Zone.metaClass.greet = { -> "hello from ${delegate.name}" }
println registry.getMetaClass(Zone).getClass().simpleName
println new Zone().greet()
registry.removeMetaClass(Zone)
println registry.getMetaClass(Zone).getClass().simpleName
MetaClassImpl
MetaClassImpl
MetaClassCreationHandle
HandleMetaClass
ExpandoMetaClass
hello from north
MetaClassImpl
Zone.metaClass returns a HandleMetaClass, a wrapper that lets Zone.metaClass.greet = ... work whatever the registry holds.
The first assignment replaces the registry entry with an ExpandoMetaClass; removeMetaClass drops it and the class is back
to a plain MetaClassImpl. The MetaClassCreationHandle is the factory the registry uses to make a metaclass the first time it
is needed; replacing it changes the default for every class that has no metaclass yet.
An object takes its metaclass when it is created. An instance made before a class-level change keeps the metaclass it had, and does not see the new methods:
class Zone { String name = 'north' }
def z = new Zone()
Zone.metaClass.greet = { -> "hello from ${delegate.name}" }
println new Zone().greet()
try { z.greet() } catch (MissingMethodException e) { println 'old instance: ' + e.class.simpleName }
hello from north
old instance: MissingMethodException
Make the metaclass changes at start-up, before the affected objects exist, or give each object its own metaclass as shown below.
Custom metaclasses
A custom metaclass is a class that implements MetaClass. The convenient way to write one is to extend DelegatingMetaClass,
which forwards every operation to the metaclass it wraps, and override only the operations to change, here invokeMethod.
Register it in the registry for the class:
class LoggingMetaClass extends DelegatingMetaClass {
LoggingMetaClass(MetaClass delegate) { super(delegate) }
Object invokeMethod(Object object, String name, Object[] args) {
System.out.println "invoking ${name}"
super.invokeMethod(object, name, args)
}
}
class Tank { int level() { 40 } }
def registry = GroovySystem.metaClassRegistry
def mc = new LoggingMetaClass(registry.getMetaClass(Tank))
mc.initialize()
registry.setMetaClass(Tank, mc)
println new Tank().level()
registry.removeMetaClass(Tank)
println new Tank().level()
invoking level
40
40
Registering by naming convention
Groovy also finds a custom metaclass without any registration code if its class lives in the package
groovy.runtime.metaclass followed by the package of the class it serves, and is named <ClassName>MetaClass. For a class
Tank in the default package that is groovy.runtime.metaclass.TankMetaClass; for java.lang.String it would be
groovy.runtime.metaclass.java.lang.StringMetaClass. The metaclass must have a constructor taking a MetaClass. Two source files
(compiled, because the classes must be on the classpath):
// src/groovy/runtime/metaclass/TankMetaClass.groovy
package groovy.runtime.metaclass
class TankMetaClass extends DelegatingMetaClass {
TankMetaClass(MetaClass delegate) { super(delegate) }
Object invokeMethod(Object object, String name, Object[] args) {
System.out.println "convention metaclass: ${name}"
super.invokeMethod(object, name, args)
}
}
// src/Tank.groovy
class Tank { int level() { 40 } }
Compile both, then run a script that never mentions the metaclass:
groovyc -d out src/groovy/runtime/metaclass/TankMetaClass.groovy src/Tank.groovy
echo 'println new Tank().level()' > main.groovy
groovy -cp out main.groovy
convention metaclass: level
40
Per-instance metaclasses
Assigning to obj.metaClass.something on an instance creates a metaclass for that object only (this is what the
HandleMetaClass wrapper does) and leaves every other instance and the class untouched. This is the safest form of
metaprogramming for tests and for adding behaviour to one object.
class Pot { String id }
def a = new Pot(id: 'a')
def b = new Pot(id: 'b')
a.metaClass.water = { -> "watering ${delegate.id}" }
println a.water()
try { b.water() } catch (MissingMethodException e) { println 'b: ' + e.class.simpleName }
def emc = new ExpandoMetaClass(Pot, false, true)
emc.shout = { -> delegate.id.toUpperCase() }
emc.initialize()
b.metaClass = emc
println b.shout()
watering a
b: MissingMethodException
B
The second part builds an ExpandoMetaClass by hand and assigns it with b.metaClass = emc. The constructor flags are
registerWithRuntime (false: do not put it in the registry, so it is private to this object) and allowChangesAfterInit
(true: methods can still be added after initialize()).
ExpandoMetaClass
ExpandoMetaClass (EMC) is the metaclass that lets you add and replace members by assignment. Reaching it is a matter of
writing to SomeClass.metaClass; Groovy swaps in an EMC the first time. The same works on classes you do not own, including
Java and Groovy library classes.
Methods, properties, constructors and static methods
class Greenhouse { String name }
Greenhouse.metaClass.describe = { -> "greenhouse ${delegate.name}" }
Greenhouse.metaClass.describe << { String prefix -> "${prefix}: ${delegate.name}" }
Greenhouse.metaClass.volume = 120
Greenhouse.metaClass.getArea = { -> 40 }
Greenhouse.metaClass.static.create = { String n -> new Greenhouse(name: n) }
Greenhouse.metaClass.constructor = { -> new Greenhouse(name: 'default') }
def g = Greenhouse.create('alpha')
println g.describe()
println g.describe('info')
println g.area
println g.volume
println new Greenhouse().name
greenhouse alpha
info: alpha
40
120
default
-
An assignment
metaClass.name = closuredefines or replaces an instance method. Inside the closuredelegateis the receiver (andthisis the script or class where the closure was written). -
metaClass.name << closureadds an overload and keeps existing methods of that name; plain assignment replaces them. -
metaClass.name = value(not a closure) adds a property with that initial value; a closure namedgetNameadds a property computed on each read. -
metaClass.static.name = closuredefines a static method. -
metaClass.constructor = closureadds a constructor;metaClass.constructor << closureadds one alongside the existing ones.
Methods added to a class are inherited by its subclasses and are also visible through interfaces on Groovy 6.0.0:
class Base {}
class Child extends Base {}
interface Named {}
class Plant implements Named {}
Base.metaClass.hello = { -> 'hello' }
println new Child().hello()
Named.metaClass.label = { -> "named ${delegate.getClass().simpleName}" }
println new Plant().label()
hello
named Plant
ExpandoMetaClass.enableGlobally() was needed in older releases for interface and inheritance cases and still exists; it is not
needed for the two cases above in Groovy 6.0.0.
Adding to Java classes works the same way:
String.metaClass.shout = { -> delegate.toUpperCase() + '!' }
Integer.metaClass.doubled = { -> delegate * 2 }
String.metaClass.static.greenhouse = { -> 'greenhouse' }
println 'basil'.shout()
println 21.doubled()
println String.greenhouse()
BASIL!
42
greenhouse
Borrowing methods
A method reference (obj.&method, see Closures) can be assigned as a metaclass
method, which "borrows" the implementation from another object. The receiver is the object that owns the reference, not the
object the new method is called on.
class Sprinkler { def spray(int ml) { "sprayed ${ml} ml via ${this.getClass().simpleName}" } }
class Hose {}
Hose.metaClass.spray = new Sprinkler().&spray
println new Hose().spray(5)
sprayed 5 ml via Sprinkler
metaClass.mixin(OtherClass) also copies the methods of another class into a metaclass; it is a run-time-only mechanism that
does not make the target an instance of the mixed-in type. Traits are the type-safe alternative, see
Traits.
Runtime discovery
An EMC records what has been added to it, and the metaclass can be asked what a class supports at any time:
class Fan { int speed = 2; def rpm() { speed * 600 } }
Fan.metaClass.boost = { int n -> delegate.speed += n; delegate }
def f = new Fan()
println f.boost(2).rpm()
println Fan.metaClass.methods.findAll { it.name in ['rpm', 'boost'] }*.name.sort()
println Fan.metaClass.expandoMethods*.name
println Fan.metaClass.expandoProperties*.name
println Fan.metaClass.hasMetaMethod('boost', [Integer] as Class[])
println Fan.metaClass.hasMetaProperty('speed')
println Fan.metaClass.respondsTo(f, 'boost').size()
println Fan.metaClass.getMetaMethod('rpm').declaringClass.name
println Fan.metaClass.getMetaMethod('missing')
2400
[boost, rpm]
[boost]
[]
true
true
1
Fan
null
expandoMethods and expandoProperties list only what was added through the EMC; methods lists everything.
GroovyObject methods
The MOP hooks can be defined on an EMC instead of in the class, which is the way to give them to a class you cannot edit.
methodMissing, propertyMissing, invokeMethod, getProperty and setProperty all work as assignments:
class Gauge { String unit = 'C' }
Gauge.metaClass.methodMissing = { String n, a -> "emc methodMissing:${n}" }
Gauge.metaClass.propertyMissing = { String n -> "emc propertyMissing:${n}" }
println new Gauge().whatever()
println new Gauge().unknown
Gauge.metaClass.invokeMethod = { String name, args -> "emc invokeMethod:${name}" }
println new Gauge().whatever()
Gauge.metaClass.getProperty = { String n -> "getProperty:${n}" }
println new Gauge().unit
emc methodMissing:whatever
emc propertyMissing:unknown
emc invokeMethod:whatever
getProperty:unit
Once invokeMethod is defined on the EMC it also takes the calls that methodMissing answered a moment before (third line), and a
getProperty on the EMC is called even for an existing property such as unit.
Categories
A category is a class whose static methods, whose first parameter is the receiver, become instance methods of that receiver
type inside a use block. The effect begins when the block starts and ends when it finishes, and is limited to the current
thread, so categories are the lighter and safer way to add methods temporarily.
class TemperatureCategory {
static double toFahrenheit(Number celsius) { celsius * 9 / 5 + 32 }
static String describe(Number celsius, String unit) { "${celsius} ${unit}" }
}
use(TemperatureCategory) {
println 25.toFahrenheit()
println 25.describe('degrees C')
}
try { 25.toFahrenheit() } catch (MissingMethodException e) { println 'outside use: ' + e.class.simpleName }
77.0
25 degrees C
outside use: MissingMethodException
A static method named getX becomes a property x, as with any getter:
class Units { static double getKelvin(Number c) { c + 273.15 } }
use(Units) { println 25.kelvin }
298.15
Rules to remember, each checked on Groovy 6.0.0:
-
A category method wins over an existing method of the same name and signature. The next example shows
String.length()replaced insideuse. -
useblocks nest, and the innermost category that defines the method wins.use(A, B) { ... }applies several, the later one winning. -
The category is not active in other threads, and not in a closure that outlives the block and is called afterwards.
class Over { static String length(String self) { 'category length' } }
use(Over) { println 'abc'.length() }
println 'abc'.length()
class A { static String tag(String s) { "A:${s}" } }
class B { static String tag(String s) { "B:${s}" } }
use(A) {
println 'x'.tag()
use(B) { println 'x'.tag() }
println 'x'.tag()
}
use(A, B) { println 'x'.tag() }
use(A) {
Thread.start {
try { println 'x'.tag() } catch (MissingMethodException e) { println 'thread: no category' }
}.join()
}
def c
use(A) { c = { 'x'.tag() } }
try { c() } catch (MissingMethodException e) { println 'closure run later: ' + e.class.simpleName }
category length
3
A:x
B:x
A:x
B:x
thread: no category
closure run later: MissingMethodException
Source: Groovy documentation: Categories
The Category transformation
Writing every method as a static with an explicit receiver parameter is noisy. The @Category AST transformation lets you write
instance methods instead, and treats this as the receiver. The transformation turns them into the static form:
@Category(String)
class Shouting {
String shout() { this.toUpperCase() + '!' }
String twice() { "${this} ${this}" }
}
use(Shouting) {
println 'basil'.shout()
println 'mint'.twice()
}
println Shouting.getMethod('shout', String).parameterTypes*.simpleName
BASIL!
mint mint
[String]
The last line shows the generated method: a static shout(String). The duration DSL (TimeCategory, 2.hours) is a category; see Dates & Times.
Choosing between them
| ExpandoMetaClass | Category (use) |
Extension module | |
|---|---|---|---|
Scope |
the class, from the change until it is removed, every thread |
a block, in the current thread |
the whole classpath |
Visible to static type checking |
no |
no |
yes |
Best for |
tests, scripts, one-off additions, DSLs that must be global |
a temporary, bounded addition |
library-quality additions to classes you do not own |
Cost |
changes a class-wide shared object |
entering and leaving a block |
packaging |
Scoped invokedynamic invalidation (Groovy 6.0)
Dynamic calls in Groovy are linked to their targets with invokedynamic, and a link is only valid while the metaclass it relied on is
unchanged. Before Groovy 6.0, one process-wide switch guarded every call site, so any metaclass change, one
Foo.metaClass.bar = {...}, invalidated every linked call site in the JVM. In Groovy 6.0 call sites guard on a switch owned by
the receiver’s metaclass: a change to one class leaves hot call sites on unrelated classes linked. Semantics are unchanged; it is a
performance improvement, most visible in frameworks and test suites that install and remove metaclasses constantly. Entering and
leaving a category block, a non-standard custom MetaClass and registry events that name no class still invalidate everything, and
-Dgroovy.indy.invalidation.stats=true logs invalidation counts for diagnosis.
Practical consequence: metaclass changes are cheaper than they used to be, but a use block in a hot loop, or a custom
MetaClass on a hot class, is still comparatively costly. More on performance is on
Performance, invokedynamic & Native Images.
Source: Groovy 6.0 release notes
Where the book is outdated
Older books treat ExpandoMetaClass as the standard tool and give ExpandoMetaClass.enableGlobally() a central role; as shown
above, subclass and interface cases work without it on Groovy 6.0. They describe @Mixin and metaClass.mixin for composition;
@Mixin was deprecated in favour of traits (Groovy 2.3). They also pre-date the scoped invokedynamic invalidation of Groovy 6.0
and the extension module mechanism covered on the next page, which is the modern replacement for most global
metaClass additions.