AST Transformations: Logging, Safety, Concurrency & Directives
|
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). It covers the built-in AST transformations that are not about generating value classes: logging,
locking, cloning, protecting scripts from runaway code, compiler directives, Swing bean helpers, two testing aids and the
@Grab annotations. The code-generation and class-design transformations (@ToString, @Canonical, @Immutable, @Delegate,
@Builder…) are on
AST Transformations: Code Generation & Class Design,
and writing your own transformation is on
Writing AST Transformations & Macros.
An AST transformation is a hook the compiler runs on the syntax tree after parsing: the annotation is read at compile time and
the generated members are real bytecode, so there is no runtime reflection cost and the result is visible with javap or
reflection. Each example below runs as a script, with import lines at the top.
Logging: @Log, @Commons, @Log4j, @Log4j2, @Slf4j and @PlatformLog
One annotation per logging framework adds a private static final logger field named log and rewrites every call on it so
that the message is built only when the level is enabled. The annotations and the logger they create:
| Annotation | Framework | Logger type behind log |
|---|---|---|
|
|
|
|
Apache Commons Logging |
|
|
Log4j 1.x (legacy, end of life) |
|
|
Log4j 2 |
|
|
SLF4J |
|
|
JDK platform logging ( |
|
All of them take the same four members: value is the name of the field (default log), category the logger name (default is
the class name), visibilityId and loggingStrategy are advanced members (the latter takes a LoggingStrategy class, which is how a team can
support a logging framework that Groovy does not know).
@Log and guarded messages
import groovy.util.logging.Log
import java.lang.reflect.Modifier
import java.util.logging.Level
import java.util.logging.Logger
@Log
class Pump {
int evaluated = 0
String detail() { evaluated++; 'detail' }
void start() {
log.info 'starting pump'
log.fine "fine: ${detail()}" // guarded: FINE is not enabled
log.warning "warning: ${detail()}" // evaluated: WARNING is enabled
}
}
def pump = new Pump()
pump.start()
println "evaluated = ${pump.evaluated}"
println Pump.log.name
println Modifier.toString(Pump.getDeclaredField('log').modifiers)
@Log(value = 'irrigationLog', category = 'greenhouse.irrigation')
class Valve {
void open() { irrigationLog.warning 'valve opened' }
}
new Valve().open()
println Logger.getLogger('greenhouse.irrigation').name
Logger.getLogger('Pump').level = Level.ALL
Logger.getLogger('').handlers.each { it.level = Level.ALL }
pump.start()
println "evaluated = ${pump.evaluated}"
The script ran with a logging.properties file so that the output is compact:
handlers = java.util.logging.ConsoleHandler
.level = INFO
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.SimpleFormatter.format = %4$s | %3$s - %5$s%n
INFO | Pump - starting pump
WARNING | Pump - warning: detail
evaluated = 1
Pump
private static final transient
WARNING | greenhouse.irrigation - valve opened
greenhouse.irrigation
INFO | Pump - starting pump
FINE | Pump - fine: detail
WARNING | Pump - warning: detail
evaluated = 3
Two points matter. First, log.fine "fine: ${detail()}" is compiled to
if (log.isLoggable(Level.FINE)) log.fine(...), so detail() ran only for the warning call the first time
(evaluated = 1) and for both once FINE was enabled (evaluated = 3): an expensive GString argument costs nothing when the
level is off. The guard is added for the calls made on the generated field. Second, the field name is the annotation’s value: @Log(value = 'irrigationLog') creates a
field called irrigationLog and no field called log. A script that uses log there gets a MissingPropertyException.
The other frameworks
The framework jars are not part of Groovy, so they must be on the classpath. In a script that is a @Grab (see
the @Grab family below). SLF4J discovers its provider through the system class loader, so the grab needs
@GrabConfig(systemClassLoader = true).
@GrabConfig(systemClassLoader = true)
@Grab('org.slf4j:slf4j-simple:2.0.17')
import groovy.util.logging.*
@Slf4j
class Sensor {
void read() { log.info 'reading sensor {}', 'T-1' }
}
new Sensor().read()
println Sensor.log.class.name
@Commons
class Legacy { void go() { log.info 'via commons-logging' } }
new Legacy().go()
println Legacy.log.class.name
@PlatformLog
class Plat { void go() { log.log(System.Logger.Level.INFO, 'via System.Logger') } }
new Plat().go()
println Plat.log.class.name
[main] INFO Sensor - reading sensor T-1
org.slf4j.simple.SimpleLogger
[main] INFO Legacy - via commons-logging
org.apache.commons.logging.impl.SLF4JLog
INFO | Plat - via System.Logger
sun.util.logging.internal.LoggingProviderImpl$JULWrapper
The Legacy logger is a SLF4JLog because the Groovy distribution ships jcl-over-slf4j, which redirects Commons Logging
to SLF4J (and therefore to slf4j-simple here). Without a provider on the class path, SLF4J prints
SLF4J(W): No SLF4J providers were found and falls back to a no-operation logger that discards every message; if your
@Slf4j output disappears, look for that line first. @PlatformLog goes through java.lang.System.Logger, which by default
delegates to java.util.logging and can be redirected to any framework that supplies a System.LoggerFinder. Groovy 6.0
itself moved its own diagnostics from System.err to the same Platform Logging API, so the runtime’s warnings and the
application’s @PlatformLog output can be configured in one place.
Compile-time caller locations with @Log4j2(staticLocation = true)
A logging framework that prints the class, method and line of the caller (%C, %M, %L in a Log4j 2 pattern) finds them by
walking the stack. In Groovy the stack contains frames of the runtime itself (metaclass dispatch, the reflective cold dispatch
tier, closure classes), so the answer can name the runtime or a synthetic closure class instead of the code that logged. Groovy
6.0 adds a staticLocation option to @Log4j2 (GROOVY-12378): the AST transformation knows exactly where the log statement is,
so it stores a StackTraceElement for that statement in a synthetic static field and passes it to Log4j 2, which then skips the
stack walk altogether. The option is opt-in because the call shape changes and the transformation needs Log4j 2.13 or later.
@Grab('org.apache.logging.log4j:log4j-api:2.25.2')
@Grab('org.apache.logging.log4j:log4j-core:2.25.2')
import groovy.util.logging.Log4j2
@Log4j2
class Plain {
void go() { log.info 'plain' }
void inClosure() { [1].each { log.info 'closure' } }
}
@Log4j2(staticLocation = true)
class Located {
void go() { log.info 'staticLocation' }
void inClosure() { [1].each { log.info 'closure' } }
}
new Plain().go()
new Plain().inClosure()
new Located().go()
new Located().inClosure()
with a log4j2.properties whose layout pattern is %p %C.%M:%L - %m%n (run with -Dlog4j.configurationFile=log4j2.properties):
INFO Plain.go:7 - plain
INFO Plain$_inClosure_closure1.doCall:8 - closure
INFO Located.go:13 - staticLocation
INFO Located.inClosure:14 - closure
In a simple method both variants report the right place. Inside a closure the plain transformation reports the synthetic closure
class Plain$_inClosure_closure1.doCall, whereas staticLocation = true reports the enclosing method Located.inClosure and
its line. The release notes also describe the case of a call made through a metaclass or in a GraalVM native image; those paths
were not reproduced here.
Declarative concurrency: @Synchronized, @WithReadLock and @WithWriteLock
@Synchronized is a safer synchronized modifier. The Java keyword locks this (or the class), which is a public object any
outsider can also lock, so a rogue synchronized (counter) { ... } elsewhere can stall every method. @Synchronized locks a
private object instead: a field $lock for instance methods and a static field $LOCK for static methods, both created on
demand. @Synchronized('lock') names a lock field you declare yourself, which is how several methods share one lock.
import groovy.transform.Synchronized
import groovy.transform.WithReadLock
import groovy.transform.WithWriteLock
class Counter {
private int plain = 0
private int safe = 0
@Synchronized void bumpSafe() { safe++ }
void bumpPlain() { plain++ } // racy on purpose
int getSafe() { safe }
int getPlain() { plain }
}
def c = new Counter()
def threads = (1..8).collect {
Thread.start { 10_000.times { c.bumpSafe(); c.bumpPlain() } }
}
threads*.join()
println "safe = ${c.safe}"
println "plain lost updates: ${c.plain < 80_000}"
println Counter.declaredFields*.name.findAll { it.startsWith('$') && it != '$staticClassInfo' }
class Cache {
private final Map<String, String> data = [:]
@WithReadLock String read(String k) { data[k] }
@WithWriteLock void write(String k, String v) { data[k] = v }
}
def cache = new Cache()
cache.write('zone-1', 'basil')
println cache.read('zone-1')
println Cache.declaredFields.find { it.name == '$reentrantlock' }.type.simpleName
safe = 80000
plain lost updates: true
[$lock]
basil
ReentrantReadWriteLock
The unannotated counter loses updates (the exact number varies by run, so the example prints only whether any were lost) while the
synchronized one always reaches 80000. @WithReadLock and @WithWriteLock wrap the method body in the read or write lock of a
java.util.concurrent.locks.ReentrantReadWriteLock, again created in a synthetic field ($reentrantlock) unless you pass the
name of your own field, as in @WithReadLock('rw'). Many readers can hold the read lock together; a writer waits for exclusive
access. These annotations do not make compound read-modify-write sequences across several methods atomic: use
one lock for the whole sequence.
For Groovy’s higher-level concurrency features (async/await, channels, dataflow, actors) see Async/Await and Parallel Collections & Dataflow.
Cloning and externalizing: @AutoClone and @AutoExternalize
@AutoClone writes a clone() method and makes the class Cloneable. The default style calls super.clone() and then clones
each field that is itself Cloneable (collections, arrays, dates, other @AutoClone classes). Other styles are chosen with
style: SIMPLE (a regular constructor call, then the properties are copied from the source), COPY_CONSTRUCTOR (a copy
constructor is created and used) and SERIALIZATION (the object is cloned through serialization or externalization, so its
fields must be serializable). The excludes member lists properties that are not cloned: the copy shares them with the original.
import groovy.transform.*
@AutoClone
class Plan {
String name
List<String> zones = []
Date created = new Date(0)
}
def a = new Plan(name: 'spring', zones: ['A', 'B'])
def b = a.clone()
b.zones << 'C'
println "a.zones = ${a.zones}, b.zones = ${b.zones}"
println a.is(b)
@AutoClone(style = AutoCloneStyle.COPY_CONSTRUCTOR)
class Bed {
String id
List<String> crops
}
def bed = new Bed(id: 'b1', crops: ['kale'])
def bed2 = bed.clone()
bed2.crops << 'leek'
println "bed.crops = ${bed.crops}, bed2.crops = ${bed2.crops}"
@AutoClone(style = AutoCloneStyle.SERIALIZATION)
class Tray implements Serializable {
String label
Map<String, List<Integer>> counts = [a: [1]]
}
def t = new Tray(label: 'T')
def t2 = t.clone()
t2.counts.a << 2
println "t.counts = ${t.counts}, t2.counts = ${t2.counts}"
@AutoClone(excludes = 'shared')
class Shared { List<String> own = ['a']; List<String> shared = ['s'] }
def s = new Shared(); def q = s.clone()
println "own copied: ${!q.own.is(s.own)}, shared copied: ${!q.shared.is(s.shared)}"
a.zones = [A, B], b.zones = [A, B, C]
false
bed.crops = [kale], bed2.crops = [kale, leek]
t.counts = [a:[1]], t2.counts = [a:[1, 2]]
own copied: true, shared copied: false
In every style the copy has its own list, so adding to the clone’s list does not touch the original. The two lists named
own and shared show excludes: shared is not cloned, the original and the clone point at the same list. A class that
defines its own clone() is left alone.
@AutoExternalize generates writeExternal and readExternal so that a class implements java.io.Externalizable, writing
each property in declaration order. Externalization avoids the reflective, field-by-field format of default serialization, so
the stream is smaller and faster, at the cost of a stream layout you own. excludes skips properties, and the class needs a
no-argument constructor, which Groovy supplies for a plain property class.
import groovy.transform.*
@AutoExternalize
class Reading {
String sensor
double value
int seq
}
def r = new Reading(sensor: 'T-1', value: 21.5d, seq: 7)
println r instanceof Externalizable
def bos = new ByteArrayOutputStream()
new ObjectOutputStream(bos).withCloseable { it.writeObject(r) }
def back = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())).readObject()
println "${back.sensor} ${back.value} ${back.seq}"
@AutoExternalize(excludes = ['seq'])
class Reading2 { String sensor; int seq }
def bos2 = new ByteArrayOutputStream()
new ObjectOutputStream(bos2).withCloseable { it.writeObject(new Reading2(sensor: 'S', seq: 9)) }
def b2 = new ObjectInputStream(new ByteArrayInputStream(bos2.toByteArray())).readObject()
println "${b2.sensor} ${b2.seq}"
true
T-1 21.5 7
S 0
The excluded property seq was not written, so it comes back with its default value 0. Java deserialization of untrusted
data is a well-known attack vector: only read streams you wrote, and see the filtering options in the JDK’s serialization
documentation before accepting external ones.
Safer scripting: @ThreadInterrupt, @TimedInterrupt, @ConditionalInterrupt and @SafeRegex
A script that loops forever cannot be stopped from outside unless the code cooperates. The three interrupt transformations insert a check into loop bodies, method bodies and closure bodies, so that an infinite loop or a runaway recursion is cut short. They are meant to protect a host application that evaluates user-supplied scripts, and they work together with the compilation customizers on Compilation Customizers & Secure Scripting.
-
@ThreadInterruptchecksThread.currentThread().isInterrupted()and throwsInterruptedException, sothread.interrupt()from the host now stops the script. -
@TimedInterrupt(value, unit)throwsjava.util.concurrent.TimeoutExceptiononce the given time has passed since the annotated object was created. The defaultunitis seconds, so write the unit explicitly when you mean milliseconds. -
@ConditionalInterrupttakes a closure; when it returnstruethe script throwsInterruptedException.
All three have applyToAllClasses (default true): when the annotation is on one class, the check is added to every class
declared in the same source file. That is useful for a script, and surprising in a file with several classes (an annotation
that refers to a field of the annotated class fails to compile in the others), so set applyToAllClasses = false when a file
holds more than one class. checkOnMethodStart (default true) adds the check at method entry as well.
import groovy.transform.TimedInterrupt
import java.util.concurrent.TimeUnit
@TimedInterrupt(value = 200L, unit = TimeUnit.MILLISECONDS)
class Slow {
long spin() {
long n = 0
while (true) { n++ }
}
}
try {
new Slow().spin()
} catch (java.util.concurrent.TimeoutException e) {
println "timed out: ${e.class.name}"
println e.message.take(42)
}
timed out: java.util.concurrent.TimeoutException
Execution timed out after 200 milliseconds
import groovy.transform.ConditionalInterrupt
@ConditionalInterrupt(value = { counter.calls++ > 1000 }, applyToAllClasses = false)
class Capped {
def counter = [calls: 0]
void run() { while (true) { } }
}
try {
new Capped().run()
} catch (InterruptedException e) {
println "interrupted: ${e.message}"
}
interrupted: Execution interrupted. The following condition failed: { counter.calls++ > 1000 }
import groovy.transform.ThreadInterrupt
@ThreadInterrupt
class Worker {
void run() { while (true) { } }
}
def t = Thread.start {
try {
new Worker().run()
} catch (InterruptedException e) {
println "worker stopped: ${e.class.name}"
}
}
sleep 100
t.interrupt()
t.join(2000)
println "alive after interrupt: ${t.alive}"
worker stopped: java.lang.InterruptedException
alive after interrupt: false
The usual way to apply a transformation to code you do not control is not an annotation at all: an
ASTTransformationCustomizer adds it to every class and script a GroovyShell compiles.
import groovy.transform.TimedInterrupt
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
def config = new CompilerConfiguration()
config.addCompilationCustomizers(
new ASTTransformationCustomizer(
[value: 250L, unit: java.util.concurrent.TimeUnit.MILLISECONDS], TimedInterrupt))
def shell = new GroovyShell(config)
println shell.evaluate('(1..10).sum()')
try {
shell.evaluate('while (true) { }')
} catch (java.util.concurrent.TimeoutException e) {
println "user script stopped: ${e.message.take(36)}"
}
55
user script stopped: Execution timed out after 250 millis
Leaving unit out would give the script 250 seconds, because the default unit is seconds. These interrupts are cooperative checks on the Groovy code, not a security boundary: they do not stop a single
blocking Java call such as a long Thread.sleep or an I/O read, and they do not restrict what the script is allowed to call.
@SafeRegex
The interrupt checks live in Groovy code, so they cannot stop one long call inside the JDK, and a regular expression with
catastrophic backtracking is exactly such a call. @SafeRegex(millis = 200) (default 1000) rewrites the find (=~) and match
(==~) operators in the annotated class, method, constructor, field or local variable declaration into deadline-guarded calls
that throw groovy.util.regex.RegexTimeoutException. Calls such as String#matches, replaceAll or split are not rewritten.
import groovy.transform.SafeRegex
import groovy.util.regex.RegexTimeoutException
@SafeRegex(millis = 200)
class Handler {
boolean check(String input) {
input ==~ /(.*,){16}X/
}
}
def handler = new Handler()
assert !handler.check('1,2,3')
println 'well-behaved input: no match, same semantics'
try {
handler.check('1,' * 40)
println 'no timeout'
} catch (RegexTimeoutException e) {
println "adversarial input cut off: ${e.class.simpleName}"
}
well-behaved input: no match, same semantics
adversarial input cut off: RegexTimeoutException
Regular expressions are covered on Regular Expressions.
Compiler directives
These annotations change how the compiler treats code rather than generating a feature.
@Field
In a script, a variable declared with def or a type is local to the run() method, so a method declared in the same script
cannot see it. @Field turns the declaration into a field of the script class.
import groovy.transform.Field
@Field int readings = 0
void bump() { readings++ }
bump(); bump()
println "readings = $readings"
readings = 2
Without @Field, readings would be a local variable and bump() would fail with a MissingPropertyException. Writing a bare
readings = 0 (no declaration) creates a binding variable instead, which is visible to methods but is shared with whoever
supplies the Binding.
See Program Structure, Packages, Imports & Scripts.
@PackageScope
A Groovy field without a modifier is a property: a private field plus a public getter and setter. A method without a modifier
is public. @PackageScope requests Java’s package-private visibility instead.
import groovy.transform.PackageScope
import java.lang.reflect.Modifier
class Zone {
@PackageScope String code = 'Z1'
@PackageScope int slots() { 4 }
String plain = 'p'
}
def code = Zone.getDeclaredField('code')
println "code field modifiers: '${Modifier.toString(code.modifiers)}'"
println "slots() modifiers: '${Modifier.toString(Zone.getDeclaredMethod('slots').modifiers)}'"
println "plain field modifiers: '${Modifier.toString(Zone.getDeclaredField('plain').modifiers)}'"
println Zone.declaredMethods.collect { it.name }.findAll { it.contains('Plain') || it.contains('Code') }.sort()
code field modifiers: ''
slots() modifiers: ''
plain field modifiers: 'private'
[getPlain, setPlain]
The empty modifier list is package-private. code is a plain package-private field with no getter or setter (there is no
getCode), whereas plain is the usual private field with a property accessor pair. On a class, the annotation takes a list of
PackageScopeTarget values (CLASS, FIELDS, METHODS, CONSTRUCTORS) to apply package scope to a whole group of members at once.
@AutoFinal
@AutoFinal marks the parameters of the annotated method or constructor, or of every method and constructor of the annotated
class, as final, so that reassigning a parameter is a compile-time error. Reassigning parameters is widely considered bad
practice, and this turns the convention into a compiler check. Opt a member back out with @AutoFinal(enabled = false).
import groovy.transform.*
@AutoFinal
class Calc {
int twice(int x) { x * 2 }
@AutoFinal(enabled = false) int free(int x) { x = x + 1; x }
}
println new Calc().twice(4)
println new Calc().free(4)
8
5
Reassigning a parameter in an @AutoFinal method is rejected at compile time. Compiling this Scale.groovy with groovyc:
import groovy.transform.AutoFinal
@AutoFinal
class Scale {
int scale(int x) { x = x * 3; x }
}
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
Scale.groovy: 5: The parameter [x] is declared final but is reassigned
. At [5:24] @ line 5, column 24.
int scale(int x) { x = x * 3; x }
^
1 error
The val keyword introduced in Groovy 6.0 declares a single final local variable; see
Variables, Typing, def, var & val.
@AnnotationCollector
@AnnotationCollector builds one annotation out of several, so that a combination repeated on many classes is written once.
Put it on an @interface and list the annotations it stands for, either in the annotation’s value or by annotating the
interface with them (the processor mode).
import groovy.transform.*
@AnnotationCollector([ToString, EqualsAndHashCode])
@interface Value {}
@Value
class Point { int x; int y }
println new Point(x: 1, y: 2)
println new Point(x: 1, y: 2) == new Point(x: 1, y: 2)
// Members of the collected annotations can be given on the alias
@AnnotationCollector([ToString, EqualsAndHashCode])
@interface Entity {}
@Entity(includeNames = true)
class Plot { String name; int area }
println new Plot(name: 'north', area: 12)
// Alternative form: annotate the alias itself
@AnnotationCollector
@ToString(includeNames = true) @EqualsAndHashCode
@interface Pojo {}
@Pojo class Bag { List items = [1] }
println new Bag()
Point(1, 2)
true
Plot(name:north, area:12)
Bag(items:[1])
The alias is expanded by the compiler, so nothing is left of it at run time. A member given on the alias
(includeNames = true above) is passed to every collected annotation that has a member of that name. The mode member controls
what happens when the target already carries one of the collected annotations: the default DUPLICATE reports an error,
PREFER_COLLECTOR and PREFER_EXPLICIT keep one of the two, and PREFER_COLLECTOR_MERGED and PREFER_EXPLICIT_MERGED merge
their members, favouring the named side.
@TypeChecked, @CompileStatic and @CompileDynamic
@TypeChecked runs the static type checker but still emits dynamic Groovy bytecode; @CompileStatic runs the checker and also
emits Java-like bytecode that bypasses the metaobject protocol; @CompileDynamic switches checking off again for a method inside
an annotated class. They are the subject of
Static Type Checking & @CompileStatic; the
compact form is:
import groovy.transform.*
@CompileStatic
class Typed {
int sum(List<Integer> xs) { xs.sum() as int }
@CompileDynamic
def dyn(o) { o.whatever() } // dynamic dispatch opted back in
}
println new Typed().sum([1, 2, 3])
class Duck { def whatever() { 'quack' } }
println new Typed().dyn(new Duck())
try {
new GroovyShell().evaluate '''
import groovy.transform.TypeChecked
@TypeChecked
class Bad { int f() { String s = 'x'; s.foo() } }
'''
} catch (e) { println e.message.readLines()[1].trim() }
6
quack
Script1.groovy: 4: [Static type checking] - Cannot find matching method java.lang.String#foo(). Please check if the declared type is correct and if the method exists.
@DelegatesTo
A closure parameter that a builder method runs with a delegate looks like Closure spec to the type checker, which cannot guess
what the delegate is. @DelegatesTo(Type) on the parameter documents it and tells the type checker (and IDEs) which members
the closure body may call. The example compiles the same builder with and without the annotation under @CompileStatic:
def attempt(String label, String annotation) {
try {
new GroovyShell().evaluate """
import groovy.transform.*
class Schedule { List<String> entries = []; void at(String time, String task) { entries << "\$time \$task" } }
class Builder {
static Schedule build($annotation Closure spec) {
def s = new Schedule(); spec.delegate = s; spec.resolveStrategy = Closure.DELEGATE_FIRST; spec(); s
}
}
@CompileStatic
def plan() { Builder.build { at '06:00', 'water' }.entries }
plan()
"""
println "$label: compiles"
} catch (e) { println "$label: " + e.message.readLines()[1].trim().replaceFirst(/^Script1.groovy: \d+: /, '') }
}
attempt 'without @DelegatesTo', ''
attempt 'with @DelegatesTo ', '@DelegatesTo(value = Schedule, strategy = Closure.DELEGATE_FIRST)'
without @DelegatesTo: [Static type checking] - Cannot find matching method Script1#at(java.lang.String, java.lang.String). Please check if the declared type is correct and if the method exists.
with @DelegatesTo : compiles
The strategy member must match the resolveStrategy the builder actually sets, otherwise the checker looks at the wrong
object first: with the default OWNER_FIRST the failing call above is reported against the enclosing script. Delegation is
explained on
Closure Delegation: owner, delegate & this;
@DelegatesTo can also name a genericTypeIndex or a target parameter when the delegate type is a method argument.
@SelfType
A trait can only be applied to classes that also implement the type(s) in @SelfType; the trait’s code may then call the
methods of those types on this, and the compiler rejects a class that does not satisfy the requirement.
import groovy.transform.SelfType
interface Pump { void start() }
@SelfType(Pump)
trait Monitored {
String report() { 'monitoring ' + this.class.simpleName }
}
class Booster implements Pump, Monitored { void start() {} }
println new Booster().report()
try {
new GroovyShell().evaluate '''
import groovy.transform.SelfType
interface Pump { void start() }
@SelfType(Pump) trait Monitored { String r() { 'x' } }
class Lonely implements Monitored { }
'''
} catch (e) { println e.message.readLines()[1].trim() }
monitoring Booster
Script1.groovy: 5: class 'Lonely' implements trait 'Monitored' but does not implement self type interface 'Pump'
See Traits for the trait mechanics behind it.
Swing patterns: @Bindable, @Vetoable and @ListenerList
These three live in the groovy.beans package of the core jar and are plain JavaBeans helpers: they work
without a window, which is why the example runs headless.
@Bindable adds a PropertyChangeSupport, the listener registration methods and a firing setter. @Vetoable does the same
for constrained properties, with VetoableChangeListener`s that may throw a `PropertyVetoException to reject a change before it
is applied. @ListenerList on a List property generates addX, removeX, getXs and one fire method per listener method
(the name comes from the element type’s simple name).
import groovy.beans.*
import java.beans.*
class Thermostat {
@Bindable double target = 20.0
@Vetoable int fanSpeed = 1
}
def t = new Thermostat()
t.addPropertyChangeListener { PropertyChangeEvent e -> println "changed ${e.propertyName}: ${e.oldValue} -> ${e.newValue}" }
t.addVetoableChangeListener { PropertyChangeEvent e ->
if (e.newValue > 3) throw new PropertyVetoException('too fast', e)
}
t.target = 22.5
t.target = 22.5 // same value: no event
t.fanSpeed = 2
try { t.fanSpeed = 9 } catch (PropertyVetoException e) { println "vetoed: ${e.message}" }
println t.fanSpeed
class Alarm {
@ListenerList List<Runnable> listeners
}
def a = new Alarm()
a.addRunnable { println 'ring 1' }
a.addRunnable { println 'ring 2' }
a.fireRun()
println a.getRunnables().size()
println Alarm.declaredMethods*.name.findAll { it =~ /unnable|fire/ }.sort().unique()
changed target: 20.0 -> 22.5
vetoed: too fast
2
ring 1
ring 2
2
[addRunnable, fireRun, getRunnables, removeRunnable]
@Bindable fires only when the new value differs from the old one, so the second assignment is silent. The vetoed value
9 was never stored: fanSpeed still reads 2. @Bindable and @Vetoable can be combined on one property, and on a class
they apply to every property. They integrate with the bind() syntax of SwingBuilder, a Swing-specific builder that is not
covered further here.
Test assistance: @NotYetImplemented and @ASTTest
@NotYetImplemented (in groovy.test) marks a test that documents behaviour you have not written yet. The test runs; if it
fails the failure is swallowed so the build stays green, and if it unexpectedly passes the transformation throws, which tells you
to remove the annotation. The annotation works on any method; in a test class it is run by the test framework as usual.
import groovy.test.NotYetImplemented
class Planner {
@NotYetImplemented
void learnsFrostDates() {
assert 1 == 2 // not implemented yet: the failure is swallowed
}
@NotYetImplemented
void alreadyWorks() {
assert 1 == 1 // passes: the transformation raises an error
}
}
def p = new Planner()
p.learnsFrostDates()
println 'learnsFrostDates: ok (failure swallowed)'
try { p.alreadyWorks() } catch (AssertionError e) { println 'alreadyWorks: ' + e.message }
learnsFrostDates: ok (failure swallowed)
alreadyWorks: Method is marked with @NotYetImplemented but passes unexpectedly
@ASTTest runs a closure on the syntax tree at a chosen compilation phase, with the annotated node in a variable called node.
It is how the Groovy project itself asserts what the compiler did, and it is the quickest way to inspect inferred types.
Since Groovy 6.0 it can also be placed on loop statements, along with the other transformations that gained loop support (see
Writing AST Transformations & Macros).
import groovy.transform.ASTTest
import groovy.transform.CompileStatic
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.Parameter
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.transform.stc.StaticTypesMarker
@ASTTest(phase = CompilePhase.SEMANTIC_ANALYSIS, value = {
assert node instanceof ClassNode
assert node.name == 'Probe'
assert node.getDeclaredMethod('size', Parameter.EMPTY_ARRAY) != null
})
class Probe { int size() { 3 } }
println 'class-level compile-time assertions passed'
@CompileStatic
void check() {
@ASTTest(phase = CompilePhase.INSTRUCTION_SELECTION, value = {
def inferred = node.getNodeMetaData(StaticTypesMarker.INFERRED_TYPE)
assert inferred.toString(false) == 'java.util.List<java.lang.Integer>'
println "inferred type: ${inferred.toString(false)}"
})
def doubled = [1, 2, 3].collect { it * 2 }
}
check()
inferred type: java.util.List<java.lang.Integer>
class-level compile-time assertions passed
The order of the two lines is the point: the println inside @ASTTest ran while the script was being compiled, before the
script’s own first statement. A failing assert inside an @ASTTest closure aborts the compilation with a compile error.
The @Grab family
@Grab downloads a library and adds it to the class loader before the script runs, which lets a single-file script declare its
dependencies. The supporting annotations are:
| Annotation | Purpose |
|---|---|
|
One dependency, as |
|
A container for several |
|
Adds a repository ( |
|
Removes one transitive dependency from the resolution. |
|
Controls the grab itself, for example |
@GrabResolver(name = 'mirror', root = 'https://repo1.maven.org/maven2/')
@Grab(group = 'org.apache.commons', module = 'commons-text', version = '1.13.0')
import org.apache.commons.text.WordUtils
println WordUtils.capitalizeFully('greenhouse north wing')
Greenhouse North Wing
The @GrabResolver here points at Maven Central, which Grape already searches by default; a name that collides with a built-in
resolver (calling this one central, for instance) fails with unknown resolver central in this setup, so pick a distinct name.
Excluding a transitive dependency that the code actually needs shows the effect of @GrabExclude:
@Grapes([
@Grab(group = 'org.apache.commons', module = 'commons-text', version = '1.13.0'),
@GrabExclude(group = 'org.apache.commons', module = 'commons-lang3')
])
import org.apache.commons.text.WordUtils
try {
println WordUtils.capitalizeFully('greenhouse north wing')
} catch (NoClassDefFoundError e) {
println "excluded dependency is missing: ${e.message}"
}
excluded dependency is missing: org/apache/commons/lang3/StringUtils
Groovy 6.0 adds a second Grape engine, based on Maven Resolver, next to the default Apache Ivy one (incubating in 6.0). Both
accept the same annotations; the Maven engine is selected with -Dgroovy.grape.impl=groovy.grape.maven.GrapeMaven, keeps its
cache in ~/.groovy/grapesM2 instead of ~/.groovy/grapes, does not read ~/.groovy/grapeConfig.xml, and does not honour
@GrabConfig(autoDownload = false). The engines, the cache layout and the offline use of Grape are described in
Grape Dependency Management. Fetching code at run time
from a repository executes whatever the repository serves: pin versions, and use only repositories you trust.
Where the book is outdated
Books written for Groovy 1.x and 2.x predate most of this page. A book of that age covers @Log and its Commons and Log4j 1.x
siblings; @Slf4j, @Log4j2 and @PlatformLog come later, the staticLocation option is new in 6.0, and Log4j 1.x is end of life. @Synchronized, @WithReadLock, @WithWriteLock, @AutoClone and @AutoExternalize have been stable for many releases
and are the parts older texts get right, but the options of the interrupt family (applyToAllClasses, checkOnMethodStart)
are worth re-reading in the current documentation. @AutoFinal and @SafeRegex are newer than the book, @NotYetImplemented lives in the groovy.test package, and
@Grab now has the second engine of Groovy 6.0 behind it. The Swing patterns still apply but Swing-based tooling plays a
smaller part in new projects.