Skip to main content

2024-07-31

One-liner

"Unreasonable things aren't always obvious." — "Thriller Paradise" · 2879824237


Here’s the translated content with a conversational tone:


Gradle Closure Example

Let's break down that Java method in your Gradle plugin, written using Groovy DSL. This method takes advantage of Groovy’s Closure and Delegate mechanisms. Let’s dive into each part of this code:

public void forcedTypes(@DelegatesTo(ForcedTypesHandler.class) Closure<?> closure) {
closure.setDelegate(this.forcedTypes);
closure.setResolveStrategy(1);
closure.call();
}

Breaking it Down

  1. Method Signature

    public void forcedTypes(@DelegatesTo(ForcedTypesHandler.class) Closure<?> closure)
    • public void forcedTypes(...): This defines a public method named forcedTypes with no return value.
    • @DelegatesTo(ForcedTypesHandler.class) Closure<?> closure: The parameter is a Groovy Closure, and the @DelegatesTo annotation specifies that the delegate for this closure should be of type ForcedTypesHandler.
  2. Setting the Closure's Delegate

    closure.setDelegate(this.forcedTypes);
    • closure.setDelegate(...): This sets the delegate of the closure to this.forcedTypes, meaning it will delegate method calls to this forcedTypes object.
  3. Setting the Closure's Resolve Strategy

    closure.setResolveStrategy(1);
    • closure.setResolveStrategy(...): This sets the closure's resolve strategy to 1, which corresponds to DELEGATE_FIRST. This means the closure will look for properties and methods on the delegate object first.
  4. Calling the Closure

    closure.call();
    • closure.call(): This executes the closure.

What’s the Point?

This kind of code is typical in Gradle plugins to allow users to configure forcedTypes in a more intuitive way using DSL. When a user calls forcedTypes { ... } in their Gradle script, the closure they pass in is dynamically bound to the forcedTypes object. This allows users to directly configure forcedTypes within the closure, like this:

forcedTypes {
type {
name = 'VARCHAR'
expression = '.*'
}
}

In this example, the configuration inside the forcedTypes closure applies directly to the forcedTypes object, making the setup process simpler.

Adding Dependencies Dynamically in a Gradle Plugin

Here's some code that dynamically adds dependencies in a Gradle plugin. It does this by calling project.getDependencies().addProvider, which adds a dependency to a specific configuration. Let’s break it down:

project.getDependencies().addProvider(
sourceSet.getImplementationConfigurationName(),
jooqExtension.getEdition().map(e -> e.getGroupId() + ":jooq")
.flatMap(ga -> jooqExtension.getVersion().map(v -> ga + ":" + v))
);

Breaking it Down

  1. Getting the Configuration Name

    sourceSet.getImplementationConfigurationName()
    • sourceSet.getImplementationConfigurationName(): This gets the implementation configuration name for the sourceSet. sourceSet typically refers to a source set in the Gradle project (like main, test, etc.), and the implementation configuration name is something like implementation (e.g., mainImplementation).
  2. Building the Dependency String

    jooqExtension.getEdition().map(e -> e.getGroupId() + ":jooq")
    .flatMap(ga -> jooqExtension.getVersion().map(v -> ga + ":" + v))
    • jooqExtension.getEdition(): Gets the Edition property from jooqExtension, which is a Provider object.
    • .map(e -> e.getGroupId() + ":jooq"): Maps the Edition object to a string combining its GroupId and "jooq".
    • .flatMap(ga -> jooqExtension.getVersion().map(v -> ga + ":" + v)): Further maps this to combine it with the Version property, forming a complete dependency string like org.jooq:jooq:3.14.8.
  3. Adding the Dependency

    project.getDependencies().addProvider(...);
    • project.getDependencies().addProvider: This Gradle method adds a dynamically provided dependency to the specified configuration.
    • The first argument is the configuration name obtained from sourceSet.getImplementationConfigurationName().
    • The second argument is the dependency coordinate string, dynamically generated from jooqExtension's Edition and Version properties.

Where Does the Dependency Go?

The dependencies added here are for the project managed by the plugin, not the plugin itself. Specifically:

  • project.getDependencies().addProvider adds the dependency to the specified configuration of the project using the plugin.
  • sourceSet represents the source set of the project using the plugin, so the dependency is added to that project’s implementation configuration.

Wrapping Up the Dependency Add

In summary, this code dynamically gets the Edition and Version properties from jooqExtension, creates the dependency coordinate string, and adds this dependency to the implementation configuration of the source set in the project using the plugin. This way, users can configure their project dependencies flexibly via the jooqExtension without manually adding them in their build scripts.

Gradle Property.finalizeValueOnRead()

In Gradle, the Property interface provides a flexible, type-safe way to handle properties. The finalizeValueOnRead() method is part of the Property interface and controls the lifecycle and mutability of the property value.

Breaking it Down

private final Property<String> version;
private final Property<JooqEdition> edition;
  • These lines define two private, final properties, version and edition. They are of type Property, which is a Gradle interface for handling properties.
version.finalizeValueOnRead();
edition.finalizeValueOnRead();
  • finalizeValueOnRead() is called on these properties. This method fixes the property value upon first read, making it immutable afterward.

What's finalizeValueOnRead() About?

The main job of finalizeValueOnRead() is to control the immutability of a property. Specifically:

  • Fixing the Property Value: Once the property value is read, it’s locked in (finalized). This means after the first read, you can’t change it anymore.
  • Ensuring Consistency: In some cases, you want to ensure that once a property value is used, it remains consistent and doesn’t change. This helps maintain consistent configuration and prevents potential issues from concurrent modifications.
  • Preventing Subsequent Modifications: After calling finalizeValueOnRead(), any attempt to modify the property value after it has been read will throw an exception. This helps catch and prevent unintended changes.

Use Cases

This method is typically used in scenarios like:

  • Lazy Initialization: The property value might be dynamically computed or set during plugin or task execution. Before it’s read, it can be mutable.
  • Configuration Locking: Once configuration values are read, lock them to prevent further changes, ensuring the task or plugin behavior remains predictable.
  • Ensuring Safety: In a multithreaded environment, ensure a property, once read, isn’t modified by other threads, avoiding concurrency issues.

Example

public class MyPluginExtension {
private final Property<String> version;
private final Property<JooqEdition> edition;

@Inject
public MyPluginExtension(ObjectFactory objects) {
this.version = objects.property(String.class);
this.edition = objects.property(JooqEdition.class);

version.finalizeValueOnRead();
edition.finalizeValueOnRead();
}

// Getter and setter methods
public Property<String> getVersion() {
return version;
}

public Property<JooqEdition> getEdition() {
return edition;
}
}

In this example:

  • version and edition are properties whose values are fixed upon first read.
  • version.finalizeValueOnRead(); and edition.finalizeValueOnRead(); ensure that these properties’ values cannot be changed after being read for the first time.

Wrapping It Up

The finalizeValueOnRead() method fixes the property value upon first read to ensure consistency and stability throughout the build process. This mechanism helps prevent unexpected changes in configuration at runtime, making plugin or task behavior predictable and reliable.