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 = closure defines or replaces an instance method. Inside the closure delegate is the receiver (and this is the script or class where the closure was written).

  • metaClass.name << closure adds 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 named getName adds a property computed on each read.

  • metaClass.static.name = closure defines a static method.

  • metaClass.constructor = closure adds a constructor; metaClass.constructor << closure adds 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 inside use.

  • use blocks 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

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.

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.