Closure vs Action

I see in the task doc, there are:

Task doLast ( Closure action)

and

Task doLast ( Action <? super Task > action)

What is the difference between Clousre and Action?
I know how to use Closure to define doLast:

task hello {
   doLast {
        println 'Hello Earth'
   }
}

how to do it with Action?

Thanks.

2 Likes

Much of the Gradle core is written in Java. The API contains an Action interface that has its execute method called. This is much easier to implement from other JVM languages. The method that takes a Closure is just a helper for the Groovy DSL. Behind the scenes, it actually creates a ClosureBackedAction and then delegates to the method with the Action.

The full, very verbose version could look like this:

project.task("hello", new Action<Task>() {
    @Override
    public void execute(Task task) {
        task.doLast(new Action<Task>() {
            @Override
            public void execute(Task task) {
                System.out.println("Hello Earth");
            }
        });
    }
});

However, with Java 8, this can be shortened as a lambda due to Action being functional interface:

project.task("hello", task -> {
    task.doLast(task -> {
        System.out.println("Hello Earth");
    });
});

or even:

project.task("hello", task -> task.doLast(t -> System.out.println("Hello Earth")));

If you’re writing a build script using the Groovy DSL, just use the Closure. Otherwise, the Action is much easier from Java, especially in 8+, which you need anyway for recent versions of Gradle.

4 Likes

Extra note; do not use lambdas for task actions (doFirst/doLast). If lambdas are used Gradle cannot properly track the task implementation for up-to-date/caching purposes.

Buried in Authoring Tasks

For tracking the implementation of tasks, task actions and nested inputs, Gradle uses the class name and an identifier for the classpath which contains the implementation. There are some situations when Gradle is not able to track the implementation precisely:

Unknown classloader

When the classloader which loaded the implementation has not been created by Gradle, the classpath cannot be determined.

Java lambda

Java lambda classes are created at runtime with a non-deterministic classname. Therefore, the class name does not identify the implementation of the lambda and changes between different Gradle runs.

When the implementation of a task, task action or a nested input cannot be tracked precisely, Gradle disables any caching for the task. That means that the task will never be up-to-date or loaded from the build cache.

4 Likes

Thanks for @jjustinic and @Chris_Dore, I understand now.

The method that takes a Closure is just a helper for the Groovy DSL. Behind the scenes, it actually creates a ClosureBackedAction and then delegates to the method with the Action .

For curious how ClosureBackedAction is here read:

subprojects/model-core/src/main/java/org/gradle/util/ConfigureUtil.java
subprojects/model-core/src/main/java/org/gradle/internal/instantiation/generator/AsmBackedClassGenerator.java
subprojects/model-core/src/main/java/org/gradle/internal/instantiation/generator/AbstractClassGenerator.java

As Gradle transitioned from Groovy Closure to Action.

Implementations of interfaces with Closure jump to Action with help of ConfigureUtil.configure().

Seems that new API is built around Action only ))

And what we see in built-in JavaPlugin.java?

project.getTasks().withType(Test.class).configureEach(test -> {
  test.getConventionMapping().map("testClassesDirs", () -> sourceSetOf(pluginConvention, SourceSet.TEST_SOURCE_SET_NAME).getOutput().getClassesDirs());
  test.getConventionMapping().map("classpath", () -> sourceSetOf(pluginConvention, SourceSet.TEST_SOURCE_SET_NAME).getRuntimeClasspath());
  test.getModularity().getInferModulePath().convention(javaPluginExtension.getModularity().getInferModulePath());
});

Where is the truth?

Probably it is OK to use Java lambdas to configure tasks from within plugin, Really, plugin is fixed and comes from Gradle distro or from Maven repository. Highly unlikely it will change.

But adding Java lambdas to build.gradle should be disastrous for caching as the Docs states ))

Now the question: how can I add Java lambda in Groovy build.gradle ? Or it is the warning about Kotlin?

Groovy does not support Java 8 style lambdas, that’s what Groovy’s Closure is for.

Yes, using lambdas for configuration is fine. I was referring to task actions, ones added via Task.doFirst or Task.doLast. For example, if you had a plugin that did something like:

class MyPlugin implements Plugin<Project> {
    void apply( Project project ) {
        project.getTasks().getByName( "someTask" ).doLast( t -> t.getLogger().lifecycle( "from my plugin" ) );
    }
}
2 Likes

Now I got it. Thx!

The examples I saw in Gradle plugins itself use Java 8 lambdas during configuration phase.

Your example is clearly showcase task actions for which, as docs warns, Java 8 lambdas might break UPDATE checks. So it only effects doFirst(Action) / doLast(Action) (or more precisely, according to docs: implementation of a task, task action or a nested input).

I tried to find explanation of Java behavior when runtime instantiate lambda but cannot find anything relevant (about lambda class name stability between JVM instances).

Also I wonder how it is possible that there is no problem with Groovy closures. They are also dynamic. Even worse, they usually wrapped around by ClosureBackedAction to jump from Closure → Action. So devs managed to make “persistent” fingerprint for Groovy closures but unable to make such for Java lambdas. And here is Kotlin SAM adding complexity…

1 Like

Groovy closures are compiled to inner classes, who’s name will not change if the code is not modified/recompiled. lambdas are not implemented that way.

Groovy that will always produce the same output, unless of course you modify the code enough that the generated class names change:

Closure x = {
    println "yo"
}
println x.class.simpleName
// __spock_feature_0_0_closure1
// __spock_feature_0_0_closure1

JVM instances can produce different output for this Java:

Runnable x = () -> System.out.println("yo");
System.out.println(x.getClass().getSimpleName());
// LTest$$Lambda$3/26305445
// LTest$$Lambda$3/21184090
2 Likes