100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Groovy and Annotations

Understand how Groovy's AST transformation annotations like @Canonical, @Immutable, and @CompileStatic generate real code at compile time, and how to build your own.

Object-Oriented GroovyIntermediate9 min readJul 10, 2026
Analogies

Annotations in Groovy: Java-Style and AST Transformations

Groovy fully supports standard Java-style annotations such as @Override and @Deprecated, but its real power comes from AST (Abstract Syntax Tree) transformation annotations: markers like @ToString or @Immutable that hook into the compiler and inject generated code — methods, fields, constructors — directly into the class before bytecode is produced, rather than being passive metadata read only at runtime.

🏏

Cricket analogy: A Groovy AST transformation annotation is like a pre-match team briefing note attached to a player's profile that automatically triggers specific preparation drills the moment the squad list is finalized, rather than the coach having to manually remind each player individually.

Common AST Transformation Annotations

Several built-in annotations generate common boilerplate: @ToString produces a readable toString(), @EqualsAndHashCode produces value-based equals() and hashCode(), and @TupleConstructor produces positional constructors from the class's properties. The @Canonical annotation is a convenience meta-annotation that bundles all three together, so @Canonical class Score { int runs; int wickets } gets sensible string representation, equality, and construction with a single line.

🏏

Cricket analogy: @Canonical class Player { String name; int runs } bundling @ToString, @EqualsAndHashCode, and @TupleConstructor together is like a franchise's all-in-one player-registration kit that automatically generates the scorecard display, the duplicate-player check, and the intake form in one bundled package, instead of three separate administrative processes.

groovy
import groovy.transform.Canonical
import groovy.transform.Immutable
import groovy.transform.CompileStatic

@Canonical
class Score {
    int runs
    int wickets
}

@Immutable
class MatchResult {
    String winner
    int margin
}

@CompileStatic
class ScoreCalculator {
    int runRate(int runs, double overs) {
        return (int) (runs / overs)
    }
}

def s1 = new Score(180, 6)
def s2 = new Score(180, 6)
println s1 == s2              // true, from @Canonical's generated equals()
println s1                    // Score(runs:180, wickets:6), from generated toString()

def result = new MatchResult('India', 25)
// result.winner = 'Australia'   // would throw ReadOnlyPropertyException

@Immutable and @CompileStatic

@Immutable generates a class whose properties are read-only after construction, defensively copying mutable field types like lists, sets, or dates so external references can't secretly mutate the object's internal state; attempting to reassign a property throws a ReadOnlyPropertyException at runtime. @CompileStatic takes a different approach, opting a class or method into Java-like static type checking and compilation, which improves performance and catches type errors earlier.

🏏

Cricket analogy: @Immutable class Score { int runs; int wickets } creating a read-only value object with defensive copying is like an official printed scorecard that, once the match ends, cannot be altered by anyone — any "update" actually produces a brand-new scorecard rather than editing the sealed original.

@CompileStatic opts a class or method into strict static compilation, which improves performance and catches type errors early — but it also disables or restricts several dynamic Groovy features within that scope, including methodMissing, propertyMissing, and other runtime metaprogramming hooks, since method resolution happens at compile time against known types.

Writing Custom Annotations

Writing a fully custom annotation starts like Java: declare @interface AllRounder with @Retention and @Target metadata to control where and how long it lives. To make the annotation actually change compiled behavior, rather than just serve as a marker, you pair it with an AST transformation class — typically implementing ASTTransformation — and wire it to the annotation using @GroovyASTTransformationClass, so the compiler invokes your transform whenever the annotation appears.

🏏

Cricket analogy: Writing a custom @interface AllRounder with @Target(ElementType.TYPE) is like the ICC creating a brand-new official player classification badge from scratch, deciding exactly which contexts — Test matches only, or all formats — it's allowed to be awarded in, before any selector can start tagging players with it.

A plain @interface declaration with @Retention(RetentionPolicy.RUNTIME) and @Target(ElementType.TYPE) only defines annotation metadata. To make a custom annotation actually transform code, pair it with an AST transformation class implementing org.codehaus.groovy.transform.ASTTransformation and reference it via @GroovyASTTransformationClass so the compiler invokes your transform when the annotation is encountered.

  • AST transformation annotations generate real code at compile time, not just metadata.
  • @Canonical bundles @ToString, @EqualsAndHashCode, and @TupleConstructor.
  • @Immutable produces read-only objects with defensive copying of mutable fields.
  • Mutating an @Immutable property throws ReadOnlyPropertyException.
  • @CompileStatic trades some dynamic flexibility for static type-checking and performance.
  • methodMissing and similar dynamic hooks can break under @CompileStatic scopes.
  • Custom annotations need an associated AST transformation class to affect compiled behavior.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#GroovyStudyNotes#GroovyAndAnnotations#Groovy#Annotations#Java#Style#StudyNotes#SkillVeris#ExamPrep