Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Language Atlas (mini-courses for more languages)  Module 8 – Groovy for Java developers, Jenkins and Gradle

Groovy closures, DSLs and static checking

See how a Groovy closure finds names through its owner and delegate, read a Jenkinsfile and build.gradle as method calls, and fix what @TypeChecked reports.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, groovy 6.0.0 and Java temurin-21.0.12+101.0.LTS
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain how a closure resolves a name through its owner and delegate, and choose a resolve strategy
  • Build a small DSL whose blocks are closures run against spec objects
  • Read a Jenkinsfile and a Gradle build script as ordinary method calls that take closures
  • Switch on @TypeChecked or @CompileStatic, fix what they report, and type a builder with @DelegatesTo

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain how a closure resolves a name through its owner and delegate, and choose a resolve strategy
  • Build a small DSL whose blocks are closures run against spec objects
  • Read a Jenkinsfile and a Gradle build script as ordinary method calls that take closures
  • Switch on @TypeChecked or @CompileStatic, fix what they report, and type a builder with @DelegatesTo

Before you start

On this page

pipeline { stages { stage('Build') { steps { sh 'make' } } } } looks like configuration, yet it is five method calls, four of them taking a closure. Fed to a builder of 60 lines in this lesson, the Jenkinsfile below produces a call log of 10 lines and a two-stage plan, and nothing in it is Jenkins.

Every Groovy DSL works this way, a Jenkinsfile and a build.gradle among them: a method takes a closure and runs it with another object as its delegate. This lesson shows how a closure finds a name, how the resolve strategy changes the answer, how a builder uses that, how to read the two files you meet most as calls, and how @TypeChecked and @CompileStatic move mistakes from the run to the compile.

A closure finds names in its owner, then in its delegate

A closure (Groovy): A block of code in braces, such as { b -> b.minutes }, that is an object of class groovy.lang.Closure: it can be stored, passed and called later, and it keeps this, its owner and its delegate for the names it uses. is an object, and it keeps three references for the names it uses but does not declare. The closures guide defines them: this is “the enclosing class where the closure is defined”; owner is “the direct enclosing object, be it a closure or a class”; and delegate is an object of your choosing, which “by default … is set to owner”. scope.groovy defines a closure in a method of a class and a second closure inside it, and prints what each one sees:

scope.groovy GROOVY · scope/scope.groovy
// Who are this, owner and delegate for a closure defined in a method, and for a closure nested inside it?
class Report {
    String title = 'Nightly builds'

    def show() {
        Closure outer
        outer = {
            println "outer: this is a ${this.getClass().simpleName}, owner is a ${owner.getClass().simpleName}"
            def inner = {
                println "inner: this is a ${this.getClass().simpleName}, owner is the outer closure: ${owner.is(outer)}"
                println "inner: title found through the owners: $title"
            }
            inner()
        }
        outer()
        println "a closure's delegate starts as its owner: ${outer.delegate.is(outer.owner)}"
    }
}

new Report().show()
this, owner and delegate, then four resolve strategies Python · scope_session.py
from session import run

run("""
cd scope
groovy scope.groovy
groovy strategy.groovy
""")

Output

$ cd scope
$ groovy scope.groovy
outer: this is a Report, owner is a Report
inner: this is a Report, owner is the outer closure: true
inner: title found through the owners: Nightly builds
a closure's delegate starts as its owner: true
$ groovy strategy.groovy
OWNER_FIRST    name=release  agent=linux
DELEGATE_FIRST name=test  agent=linux
OWNER_ONLY     name=release  MissingPropertyException: agent
DELEGATE_ONLY  name=test  agent=linux
a local variable under DELEGATE_ONLY: name=local

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 scope_session.py

The first four lines are scope.groovy. In both closures this is the Report object, because this is always the class around the code. The owners differ: the outer closure’s owner is the Report object, the inner closure’s owner is the outer closure, which the script proved with owner.is(outer). The inner closure printed title, which neither closure declares; step through how it was found:

Interactive · step by step How the inner closure found title Step through the lookup of one name, from the closure that uses it to the object that has it.
How the inner closure found title, step 5 of 5. Showing The inner closure, The outer closure, A Report object, The delegate. In focus: The delegate.ownerownerdelegateThe inner closurereads $titleThe outer closureowner of the inner oneA Report objecttitle = 'Nightly builds'The delegatestarts as the owner

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. A name nobody declared. The inner closure prints $title. title is not a parameter of the closure and not a local variable, so the closure has to look for it.
  2. Its owner first. Under the default strategy, OWNER_FIRST, the closure asks its owner first. The owner of a closure defined inside another closure is that outer closure.
  3. The owner's owner. The outer closure has no title either, so it passes the question to its own owner, the Report object whose method defined it.
  4. Found. Report has the property title, and the inner closure prints Nightly builds. this is the Report object in both closures, because this is always the class around the code.
  5. The delegate. Every closure also has a delegate, which starts out as its owner: the last line printed true. A builder changes the delegate, and the strategy says who is asked first.

The lookup went from the closure to its owner, and from that owner to its own owner, until an object had the name. The delegate (of a closure): An object that a Groovy closure can send the names it does not declare to. It starts as the closure's owner; a builder sets it to the object that knows the words of a block. was not needed here, and it was the same object as the owner anyway, as the last line printed. A builder is the code that changes it.

Checkpoint

Check: this, owner and delegate

  1. Question 1 of 2 In scope.groovy a closure named inner is defined inside the closure outer, in a method of Report. What is the owner of inner?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The outer closure

    this is the class around the code, Report in both closures; owner is the object or closure the closure was defined in, so it differs between outer and inner.

  2. Question 2 of 2 Neither closure in scope.groovy declares title, yet the inner one printed Nightly builds. How was it found?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The inner closure asked its owner, the outer closure, which asked its owner, the Report object, which has title

    An undeclared name goes to the owner first under the default strategy, and an owner that is itself a closure passes the question on to its own owner.

The resolve strategy decides who answers when both can

When the delegate is another object, a name may be found in two places, and the closure’s resolve strategy: The rule that tells a Groovy closure where to look for a name it does not declare: OWNER_FIRST, the default, DELEGATE_FIRST, OWNER_ONLY, DELEGATE_ONLY or TO_SELF. says which one is asked. strategy.groovy makes the question real: its owner, the script, has a variable name set to release in its binding, and its delegate, a Stage, has a property name set to test and another, agent, set to linux. It runs the same two closures under four strategies, then one more closure next to a local variable. The last five lines of the session above are its output:

strategy.groovy GROOVY · scope/strategy.groovy
// One name, two places that have it: the closure's owner (this script) and its delegate (a Stage).
class Stage {
    String name = 'test'
    String agent = 'linux'
}

name = 'release'                       // a variable of the script's binding, so a property of the owner
def describe = { "name=$name" }
def onlyStage = { "agent=$agent" }

[Closure.OWNER_FIRST, Closure.DELEGATE_FIRST, Closure.OWNER_ONLY, Closure.DELEGATE_ONLY].each { strategy ->
    def label = [0: 'OWNER_FIRST', 1: 'DELEGATE_FIRST', 2: 'OWNER_ONLY', 3: 'DELEGATE_ONLY'][strategy]
    def c = describe.rehydrate(new Stage(), this, this)
    c.resolveStrategy = strategy
    def d = onlyStage.rehydrate(new Stage(), this, this)
    d.resolveStrategy = strategy
    def agent
    try {
        agent = d()
    } catch (MissingPropertyException e) {
        agent = "MissingPropertyException: ${e.property}"
    }
    println "${label.padRight(14)} ${c()}  ${agent}"
}

// A local variable is not looked up at all: the closure captures it where it is written.
String withLocal() {
    def name = 'local'
    def c = { "name=$name" }.rehydrate(new Stage(), this, this)
    c.resolveStrategy = Closure.DELEGATE_ONLY
    c()
}
println "a local variable under DELEGATE_ONLY: ${withLocal()}"
Strategy Asks name gave agent gave
OWNER_FIRST, the default the owner, then the delegate release linux, from the delegate
DELEGATE_FIRST the delegate, then the owner test linux
OWNER_ONLY the owner only release MissingPropertyException: agent
DELEGATE_ONLY the delegate only test linux

The guide lists a fifth strategy, TO_SELF, for closures that are subclasses of Closure. The table shows the rule a builder needs: under the default, any name the owner has wins over the delegate. That is why builders set DELEGATE_FIRST, so that the words of the DSL go to the object that defines them even when the code around the block has a property or a binding variable of the same name. rehydrate(delegate, owner, thisObject) in the script made a copy of each closure with a new delegate, and setting resolveStrategy chose the order.

The last line shows what the strategies do not cover. A local variable, declared with def where the closure is written, is captured by the closure and never looked up, so name gave local even under DELEGATE_ONLY. A block of a DSL that needs a word must not sit next to a local variable of the same name.

A builder that keeps the default strategy

A builder that sets body.delegate = spec but keeps OWNER_FIRST works until the code around a block has a property or a binding variable with the same name as a word of the DSL: then the owner’s value wins, silently, as name gave release here. Set body.resolveStrategy = Closure.DELEGATE_FIRST together with the delegate.

Checkpoint

Check: The resolve strategy

  1. Question 1 of 2 Which resolve strategy does a Groovy closure use unless code changes it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: OWNER_FIRST

    Under OWNER_FIRST, name gave release from the script before the Stage delegate was asked.

  2. Question 2 of 2 A block runs with DELEGATE_ONLY. It reads version, which only the surrounding script has, as a binding variable set without def. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: MissingPropertyException: the owner is never asked

    The two ONLY strategies close one side completely, which keeps a DSL block from reaching variables of the code around it.

A DSL block is a method that runs its closure against a spec

A domain-specific language (DSL): A small language made for one job, such as describing clock domains. In Tcl it is often a set of procedures, so that a file written in the language is a Tcl script made of their commands. More in “Tcl event loop, coroutines and small DSLs” in Groovy needs nothing beyond what you have seen: methods that take a closure, and a delegate for each closure. PipelineScript.groovy is a builder for the shape of a Jenkinsfile. Each block word is a method of a small spec class: stages belongs to PipelineSpec, stage to StagesSpec, steps to StageSpec, sh and echo to StepsSpec. Each such method makes the spec of the next level, sets it as the delegate of its closure with DELEGATE_FIRST and runs the closure. read.groovy compiles a file as a script whose base class is PipelineScript, the way the DSL guide describes a script base class, so that the first word, pipeline, is a method of the script:

The builder, the reader and the Jenkinsfile they read

pipeline/PipelineScript.groovy

// A small pipeline builder. Each block is a method that runs its closure with a spec object as the delegate, and
// every call a spec receives goes into a call log. Jenkins's own pipeline engine does far more; this one only records.
abstract class PipelineScript extends Script {
    void pipeline(Closure body) {
        def spec = new PipelineSpec()
        Log.call(0, 'pipeline(Closure)', spec)
        Log.runWith(spec, body)
        Log.lines.each { println it }
        println "Plan on agent ${spec.agentLabel}:"
        spec.stages.eachWithIndex { s, i -> println "  ${i + 1}. ${s.name}: ${s.steps.join(', ')}" }
    }
}

class Log {
    static List<String> lines = []

    static void call(int depth, String call, Object delegate = null) {
        lines << '  ' * depth + call + (delegate ? "  -> delegate: ${delegate.getClass().simpleName}" : '')
    }

    static void runWith(Object spec, Closure body) {
        body.delegate = spec
        body.resolveStrategy = Closure.DELEGATE_FIRST
        body()
    }
}

class PipelineSpec {
    String agentLabel = 'none'
    List<StageSpec> stages = []

    String getAny() { 'any' }
    void agent(String label) { Log.call(1, "agent('$label')"); agentLabel = label }
    void stages(Closure body) { def spec = new StagesSpec(pipeline: this); Log.call(1, 'stages(Closure)', spec); Log.runWith(spec, body) }
}

class StagesSpec {
    PipelineSpec pipeline

    void stage(String name, Closure body) {
        def spec = new StageSpec(name: name)
        Log.call(2, "stage('$name', Closure)", spec)
        Log.runWith(spec, body)
        pipeline.stages << spec
    }
}

class StageSpec {
    String name
    List<String> steps = []

    void steps(Closure body) { def spec = new StepsSpec(stage: this); Log.call(3, 'steps(Closure)', spec); Log.runWith(spec, body) }
}

class StepsSpec {
    StageSpec stage

    void sh(String command) { Log.call(4, "sh('$command')"); stage.steps << "sh $command" }
    void echo(String message) { Log.call(4, "echo('$message')"); stage.steps << "echo $message" }
}

pipeline/read.groovy

import org.codehaus.groovy.control.CompilerConfiguration

// Compiles the file named on the command line as a script whose base class is PipelineScript, so that its
// first word, pipeline, is a method of that class, then runs it.
def config = new CompilerConfiguration()
config.scriptBaseClass = PipelineScript.name
def shell = new GroovyShell(this.class.classLoader, new Binding(), config)
shell.evaluate(new File(args[0]))

pipeline/Jenkinsfile

pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                sh 'make'
            }
        }
        stage('Test') {
            steps {
                sh 'make test'
                echo 'tests passed'
            }
        }
    }
}
The Jenkinsfile read by the small builder Python · pipeline_session.py
from session import run

# The Jenkinsfile, compiled as a script whose base class is PipelineScript and run: the call log, then the plan.
run("""
cd pipeline
groovy read.groovy Jenkinsfile
""")

Output

$ cd pipeline
$ groovy read.groovy Jenkinsfile
pipeline(Closure)  -> delegate: PipelineSpec
  agent('any')
  stages(Closure)  -> delegate: StagesSpec
    stage('Build', Closure)  -> delegate: StageSpec
      steps(Closure)  -> delegate: StepsSpec
        sh('make')
    stage('Test', Closure)  -> delegate: StageSpec
      steps(Closure)  -> delegate: StepsSpec
        sh('make test')
        echo('tests passed')
Plan on agent any:
  1. Build: sh make
  2. Test: sh make test, echo tests passed

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pipeline_session.py

The call log has one line for each call the builder received, indented by its depth: pipeline(Closure) with a PipelineSpec as the delegate, then agent('any'), stages(Closure), a stage for Build and one for Test, and inside each a steps with its sh call, followed in Test by an echo. The plan under it is what the specs recorded. Step through the blocks and the object that answered each:

Interactive · stage by stage The Jenkinsfile, read as nested calls Step through the blocks of the Jenkinsfile, from pipeline down to sh, and the object that answers each one.
The Jenkinsfile, read as nested calls, step 8 of 8. Showing pipeline { … }, agent any, stages { … }, stage('Build') { … }, steps { … }, sh 'make', The plan. In focus: The plan.stage('Test')pipeline { … }PipelineScript.pipelineagent anyPipelineSpec.agentstages { … }PipelineSpec.stagesstage('Build') { … }StagesSpec.stagesteps { … }StageSpec.stepssh 'make'StepsSpec.shThe plan2 stages, 3 steps

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. The first word. The file is compiled as a script whose base class is PipelineScript, so pipeline is a method of the script itself. Its only argument is the closure in braces.
  2. agent any. pipeline runs the closure with a PipelineSpec as its delegate. agent any is the call agent(any), and any is a property the delegate answers with the text any.
  3. stages. stages is a method of PipelineSpec that takes a closure and runs it with a StagesSpec as its delegate, one level down.
  4. One stage. stage('Build') { … } is a call with two arguments, a name and a closure; the closure goes after the parentheses. Its delegate is a new StageSpec named Build.
  5. steps. steps hands its closure a StepsSpec, the object that knows the step commands, and records into the stage it belongs to.
  6. A step. sh 'make' is the call sh('make') on the StepsSpec. The builder writes it to the call log and adds sh make to the steps of Build.
  7. The next stage. Back in the stages closure, stage('Test') makes a second StageSpec, and its steps closure records sh make test and echo tests passed.
  8. The plan. When the outer closure returns, pipeline prints the call log and the plan: Build with one step and Test with two, on agent any.

Key idea

A DSL block is a method call whose last argument is a closure; the method gives the closure a delegate that knows the words of the next level, and DELEGATE_FIRST makes those words win.

That ease has a cost to keep in mind whenever a DSL reads files written by other people:

A file read by a DSL is a program

read.groovy runs whatever file it is given, with every power of Groovy, so read only files you trust with it. The DSL guide’s secure AST customizer can restrict the grammar of a script, and it says itself that it “does not replace a security manager”. Jenkins runs pipeline Groovy in its sandbox, and approving more methods is an administrator’s decision on the script approval page.

Checkpoint

Check: A builder made of closures

  1. Question 1 of 2 What does the stage method of StagesSpec do with its closure argument?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It makes a StageSpec, sets it as the closure's delegate with DELEGATE_FIRST, and runs the closure

    Inside the closure, steps then reaches the StageSpec, because the delegate is asked first.

  2. Question 2 of 2 The Jenkinsfile says agent any. What are agent and any to the builder?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: agent is a method of PipelineSpec, and any is a property it answers with the text any

    A word followed by an argument is a method call, and a bare name is a property read; both are answered by the delegate in force.

A Jenkinsfile and a build.gradle are calls that take closures

Groovy lets a call drop its parentheses when it is a statement with arguments, sh 'make', and lets a closure that is the last argument stand after the parentheses, stage('Build') { … }. With every parenthesis written out, the Jenkinsfile becomes explicit.groovy, and no_parentheses.groovy tries two blocks with their parentheses and without them. A Gradle build script is read the same way: Gradle’s primer says every script is “backed by an implicit instance of” a Project, and a block is a method whose last argument is a Closure or an Action. read_build.groovy runs build.gradle with a stand-in project as the delegate, an object that records each call it receives, through methodMissing and setProperty, instead of building anything:

The Jenkinsfile with its parentheses, two blocks without them, and a Gradle build script

pipeline/explicit.groovy

pipeline({
    agent(any)
    stages({
        stage('Build', {
            steps({
                sh('make')
            })
        })
        stage('Test', {
            steps({
                sh('make test')
                echo('tests passed')
            })
        })
    })
})

pipeline/no_parentheses.groovy

// Two blocks with their parentheses, then the same two lines without them: Groovy gives the closure to the
// argument, so stage and retry are never called.
def stage(String name, Closure body) { println "stage('$name', Closure)"; body() }
def retry(int times, Closure body) { println "retry($times, Closure)"; body() }
def sh(String command) { println "  sh('$command')" }

stage('Deploy') { sh './deploy.sh' }
retry(3) { sh 'make' }

try {
    stage 'Deploy' { sh './deploy.sh' }
} catch (MissingMethodException e) {
    println "stage 'Deploy' { … }: ${e.class.simpleName}, no method ${e.method}(Closure) in ${e.type.simpleName}"
}
try {
    retry 3 { sh 'make' }
} catch (MissingMethodException e) {
    println "retry 3 { … }: ${e.class.simpleName}, no method ${e.method}(Closure) in ${e.type.simpleName}"
}

gradle/build.gradle

plugins {
    id 'groovy'
}

version = '1.0.1'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.apache.groovy:groovy:6.0.0'
    testImplementation 'org.junit.jupiter:junit-jupiter:6.1.3'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

test {
    useJUnitPlatform()
}

gradle/read_build.groovy

// Reads build.gradle the way Gradle's Groovy DSL is read, as calls on a project object, with a stand-in project
// that records each call instead of building anything. Gradle itself is not involved.
class Recorder {
    String name
    int depth
    Recorder(String name, int depth) { this.name = name; this.depth = depth }

    def methodMissing(String method, Object args) {
        def list = (args as List)
        def shown = list.collect { it instanceof Closure ? 'Closure' : "'$it'" }.join(', ')
        println '  ' * depth + "${name}.${method}(${shown})"
        if (list && list.last() instanceof Closure) {
            Closure body = list.last()
            body.delegate = new Recorder(method, depth + 1)
            body.resolveStrategy = Closure.DELEGATE_FIRST
            body()
        }
    }

    void setProperty(String property, Object value) {
        println '  ' * depth + "${name}.set${property.capitalize()}('$value')"
    }
}

def text = new File('build.gradle').text
Closure script = new GroovyShell().evaluate("{ ->\n$text\n}")
script.delegate = new Recorder('project', 0)
script.resolveStrategy = Closure.DELEGATE_FIRST
script()
The pipeline with every parenthesis, two blocks without them, and build.gradle as calls Python · reading_session.py
from session import run

# The same pipeline with every parenthesis written out, two blocks with and without their parentheses, then
# build.gradle read by a stand-in project that records each call it receives.
run("""
cd pipeline
groovy read.groovy explicit.groovy
groovy no_parentheses.groovy
cd ../gradle
groovy read_build.groovy
""")

Output

$ cd pipeline
$ groovy read.groovy explicit.groovy
pipeline(Closure)  -> delegate: PipelineSpec
  agent('any')
  stages(Closure)  -> delegate: StagesSpec
    stage('Build', Closure)  -> delegate: StageSpec
      steps(Closure)  -> delegate: StepsSpec
        sh('make')
    stage('Test', Closure)  -> delegate: StageSpec
      steps(Closure)  -> delegate: StepsSpec
        sh('make test')
        echo('tests passed')
Plan on agent any:
  1. Build: sh make
  2. Test: sh make test, echo tests passed
$ groovy no_parentheses.groovy
stage('Deploy', Closure)
  sh('./deploy.sh')
retry(3, Closure)
  sh('make')
stage 'Deploy' { … }: MissingMethodException, no method Deploy(Closure) in no_parentheses
retry 3 { … }: MissingMethodException, no method call(Closure) in Integer
$ cd ../gradle
$ groovy read_build.groovy
project.plugins(Closure)
  plugins.id('groovy')
project.setVersion('1.0.1')
project.repositories(Closure)
  repositories.mavenCentral()
project.dependencies(Closure)
  dependencies.implementation('org.apache.groovy:groovy:6.0.0')
  dependencies.testImplementation('org.junit.jupiter:junit-jupiter:6.1.3')
  dependencies.testRuntimeOnly('org.junit.platform:junit-platform-launcher')
project.test(Closure)
  test.useJUnitPlatform()

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 reading_session.py

The explicit version printed a call log identical to the Jenkinsfile’s, line for line. The next program ran stage('Deploy') { … } and retry(3) { … } as calls of stage and retry, and then the same two lines without their parentheses, which never reached either method:

Leaving out the parentheses before a closure

stage 'Deploy' { … } is not stage('Deploy') { … }: Groovy gives the closure to the argument, not to stage. The quoted word became a call of a method named Deploy, and the 3 of retry 3 { … } was called with the closure, as call(Closure) on an Integer; both stopped with MissingMethodException. Keep the parentheses whenever a closure follows an argument.

The build script turned into the calls a Gradle project receives:

In build.gradle The call
plugins { id 'groovy' } project.plugins(Closure), then id('groovy') on its delegate
version = '1.0.1' project.setVersion('1.0.1'), an assignment to a property
repositories { mavenCentral() } project.repositories(Closure), then mavenCentral()
implementation 'org.apache.groovy:groovy:6.0.0' dependencies.implementation('org.apache.groovy:groovy:6.0.0')
test { useJUnitPlatform() } project.test(Closure), then useJUnitPlatform()

Gradle’s DSL reference writes such a line as configurationName(dependencyNotation): implementation is the name of a configuration that a plugin added, used as a method, which is why the stand-in caught it with methodMissing too. Gradle’s Kotlin DSL page presents that DSL as “an alternative to the traditional Groovy DSL” and allows both in one build, so a build.gradle you meet is ordinary Groovy, read by the Groovy inside Gradle.

Read an unknown block as a call

When a line of a Jenkinsfile or a build script puzzles you, add the parentheses in your head: sh 'make' is sh('make'), and retry(3) { … } is retry(3, { … }), a method of the delegate in force. Then look the method up in the tool’s documentation of that object.

Checkpoint

Check: Reading a Jenkinsfile and build.gradle as calls

  1. Question 1 of 2 Which line is the stage('Test') block of the Jenkinsfile with its parentheses written out?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: stage('Test', { steps({ sh('make test'); echo('tests passed') }) })

    explicit.groovy wrote every block that way and printed a call log identical to the Jenkinsfile's.

  2. Question 2 of 2 In build.gradle, version = '1.0.1' is not inside any block. Which call does it become?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: project.setVersion('1.0.1'): an assignment to a property of the project

    Assignments in a build script set properties of the object in charge, the Project at the top level.

@TypeChecked turns a run-time typo into a compile error

Dynamic Groovy resolves a property while the program runs, so a typo compiles. typo.groovy reads it.minuts instead of it.minutes; typo_checked.groovy is the same file with @TypeChecked on the method. The semantics guide says that with type checking (@TypeChecked): Checking at compile time that every method, property and assignment of the code exists and fits its types. In Groovy it is switched on for a class or a method with @TypeChecked, and the calls stay dynamic. “method calls are resolved at compile time”, so errors such as a property not found appear before the code runs. Step through the two runs:

Interactive · message by message One typo, found while running or while compiling Step through typo.groovy, then typo_checked.groovy, and see when each one learns about it.minuts.
One typo, found while running or while compiling, step 5 of 5. Messages so far: You to The compiler: typo.groovy; The compiler to The script: run(); The script to You: MissingPropertyException; You to The compiler: typo_checked.groovy; The compiler to You: 2 compile errors.Youa terminalThe compilerGroovy 6.0.0The scripton JDK 21typo.groovyrun()MissingPropertyExceptiontypo_checked.groovy2 compile errorsnever started

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Compile, dynamic. groovy typo.groovy compiles the file. In dynamic Groovy, it.minuts is a property read that is resolved only when it runs, so the compiler accepts it.
  2. Run until the line. The script starts and prints Summing the builds…. Everything before the typo has already happened, here one line of output, in a real tool perhaps a file written.
  3. The typo runs. At line 9, inside each, Build has no property minuts: MissingPropertyException, with Possible solutions: minutes, and exit status 1.
  4. Compile, type-checked. groovy typo_checked.groovy compiles the same code with @TypeChecked on the method. The checker knows that it is a Build and that Build has no minuts.
  5. Nothing runs. The compiler stops with two errors at line 12, No such property: minuts, and the plus that has no matching method as a result. No line of the script ran.

The typo without and with @TypeChecked, and a builder without and with @DelegatesTo

checked/typo.groovy

// A typo in dynamic Groovy: the file compiles, starts, and fails only when the line with the typo runs.
class Build {
    String name
    int minutes
}

int total(List<Build> builds) {
    int sum = 0
    builds.each { sum += it.minuts }
    sum
}

println 'Summing the builds…'
println total([new Build(name: 'api', minutes: 12), new Build(name: 'web', minutes: 18)])

checked/typo_checked.groovy

import groovy.transform.TypeChecked

// The same typo with @TypeChecked on the method: the compiler stops, and nothing runs.
class Build {
    String name
    int minutes
}

@TypeChecked
int total(List<Build> builds) {
    int sum = 0
    builds.each { sum += it.minuts }
    sum
}

println 'Summing the builds…'
println total([new Build(name: 'api', minutes: 12), new Build(name: 'web', minutes: 18)])

checked/dsl_unhinted.groovy

import groovy.transform.TypeChecked

// A builder method whose closure parameter says nothing about its delegate, used from type-checked code.
class StepsSpec {
    List<String> commands = []
    void sh(String command) { commands << command }
}

List<String> steps(Closure body) {
    def spec = new StepsSpec()
    body.delegate = spec
    body.resolveStrategy = Closure.DELEGATE_FIRST
    body()
    spec.commands
}

@TypeChecked
List<String> release() {
    steps {
        sh 'make'
        sh 'make test'
    }
}

println release()

checked/dsl_hinted.groovy

import groovy.transform.TypeChecked

// The same builder with @DelegatesTo: the type checker now knows which class answers sh inside the block.
class StepsSpec {
    List<String> commands = []
    void sh(String command) { commands << command }
}

List<String> steps(@DelegatesTo(value = StepsSpec, strategy = Closure.DELEGATE_FIRST) Closure body) {
    def spec = new StepsSpec()
    body.delegate = spec
    body.resolveStrategy = Closure.DELEGATE_FIRST
    body()
    spec.commands
}

@TypeChecked
List<String> release() {
    steps {
        sh 'make'
        sh 'make test'
    }
}

println release()
A typo at run time and at compile time; a builder checked without and with @DelegatesTo Python · typecheck_session.py
from session import run

# A typo found while the script runs, then by @TypeChecked before it runs; a builder under @TypeChecked without and
# with @DelegatesTo.
run("""
cd checked
!groovy typo.groovy
!groovy typo_checked.groovy
!groovy dsl_unhinted.groovy
groovy dsl_hinted.groovy
""")

Output

$ cd checked
$ groovy typo.groovy
Summing the builds…
… groovy.ui.GroovyMain run
SEVERE: Caught: {0}
groovy.lang.MissingPropertyException: No such property: minuts for class: Build
Possible solutions: minutes
	at typo$_total_closure1.doCall(typo.groovy:9)
	at typo.total(typo.groovy:9)
	at typo.run(typo.groovy:14)

groovy.lang.MissingPropertyException: No such property: minuts for class: Build
Possible solutions: minutes
	at typo$_total_closure1.doCall(typo.groovy:9)
	at typo.total(typo.groovy:9)
	at typo.run(typo.groovy:14)
[exit status 1]
$ groovy typo_checked.groovy
… groovy.ui.GroovyMain run
SEVERE: org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
typo_checked.groovy: 12: [Static type checking] - No such property: minuts for class: Build
 @ line 12, column 26.
       builds.each { sum += it.minuts }
                            ^

typo_checked.groovy: 12: [Static type checking] - Cannot find matching method java.lang.Integer#plus(java.lang.Object). Please check if the declared type is correct and if the method exists.
 @ line 12, column 19.
       builds.each { sum += it.minuts }
                     ^

2 errors
[exit status 1]
$ groovy dsl_unhinted.groovy
… groovy.ui.GroovyMain run
SEVERE: org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
dsl_unhinted.groovy: 20: [Static type checking] - Cannot find matching method dsl_unhinted#sh(java.lang.String). Please check if the declared type is correct and if the method exists.
 @ line 20, column 9.
           sh 'make'
           ^

dsl_unhinted.groovy: 21: [Static type checking] - Cannot find matching method dsl_unhinted#sh(java.lang.String). Please check if the declared type is correct and if the method exists.
 @ line 21, column 9.
           sh 'make test'
           ^

2 errors
[exit status 1]
$ groovy dsl_hinted.groovy
[make, make test]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 typecheck_session.py

The dynamic script printed Summing the builds… and then failed at line 9 with MissingPropertyException, and Groovy even suggested minutes. The checked one never started: two errors at line 12, the missing property and, as a result, a plus with no matching method. Groovy 6 reports both through Java’s logging, which is why each starts with a line from groovy.ui.GroovyMain.

A builder needs one more step under the checker. In dsl_unhinted.groovy the checker sees sh 'make' inside a closure whose delegate it cannot know, and reports “Cannot find matching method dsl_unhinted#sh(java.lang.String)”. The DSL guide’s answer is @DelegatesTo on the closure parameter. dsl_hinted.groovy declares @DelegatesTo(value = StepsSpec, strategy = Closure.DELEGATE_FIRST) Closure body, the checker then knows which class answers sh, and the run printed [make, make test]. The same annotation is what lets an IDE complete the words of a DSL.

Checkpoint

Check: @TypeChecked and @DelegatesTo

  1. Question 1 of 2 typo.groovy reads it.minuts. When does dynamic Groovy report the mistake?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: When the line runs, after the script has already printed Summing the builds…

    With @TypeChecked the same file stopped at compile time with No such property: minuts, before printing anything.

  2. Question 2 of 2 Under @TypeChecked, sh 'make' inside steps { … } failed with Cannot find matching method dsl_unhinted#sh. What fixes it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: @DelegatesTo(value = StepsSpec, strategy = Closure.DELEGATE_FIRST) on the closure parameter of steps

    The checker looked for sh on the script, the closure's owner, because nothing told it about the delegate.

@CompileStatic binds each call when the code compiles

@CompileStatic does everything @TypeChecked does and then compiles the calls as Java does, bound once, when the code compiles. The semantics guide lists three benefits: type safety, “immunity to monkey patching” and better performance on CPU-heavy code. The second is easy to see. patched.groovy replaces a method of Pricing at run time through its metaclass, the way Groovy code can change a class while it runs, and calls the method through a dynamic function and through a static one:

patched.groovy GROOVY · checked/patched.groovy
import groovy.transform.CompileStatic

// @CompileStatic binds the call when it compiles, so a method replaced at run time does not reach it.
class Pricing {
    int doubled(int x) { x * 2 }
}

int dynamicCall(Pricing p) { p.doubled(21) }

@CompileStatic
int staticCall(Pricing p) { p.doubled(21) }

println "before the patch: dynamic ${dynamicCall(new Pricing())}, static ${staticCall(new Pricing())}"
Pricing.metaClass.doubled = { int x -> x * 3 }
println "after the patch:  dynamic ${dynamicCall(new Pricing())}, static ${staticCall(new Pricing())}"
A method replaced at run time, called dynamically and statically Python · static_session.py
from session import run

run("""
cd checked
groovy patched.groovy
""")

Output

$ cd checked
$ groovy patched.groovy
before the patch: dynamic 42, static 42
after the patch:  dynamic 63, static 42

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 static_session.py

Before the patch both calls gave 42. After it, the dynamic call found the new method, 21 × 3 = 63, and the static call still gave 42: it had been bound to the compiled doubled when the file compiled. That is the trade: static compilation (@CompileStatic): Compiling Groovy code with the checks of @TypeChecked and every call bound when the code compiles, as in Java. Such code ignores methods that metaprogramming adds or replaces at run time. makes calls predictable and fast, and it ignores what metaprogramming changes at run time, so code that relies on methodMissing, an added method or a builder without @DelegatesTo must stay dynamic.

Case Dynamic Groovy @TypeChecked @CompileStatic
A typo such as it.minuts fails when the line runs compile error compile error
A method added at run time is called compile error unless declared compile error unless declared
How a call is bound while it runs while it runs when it compiles, as in Java
A builder without @DelegatesTo works compile error compile error

Static where the logic is, dynamic at the edges

Put @CompileStatic on the classes that compute, and keep the parsing, printing and DSL glue dynamic. The project lesson of this module does exactly that: its summary class is static, and the JSON reading and the report printing are not.

Checkpoint

Check: @CompileStatic

  1. Question 1 of 2 After Pricing.metaClass.doubled was replaced at run time, what did the dynamic call and the @CompileStatic call return for doubled(21)?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Dynamic 63, static 42

    This is the immunity to monkey patching that the semantics guide lists among the benefits of static compilation.

  2. Question 2 of 2 Which part of a Groovy report tool is the best first candidate for @CompileStatic?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The class that computes the summary from typed objects

    Static where the logic is, dynamic at the edges: the project lesson's summary class is compiled statically.

Practise: find a name the way a closure does

The exercise models the lookup that this lesson is built on: given what the owner and the delegate have, and a strategy, return what a closure finds or the error it gets. Write it in Python, whose sample tests the page can run for you, against the four strategies that strategy.groovy recorded:

Exercise · Easy · Python

Find a name the way a closure's resolve strategy does

A builder works only if you can predict where each name in a block is found. Write resolve(name, owner, delegate, strategy) in resolve.py, a model of the lookup a Groovy closure makes for a name it does not declare itself:

  • owner and delegate are dicts from the names each object has to their values;
  • strategy is one of "OWNER_FIRST" (Groovy's default), "DELEGATE_FIRST", "OWNER_ONLY" and "DELEGATE_ONLY";
  • return the value from the first object the strategy asks that has the name: the owner, then the delegate, for OWNER_FIRST; the delegate, then the owner, for DELEGATE_FIRST; only the owner or only the delegate for the other two;
  • when no object it may ask has the name, raise LookupError(f"No such property: {name}"), as Groovy raises MissingPropertyException.

The tests replay strategy.groovy: the owner, the script, has name = "release", and the delegate, a Stage, has name = "test" and agent = "linux".

Starter code · resolve.py

def resolve(name, owner, delegate, strategy):
    """The value a closure finds for name under the strategy, or LookupError when it may not find one."""
    # Replace this with your code: it always asks the owner first, then the delegate, whatever the strategy says.
    for place in (owner, delegate):
        if name in place:
            return place[name]
    raise LookupError(f"No such property: {name}")
The sample tests · test_resolve.py
from resolve import resolve

SCRIPT = {"name": "release"}
STAGE = {"name": "test", "agent": "linux"}


def error_of(*args):
    """The message of the LookupError that resolve raises, or None when it returns a value."""
    try:
        resolve(*args)
    except LookupError as e:
        return str(e)
    return None


def test_owner_first_finds_the_scripts_name():
    """OWNER_FIRST printed name=release and agent=linux"""
    assert resolve("name", SCRIPT, STAGE, "OWNER_FIRST") == "release"
    assert resolve("agent", SCRIPT, STAGE, "OWNER_FIRST") == "linux"


def test_delegate_first_finds_the_stages_name():
    """DELEGATE_FIRST printed name=test"""
    assert resolve("name", SCRIPT, STAGE, "DELEGATE_FIRST") == "test"
    assert resolve("agent", SCRIPT, STAGE, "DELEGATE_FIRST") == "linux"


def test_owner_only_never_asks_the_delegate():
    """OWNER_ONLY printed name=release, then MissingPropertyException: agent"""
    assert resolve("name", SCRIPT, STAGE, "OWNER_ONLY") == "release"
    assert error_of("agent", SCRIPT, STAGE, "OWNER_ONLY") == "No such property: agent"


def test_delegate_only_never_asks_the_owner():
    """DELEGATE_ONLY printed name=test, and a name only the owner has is out of reach"""
    assert resolve("name", SCRIPT, STAGE, "DELEGATE_ONLY") == "test"
    assert error_of("version", {"version": "1.0.1"}, STAGE, "DELEGATE_ONLY") == "No such property: version"


def test_a_name_nobody_has():
    """no strategy invents a value"""
    assert error_of("timeout", SCRIPT, STAGE, "DELEGATE_FIRST") == "No such property: timeout"
A hint

Turn the strategy into the list of objects to ask, in order: [owner, delegate] for OWNER_FIRST, [delegate, owner] for DELEGATE_FIRST, and a list of one for the other two. Then return from the first dict that has the name.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then test yourself on the six ideas, from the owner of a closure to @CompileStatic. A session begins with the idea you have answered worst so far, and every choice carries its own reason:

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 What is the delegate of a closure before any code sets it?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: Its owner

    The delegate starts as the owner and is the one reference that code changes, which is how builders work.

  2. Question 2 of 12 In a closure nested inside another closure, inside a method of class Report, what is this?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: The Report object

    this is fixed by where the code is written; owner by where the closure was defined; delegate by code.

  3. Question 3 of 12 Owner: name = 'release'. Delegate: name = 'test', agent = 'linux'. Under DELEGATE_FIRST, what do name and agent give?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: test and linux

    strategy.groovy printed DELEGATE_FIRST name=test agent=linux.

  4. Question 4 of 12 A script sets name = 'release' without def, then runs a builder block whose spec has the property name = 'test'. The builder sets the delegate but keeps OWNER_FIRST. What does name give inside the block?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: release: the owner, the script, is asked first and has name in its binding

    Builders set DELEGATE_FIRST together with the delegate, so that the words of the DSL win over properties and binding variables of the code around the block. A local variable declared with def is captured and wins under every strategy.

  5. Question 5 of 12 Which pieces does a Groovy DSL like the pipeline builder need?

    Choose every answer that is right.

    Show the answer: Question 5 of 12

    Answer:

    • Methods whose last parameter is a closure
    • A spec object to be the delegate of each block
    • DELEGATE_FIRST on each closure

    Methods taking closures, a delegate per block and the right strategy are the whole mechanism.

  6. Question 6 of 12 A third stage, stage('Deploy') { steps { sh './deploy.sh' } }, is added to the Jenkinsfile. How many lines does the call log have then?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: 13

    The log has one line per call received, so it mirrors the nesting of the file exactly.

  7. Question 7 of 12 What is implementation 'org.apache.groovy:groovy:6.0.0' inside dependencies { } in a Gradle build script?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: A call implementation('org.apache.groovy:groovy:6.0.0') on the object that dependencies gives the block

    Every line of a build script is Groovy: calls without parentheses, blocks that are closures, and assignments that are setters.

  8. Question 8 of 12 A colleague adds timeout 10 { sh 'make test' } to a Jenkinsfile, meaning timeout(10) { sh 'make test' }. What does Groovy make of the line?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: timeout(10({ sh('make test') })): it calls 10 with the closure, which fails when the line runs

    Read the line by adding the parentheses Groovy adds: the word, then its argument, and a closure after the argument is given to that argument. Write timeout(10) { … } so that the closure is the last argument of timeout.

  9. Question 9 of 12 Where can @TypeChecked be placed?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: On a class or on a method

    Turn checking on where you want it, class by class or method by method, which lets a code base adopt it in steps.

  10. Question 10 of 12 typo_checked.groovy got two compile errors for one typo, No such property: minuts and Cannot find matching method java.lang.Integer#plus(java.lang.Object). Why two?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: The checker could not type it.minuts, so the += that used it had an argument of unknown type as well

    Fix the first error a checker reports; errors that follow from it often disappear with it.

  11. Question 11 of 12 What does @CompileStatic add to what @TypeChecked does?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: It compiles each call bound to its method, as Java does, instead of dispatching while the program runs

    @TypeChecked checks and leaves the calls dynamic; @CompileStatic checks and binds them at compile time.

  12. Question 12 of 12 A Groovy library adds a method to String through its metaclass at start-up, and a @CompileStatic class calls that method. What happens?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: The class does not compile: the static compiler knows only methods it can see when compiling

    Keep metaprogramming out of statically compiled code, or call it from a dynamic part of the program.

Interview questions

Warm-up (fresher to mid-level): What are the owner and the delegate of a Groovy closure, and why do builders change the delegate?

The owner is the object the closure was defined in: the enclosing class instance, or the enclosing closure when one closure is nested in another. The delegate is a second object the closure can send names to; it starts out as the owner, and any code can set it. When a closure uses a name it does not declare, its resolve strategy says where to look: the default, OWNER_FIRST, asks the owner, then the delegate. A builder such as a Jenkinsfile reader sets the delegate to an object that knows the words of its block, stage or sh, and switches the strategy to DELEGATE_FIRST, so those words win even when the surrounding script has a property with the same name. In the lesson, the same closure read name as release from the owner under OWNER_FIRST and as test from the delegate under DELEGATE_FIRST.

Two longer questions, one about a DSL that read the wrong value and one about static compilation:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A release DSL has blocks like module('api') { artifact "api-${version}.jar" }, and the builder sets each block's delegate to a ModuleSpec with a version property. After someone adds version = '9.9' near the top of the script for a log line, every artifact carries 9.9. What happened, and how do you fix and test the builder?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    A team maintains 40,000 lines of dynamic Groovy: services, a few builders and some metaclass tricks in tests. A lead proposes @CompileStatic everywhere, for speed and safety. How do you evaluate the proposal, and how would you roll it out?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What do this, owner and delegate refer to inside a Groovy closure?

    this: the class around the code. owner: the object or closure in which the closure was defined. delegate: an object anyone may set, which starts out as the owner.

  2. Both the owner and the delegate have a name. Which value does a closure get under the default strategy?

    The owner's: OWNER_FIRST asks it first, so name gave release from the script's binding. Under DELEGATE_FIRST the delegate answered with test.

  3. Why does a builder set resolveStrategy to Closure.DELEGATE_FIRST as well as the delegate?

    So the words of its DSL reach the spec that defines them even when the code around the block has a property or binding variable with the same name.

  4. Which name does no resolve strategy override?

    A local variable declared where the closure is written: the closure captures it rather than looking it up, so it answered local even under DELEGATE_ONLY.

  5. How does a DSL block such as stage('Build') { steps { sh 'make' } } work underneath?

    Each word is a method call whose last argument is a closure. The method makes the spec of the next level the closure's delegate and runs it.

  6. Why is stage 'Deploy' { … } not the same call as stage('Deploy') { … }?

    Without the parentheses Groovy gives the closure to the argument, so 'Deploy' { … } turned into a call of a method named Deploy and failed. Keep the parentheses when a closure follows an argument.

  7. What does @TypeChecked do to a typo such as it.minuts?

    Makes it a compile error, since names are resolved when the code compiles; dynamic Groovy failed only when the line ran. A builder's closure parameter needs @DelegatesTo so the checker knows its delegate.

  8. A method of Pricing is replaced through its metaclass at run time. Which calls see the new method?

    Dynamic calls do, and gave 63. A call compiled with @CompileStatic was bound when the file compiled, so it still gave 42.

Key takeaways

  • A closure keeps this, the class around it, owner, the object or closure it was defined in, and delegate, which starts as the owner; an undeclared name is looked up through them.
  • The resolve strategy decides the order: OWNER_FIRST by default, so builders set DELEGATE_FIRST with the delegate, and the ONLY strategies refuse names from the other side; a captured local variable wins under every strategy.
  • A DSL is methods that take closures and run them against spec objects; a builder of 60 lines read a Jenkinsfile into a call log and a plan.
  • Without parentheses a Jenkinsfile and a build.gradle are still method calls: stage('Build') { … } and dependencies { implementation '…' }, answered by the delegate in force; a closure that follows an argument needs the parentheses, or the argument is called with it.
  • @TypeChecked turns it.minuts into a compile error, @DelegatesTo tells it the delegate of a builder’s closure, and @CompileStatic binds calls at compile time, ignoring methods replaced at run time.

Terms in this lesson

closure (Groovy)
A block of code in braces, such as { b -> b.minutes }, that is an object of class groovy.lang.Closure: it can be stored, passed and called later, and it keeps this, its owner and its delegate for the names it uses.
delegate (of a closure)
An object that a Groovy closure can send the names it does not declare to. It starts as the closure's owner; a builder sets it to the object that knows the words of a block.
resolve strategy
The rule that tells a Groovy closure where to look for a name it does not declare: OWNER_FIRST, the default, DELEGATE_FIRST, OWNER_ONLY, DELEGATE_ONLY or TO_SELF.
static compilation (@CompileStatic)
Compiling Groovy code with the checks of @TypeChecked and every call bound when the code compiles, as in Java. Such code ignores methods that metaprogramming adds or replaces at run time.
type checking (@TypeChecked)
Checking at compile time that every method, property and assignment of the code exists and fits its types. In Groovy it is switched on for a class or a method with @TypeChecked, and the calls stay dynamic.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.