Search

Dark theme | Light theme
Showing posts with label Gradle. Show all posts
Showing posts with label Gradle. Show all posts

March 8, 2024

Gradle Goodness: Organizing Tasks Using The Task Container

A Gradle build file describes what is needed to build our Java project. We apply one or more plugins, configure the plugins, declare dependencies and create and configure tasks. We have a lot of freedom to organize the build file as Gradle doesn’t really care. So to create maintainable Gradle build files we need to organize our build files and follow some conventions. In this post we focus on organizing the tasks and see if we can find a good way to do this.

It is good to have a single place where all the tasks are created and configured, instead of having all the logic scattered all over the build file. The TaskContainer is a good place to put all the tasks. To access the TaskContainer we can use the tasks property on the Project object. Within the scope of the tasks block we can create and configure tasks. Now we have a single place where all the tasks are created and configured. This makes it easier to find the tasks in our project as we have a single place to look for the tasks.

Within the scope of the TaskContainer we can use a convention to put the task creation methods at the top of the TaskContainer block. And the task configuration methods are after the task creation in the TaskContainer block. The tasks that are created at the top of the TaskContainer scope can be referenced by configuration code for tasks later in the TaskContainer scope.

The following diagram shows the build file structure and an example of the implementation:

In the example Gradle build file for a Java project we organize the tasks in the TaskContainer using this convention:

plugins {
    java
}
...
tasks {
    // ----------------------------------------------
    // Task creation at the top of the container.
    // ----------------------------------------------

    // Register new task "uberJar".
    val uberJar by registering(Jar::class) {
        archiveClassifier = "uber"

        from(sourceSets.main.get().output)

        dependsOn(configurations.runtimeClasspath)
        from({
            configurations.runtimeClasspath.get()
                .filter { it.name.endsWith("jar") }
                .map { zipTree(it) }
        })
    }

    // ----------------------------------------------
    // Task configuration after task creation.
    // ----------------------------------------------

    // The output of the "uberJar" tasks is part of
    // the output of the "assemble" task.
    // We can refer to the "assemble" task directly
    // as it is added by the Java plugin.
    assemble {
        // We can refer to the task name that
        // we just created in our
        // tasks configuration block.
        dependsOn(uberJar)
    }

    // Configure tasks with type JavaCompile.
    withType<JavaCompile>().configureEach {
        options.compilerArgs.add("--enable-preview")
    }
}
...

Although Gradle doesn’t enforce us to use this convention it can be very helpful as build file authors to use it as it makes it easier to find the tasks in the project.

Written with Gradle 8.6.

February 25, 2024

Gradle Goodness: Using System Properties Lazily

It is good practice in Gradle to use lazy configuration. This makes builds faster as only configuration values are evaluated when needed. We should try to not let Gradle spend time on evaluating configuration values that will not be used. For example tasks that are not executed could still be configured by Gradle. If we make sure the configuration of these tasks is lazy we can save time.

Gradle gives us a lazy way to get the value of a Java system property. In our build script we can use the providers property of type ProviderFactory and the method systemProperty(String). This method returns a Provider<String> instance that can be used to get the value of a system property in a lazy way. The method systemProperty can also be used with a Provider<String> argument.

In the following example we register a task that prints the value of the Java system property user.name to the console. We use lazy configuration to make sure the value of the system property is only fetched when the task is executed.

tasks {
    register<PrintSystemProperty>("printSystemProperty") {
        // We can use providers.systemProperty(String)
        // to get the value of an Java system property
        // in a lazy way.
        // The argument can also be a Provider<String> type.
        // So at this point the value is not fetched yet,
        // only when the task is executed the actual value
        // of the system property "user.name" is fetched.
        systemProperty = providers.systemProperty("user.name")
    }
}

// Simple task to print the value of a Java system property.
abstract class PrintSystemProperty : DefaultTask() {
    @get:Input
    abstract val systemProperty: Property<String> // Use lazy property.

    @TaskAction
    fun printSystemPropertyValue() {
        // Only here we actually will get the value
        // for the system property.
        logger.quiet(systemProperty.get())
    }
}
$ ./gradlew printSystemProperty

> Task :printSystemProperty
mrhaki

BUILD SUCCESSFUL in 685ms
2 actionable tasks: 2 executed

Written with Gradle 8.6.

Gradle Goodness: Using Environment Variables Lazily

It is good practice in Gradle to use lazy configuration. This makes builds faster as only configuration values are evaluated when needed. We should try to not let Gradle spend time on evaluating configuration values that will not be used. For example tasks that are not executed could still be configured by Gradle. If we make sure the configuration of these tasks is lazy we can save time.

Gradle gives us a lazy way to get the value of an environment variable. In our build script we can use the providers property of type ProviderFactory and the method environmentVariable(String). This method returns a Provider<String> instance that can be used to get the value of an environment variable in a lazy way.

In the following example we register a task that prints the value of the environment variable USER. We use lazy configuration to make sure the value of the environment variable is only fetched when the task is executed.

tasks {
    register<PrintEnvironmentVariable>("printEnvironmentVariable") {
        // We can use providers.environmentVariable(String)
        // to get the value of an environment variable
        // in a lazy way.
        // The argument can also be a Provider<String> type.
        // So at this point the value is not fetched yet,
        // only when the task is executed the actual value
        // of the environment variable "USER" is fetched.
        environmentVariable = providers.environmentVariable("USER")
    }
}

// Simple task to print the value of an environment variable.
abstract class PrintEnvironmentVariable : DefaultTask() {
    @get:Input
    abstract val environmentVariable: Property<String> // Use lazy property.

    @TaskAction
    fun printEnvironmentVariable() {
        // Only here we actually will get the value
        // for the environment variable.
        logger.quiet(environmentVariable.get())
    }
}

When we execute the tasks we see the value of the environment variable USER:

$ ./gradlew printEnvironmentVariable

> Task :printEnvironmentVariable
mrhaki

BUILD SUCCESSFUL in 599ms
2 actionable tasks: 2 executed

Written with Gradle 8.6.

February 5, 2024

Gradle Goodness: Continuous Testing For Java Projects

The command line option --continuous or the short version -t enables Gradle’s continous build. For a continuous build Gradle will keep on running and will re-execute the tasks we invoked if the input or of the input of one of the depended tasks has changed. For a project with the java plugin we can use this option for the test task. Gradle will run the test task and after the task has been executed Gradle will wait for any changes in the input of the task. This means if we change our Java test code in src/test/java and save the source file Gradle will re-execute the test task and show the output. But also if the input of other tasks changes, that the test task depends on, the test is re-executed. So also changes in source files in our src/main/java directory will trigger a re-execute of the test task, because the test task depends on the compileJava task, and the compileJava task has the src/main/java directory as input.

In the following example output we invoked the test task with the --continuous option. On the first run there was an assertion failure. We fixed the code in src/main/java and saved the file. Without having to restart Gradle we see that our assertion succeeded on the second test task run.

$ ./gradlew --continuous test

> Task :app:test FAILED

AppTest > application has a greeting FAILED
    Condition not satisfied:

    result == "Hello World!!"
    |      |
    |      false
    |      2 differences (84% similarity)
    |      Hello (w)orld!(-)
    |      Hello (W)orld!(!)
    Hello world!
        at org.example.AppTest.application has a greeting(AppTest.groovy:17)

1 test completed, 1 failed

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:test'.
> There were failing tests. See the report at: file:///Users/mrhaki/Projects/mrhaki.com/java/app/build/reports/tests/test/index.html

* Try:
> Run with --scan to get full insights.

BUILD FAILED in 3s
3 actionable tasks: 1 executed, 2 up-to-date

Waiting for changes to input files... (ctrl-d to exit)
modified: /Users/mrhaki/Projects/mrhaki.com/java/app/src/main/java/org/example/App.java
Change detected, executing build...


BUILD SUCCESSFUL in 4s
3 actionable tasks: 3 executed

Waiting for changes to input files... (ctrl-d to exit)
<=============> 100% EXECUTING [53s]
> IDLE
> IDLE

To stop continuous builds we press Ctrl+C.

To have some nice output we want to use the full exception format for our test logging. In the following example build file we configure this for our test task in the testLogging block:

// File: build.gradle.kts
...
testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            targets {
                all {
                    testTask.configure {
                        testLogging {
                            exceptionFormat = TestExceptionFormat.FULL
                        }
                    }
                }
            }
        }
    }
}
...

Now we can run the test task with the --continuous option and work on our source files and tests in our IDE. On each save of a source file the test task is executed again and we can immediately see the output of the test run.

Written with Gradle 8.6

February 3, 2024

Gradle Goodness: Java Toolchain Configuration Using User Defined Java Locations

With the java plugin we can configure a so-called Java toolchain. The toolchain configuration is used to define which Java version needs to be used to compile and test our code in our project. The location of the Java version can be determined by Gradle automatically. Gradle will look at known locations based on the operating system, package managers, IntellIJ IDEA installations and Maven Toolchain configuration.

But we can also define the locations of our Java installations ourselves using the project property org.gradle.java.installations.paths. We provide the paths to the local Java installations as a comma separated list as value for this property. When we set this property we can also disable the Gradle toolchain detection mechanism, so only the Java installations we have defined ourselves are used. To disable the automatic detection we set the property org.gradle.java.installations.auto-detect to false. If we leave the value to the default value true, then the locations we set via org.gradle.java.installations.paths are added to the Java installations already found by Gradle.

The property org.gradle.java.installations.paths is a project property we can set via the command line, but we can also set it in the gradle.properties file in our GRADLE_USER_HOME directory. Then the values we define will be used by all Gradle builds on our machine.

In the following example gradle.properties file we define the locations of two Java installations and also disable the automatic detection of Java installations. We store this file in our GRADLE_USER_HOME directory.

# File: $GRADLE_USER_HOME/gradle.properties
# We define the locations of two Java installations on our computer.
org.gradle.java.installations.paths=C:/Users/mrhaki/tools/apps/zulu11-jdk/current,C:/Users/mrhaki/tools/apps/zulu17-jdk/current

# We disable the automatic detection of Java installations by Gradle.
org.gradle.java.installations.auto-detect=false

# We also disable the automatic download of Java installations by Gradle.
org.gradle.java.installations.auto-download=false

We add the java plugin and configure our toolchain with the following Gradle build script:

// File: build.gradle.kts
plugins {
    java
}

java {
    toolchain {
        // We want to use Java 17 to compile, test and run our code.
        // Now it doesn't matter which Java version is used by Gradle itself.
        languageVersion = languageVersion.set(JavaLanguageVersion.of(17))
    }
}

We run the javaToolchains task to see the Java toolchain configuration:

$ ./gradlew javaToolchains

> Task :javaToolchains

 + Options
     | Auto-detection:     Disabled
     | Auto-download:      Disabled

 + Azul Zulu JDK 11.0.22+7-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu11-jdk\current
     | Language Version:   11
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Gradle property 'org.gradle.java.installations.paths'

 + Azul Zulu JDK 17.0.10+7-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu17-jdk\current
     | Language Version:   17
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Gradle property 'org.gradle.java.installations.paths'


BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

In the generated output we can see that Gradle detected the two Java installations we defined in the gradle.properties file using the Gradle property org.gradle.java.installations.paths.

Written with Gradle 8.5.

February 2, 2024

Gradle Goodness: Using Maven Toolchains Configuration For Gradle Java Toolchain Resolution

When we apply the Java plugin to our Gradle project we can configure which Java version we want to use for compiling our source code and running our tests using a toolchain configuration. The benefit of having a toolchain configuration is that we can use a different Java version for compiling and running our code than the Java version that is used by Gradle to execute the build. Gradle will look for that Java version on our local computer or download the correct version if it is not available. To search for a local Java installation Gradle will look for operating system specific locations, installations by package managers like SKDMAN! and Jabba, IntelliJ IDEA installations and Maven Toolchain specifications. Maven Toolchain specifications is an XML file describing the location of local Java installation. Each Java installation is described by a version and optional vendor it provides and the location of the installation. Maven uses this information to find the correct Java installation when the maven-toolchain-plugin is used in a Maven project. But Gradle can also utilize Maven Toolchain specifications to find local Java installations. This can be useful when we have to work on multiple projects where some use Maven and others use Gradle. We can place the Maven Toolchain specification file in our Maven home directory. This is also the default place where Gradle will look, but we can use a project property to override this location.

The following example shows a Maven toolchain configuration with three different Java versions:

<?xml version="1.0" encoding="UTF-8"?>
<toolchains>
	<toolchain>
		<type>jdk</type>
		<provides>
			<version>11</version>
			<vendor>Azul Zulu</vendor>
		</provides>
		<configuration>
			<jdkHome>C:/Users/mrhaki/tools/apps/zulu11-jdk/current</jdkHome>
		</configuration>
	</toolchain>
	<toolchain>
		<type>jdk</type>
		<provides>
			<version>17</version>
			<vendor>Azul Zulu</vendor>
		</provides>
		<configuration>
			<jdkHome>C:/Users/mrhaki/apps/zulu17-jdk/current</jdkHome>
		</configuration>
	</toolchain>
	<toolchain>
		<type>jdk</type>
		<provides>
			<version>21</version>
			<vendor>Azul Zulu</vendor>
		</provides>
		<configuration>
			<jdkHome>C:/Users/mrhaki/tools/apps/zulu-jdk/current</jdkHome>
		</configuration>
	</toolchain>
</toolchains>

In our Gradle build file we apply the java plugin and define in the toolchain configuration we want to use Java 17 for our builds:

// File: build.gradle.kts
plugins {
    java
}

java {
    toolchain {
        // Use Java 17 for building and running tests
        languageVersion = JavaLanguageVersion.of(17)
    }
}

We can now use the javaToolchains task to see the available Java installations:

$ ./gradlew javaToolchains

> Task :javaToolchains

 + Options
     | Auto-detection:     Enabled
     | Auto-download:      Enabled

  + Azul Zulu JDK 11.0.22+7-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu11-jdk\current
     | Language Version:   11
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Maven Toolchains

 + Azul Zulu JDK 17.0.10+7-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu17-jdk\current
     | Language Version:   17
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Current JVM

 + Azul Zulu JDK 21.0.2+13-LTS
     | Location:           C:\Users\mrhaki\tools\apps\zulu-jdk\current
     | Language Version:   21
     | Vendor:             Azul Zulu
     | Architecture:       amd64
     | Is JDK:             true
     | Detected by:        Maven Toolchains

BUILD SUCCESSFUL in 4s
1 actionable task: 1 executed

The command was run using Java 17 and we can see in the output that it is detected by Gradle as the current JVM. The Java installations for Java 11 and Java 21 are detected using Maven Toolchains.

If the location of the Maven Toolchain specification file is not in the default location, we can use the Gradle project property org.gradle.java.installations.maven-toolchain-file to specify a custom location. We can use it from the command line using the -P option or we can add it to gradle.properties in the project root directory.

$ ./gradlew javaToolchains -Porg.gradle.java.installations.maven-toolchain-file=C:/Users/mrhaki/tools/maven/toolchains.xml

...

Written with Gradle 8.5.

March 26, 2023

Gradle Goodness: Publish Version Catalog For Sharing Between Projects

A version catalog in Gradle is a central place in our project where we can define dependency references with their version or version rules. We can define a version catalog using an API in our build file, but we can also create an external file where we define our dependencies and version. In our dependencies section we can refer to the names in the version catalog using a type-safe accessor (if we use Kotlin for writing our build script) with code completion in a supported IDE (IntelliJ IDEA). If we want to share a version catalog between projects we can publish a version catalog to a Maven repository with a groupId, artifactId and version.

In order to do this we need to create a new Gradle project that will only contain the definitions for our version catalog. We must add two gradle plugins: version-catalog and maven-publish. The version-catalog plugin adds a new extension versionCatalog to our build file. Here we define the content of the version catalog we want to share. We can refer to an external version catalog file written in the TOML format that is dictated by Gradle. But we can also use an API provided by VersionCatalogBuilder to define our versions, plugins, libraries and bundles. The publication outcome of this project is a Maven POM file and generated version catalog file in TOML format. Using the maven-publish plugin we can publish our version catalog to a Maven repository. Other projects can then refer to this published version catalog in their Gradle settings file. In the build script we can use the dependencies using the type-safe accessor we alread know for a project version catalog.

In the following example we first look at the Gradle project that has all the data to publish a version catalog. We first create an external version catalog file:

# File: gradle/libs.versions.toml
[versions]
junit5 = "5.9.1"

[libraries]
junit-api = {
    module = "org.junit.jupiter:junit-jupiter-api",
    version.ref = "junit5"
}
junit-engine = {
    module = "org.junit.jupiter:junit-jupiter-engine",
    version.ref = "junit5"
}

helidon-deps = "io.helidon:helidon-dependencies:3.2.0"

We define the rootProject name as this will be the artifact identifier of the version catalog we will publish:

// File: settings.gradle.kts
rootProject.name = "version-catalog"

Finally we have a build file where we define the version catalog and Maven publish plugins. Furthermore we configure our version catalog with data from the external file and we use the API. Lastly we define the publishing configuration:

// File: build.gradle.kts
plugins {
    // Version catalog plugin will add the catalog extension
    // to our build file where we can define the version
    // catalog contents.
    `version-catalog`

    // Maven publish plugin so we can publish our version catalog
    // to a Maven repository.
    `maven-publish`
}

group = "mrhaki.shared"
version = "1.1.0"

// catalog extension added by version-catalog plugin.
catalog {
    versionCatalog {
        // We can refer to an external version catalog file.
        // This could even be a published version catalog as well.
        from(files("gradle/libs.versions.toml"))

        // But also use the methods of the VersionCatalogBuilder.
        library("helidon-deps", "io.helidon:helidon-dependencies:3.2.0")
    }
}

publishing {
    publications {
        create<MavenPublication>("maven") {
            // The version-catalog plugin adds a new component
            // "versionCatalog" that we can use a publication.
            // We will get a POM file and a generated version catalog
            // TOML file that are part of the publication.
            from(components["versionCatalog"])
        }
    }

    repositories {
        // Configuration for Maven repo to publish our version catalog to.
        maven {
            url = uri("https://intranet.repo/repository/maven-releases")

            credentials {
                val mavenRepoUsername: String by project
                val mavenRepoPassword: String by project
                username = mavenRepoUsername
                password = mavenRepoPassword
            }
        }
    }
}

Now we create the Gradle project build files that will use our published version catalog. First we must refer to the published version catalog in our settings file. We define the Maven repository where we published the catalog to and then refer to the artifact to assign it to the sharedLibs version catalog accessor:

// File: settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        // Configuration for Maven repo to get our
        // published version catalog from.
        maven {
            url = uri("https://intranet.repo/repository/maven-public")

            credentials {
                val mavenRepoUsername: String by settings
                val mavenRepoPassword: String by settings
                username = mavenRepoUsername
                password = mavenRepoPassword
            }
        }
    }

    versionCatalogs {
        // We create a new version catalog with the
        // given name sharedLibs.
        // We are free to use any name, Gradle will
        // create Kotlin accessors we can use in our build file.
        create("sharedLibs") {
            from("mrhaki.shared:version-catalog:1.1.0")
        }
    }
}

Now we are all setup and in our build file we can use the type-safe accessors to get the dependencies in our project dependencies section:

// File: build.gradle.kts
plugins {
    java // We want to craete a Java Helidon app.
}

repositories {
    // Repository for downloading the dependencies.
    mavenCentral()
}

dependencies {
    // We can reference sharedLibs.helidon.deps
    // from our shared version catalog.
    implementation(platform(sharedLibs.helidon.deps))

    // List of dependencies for Helidon where the version
    // can be left out as we use platform(libs.helidon.deps)
    // to include our Bill of Materials (BOM).
    implementation("io.helidon.webserver:helidon-webserver")
    implementation("io.helidon.config:helidon-config-yaml")
    implementation("io.helidon.media:helidon-media-jsonp")

    // We can reference sharedLibs.junit.api and
    // sharedLibs.junit.engine from our shared version catalog.
    testImplementation(sharedLibs.junit.api)
    testImplementation(sharedLibs.junit.engine)

    testImplementation("io.helidon.webclient:helidon-webclient")
}

Written with Gradle 8.0.2

December 5, 2022

Gradle Goodness: Configure Test Task With JVM Test Suite

The JVM Test Suite plugin is part of the Java plugin and provides a nice way to configure multiple test types in our build file. Even if we don't have multiple test types we have a default test type, which is used when we run the Gradle test task. Using the test suite DSL we can configure the task of type Test that belongs to a test suite type. The current release of the JVM Test Suite plugin provides a single target for a test suite type with a single Test task. This will probably change in future releases of the plugin so more task of type Test can be created and configured.

We can reference the Test task using the syntax within a JvmTestSuite configuration block:

...
targets {
    all {
        testTask
    }
}
...

Once we have the reference to the Test task we can configure it using all the methods and properties available for this class.

In the following example build script we configure the logging and set a system property for our default Test task:

plugins {
    java
}
    
repositories {
    mavenCentral()
}

testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            useJUnitJupiter()  // We want to use Jupiter engine
            
            targets {
                all {
                    // Here can access the test task for this 
                    // test suite type (we use the default in this example).
                    // The task can be referenced as testTask.
                    // The task is of type Test and we can use all methods
                    // and properties of the Test class.
                    testTask.configure {
                        // We define a system property with key greeting
                        // and value Hello, which can be used in our test code.
                        systemProperties(mapOf("greeting" to "Hello"))
                        
                        // We configure the logging for our tests.
                        testLogging {
                            exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
                            showStandardStreams = true
                        }
                    }
                }
            }
        }
    }
}

Written with Gradle 7.6.

December 2, 2022

Gradle Goodness: Set Project Version In Version Catalog

The version catalog in Gradle is very useful to have one place in our project to define our project and plugin dependencies with their versions. But we can also use it to define our project version and then refer to that version from the version catalog in our build script file. That way the version catalog is our one place to look for everything related to a version. In the version catalog we have a versions section and there we can define a key with a version value. The name of the key could be our project or application name for example. We can use type safe accessors generated by Gradle in our build script to refer to that version.

In the following example build script written with Kotlin we see how we can refer to the version from the version catalog:

// File: build.gradle.kts
description = "Sample project for Gradle version catalog"
    
// Set version using version catalog.
version = libs.versions.app.version.get()
    
// We can use the TaskContainer to keep all
// task related things in one place.
tasks {
    // Register a new task to print out the project version.
    register("projectVersion") {
        doLast {
            println("Project version: " + version)
        }
    }
}

And the version catalog is defined in the following file:

# File: gradle/libs.versions.toml
[versions]
app-version = "2.0.1"

When we run the task projectVersion we see our project version in the output:

$ gradle projectVersion

> Task :projectVersion
Project version: 2.0.1

BUILD SUCCESSFUL in 831ms
1 actionable task: 1 executed
$

Written with Gradle 7.6.

November 27, 2022

Gradle Goodness: Add Extra Dependencies For Running Tests Using JVM Test Suite Plugin

The JVM Test Suite plugin adds an extension to our build that allows us to configure test tasks. We always can access the default test task and for example specify the test framework we want to use. Gradle will then automatically add the dependencies of that test framework to the testImplementation configuration. If we want to add more dependencies to the testImplementation configuration we don’t have to do that by explicitly mentioning the testImplementation configuration. Instead we can also use a dependencies block from within the JvmTestSuite extension. Any extra dependencies we need to run our tests can be added using the configuration names without a test prefix. Gradle will automatically add them to the correct test configuration for us so the dependencies are available when we compile and run our tests. This will also work for any other new test type we add to the test suites, e.g. for integration tests.

In the following example Gradle build file we configure two extra dependencies for our test related tasks. We want to use AssertJ and Datafaker in our tests so we add them as dependencies:

plugins {
    java
}

repositories {
    mavenCentral()
}

// Using JVM Test Suite feature to configure our test task.
testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            // We set the Junit version explicit using the version catalog.
            useJUnitJupiter(libs.versions.junit)
            dependencies {
                // We add extra dependencies needed for our tests.
                // We don't use the test prefix here as we are already
                // in a "test" context.
                // Other configurations like annotationProcessor, compileOnly
                // can be used here as well.
                // Here we refer to dependencies using the version catalog,
                // but we could use string values for example.
                implementation(libs.assertj.core)
                implementation(libs.datafaker)
            }
        }
    }
}

The version catalog looks as follows:

# File: gradle/libs.versions.toml
[versions]
junit = "5.9.1"

[libraries]
assertj-core = "org.assertj:assertj-core:3.23.1"
datafaker = "net.datafaker:datafaker:1.6.0"

We can use the dependencies task to see what our test runtime classpath looks like. And we see all our newly defined dependencies as well:

$ gw dependencies --configuration testRuntimeClasspath

> Task :dependencies

------------------------------------------------------------
Root project 'testsuite'
------------------------------------------------------------

testRuntimeClasspath - Runtime classpath of source set 'test'.
+--- org.junit.jupiter:junit-jupiter:5.9.1
|    +--- org.junit:junit-bom:5.9.1
|    |    +--- org.junit.jupiter:junit-jupiter:5.9.1 (c)
|    |    +--- org.junit.jupiter:junit-jupiter-api:5.9.1 (c)
|    |    +--- org.junit.jupiter:junit-jupiter-engine:5.9.1 (c)
|    |    +--- org.junit.jupiter:junit-jupiter-params:5.9.1 (c)
|    |    +--- org.junit.platform:junit-platform-commons:1.9.1 (c)
|    |    \--- org.junit.platform:junit-platform-engine:1.9.1 (c)
|    +--- org.junit.jupiter:junit-jupiter-api:5.9.1
|    |    +--- org.junit:junit-bom:5.9.1 (*)
|    |    +--- org.opentest4j:opentest4j:1.2.0
|    |    \--- org.junit.platform:junit-platform-commons:1.9.1
|    |         \--- org.junit:junit-bom:5.9.1 (*)
|    +--- org.junit.jupiter:junit-jupiter-params:5.9.1
|    |    +--- org.junit:junit-bom:5.9.1 (*)
|    |    \--- org.junit.jupiter:junit-jupiter-api:5.9.1 (*)
|    \--- org.junit.jupiter:junit-jupiter-engine:5.9.1
|         +--- org.junit:junit-bom:5.9.1 (*)
|         +--- org.junit.platform:junit-platform-engine:1.9.1
|         |    +--- org.junit:junit-bom:5.9.1 (*)
|         |    +--- org.opentest4j:opentest4j:1.2.0
|         |    \--- org.junit.platform:junit-platform-commons:1.9.1 (*)
|         \--- org.junit.jupiter:junit-jupiter-api:5.9.1 (*)
+--- org.assertj:assertj-core:3.23.1
|    \--- net.bytebuddy:byte-buddy:1.12.10
\--- net.datafaker:datafaker:1.6.0
     \--- com.github.mifmif:generex:1.0.2
          \--- dk.brics.automaton:automaton:1.11-8

(c) - dependency constraint
(*) - dependencies omitted (listed previously)

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 709ms
1 actionable task: 1 executed
$

Written with Gradle 7.6.

November 22, 2022

Gradle Goodness: Using Spock With JVM Test Suite Plugin

Spock is an awesome test framework for testing our Java or Groovy code. Spock itself is written with Groovy and provides a nice syntax to define our tests, or specifications in Spock terminology. To configure support for using Spock in our Gradle build is very easy with the JVM Test Suite plugin (included with the Java plugin). The plugin gives us a nice syntax to define different types of tests, for example integration tests, with their own source set, dependencies and configuration. To use Spock as testing framework we only have to use the method useSpock within a test configuration. The default version of Spock that is used is 2.1-groovy-3.0 when we use Gradle 7.6. If we want to use another version we can use a String parameter when we use the useSpock method with the version we want to use.

In the following example we use the default Spock version defined by Gradle 7.6:

// File: build.gradle.kts 
plugins {
    // We need the Groovy plugin to run our
    // Spock specifications. 
    // As it includes the Java plugin it also
    // includes the JVM Test Suite plugin.
    groovy 
}

repositories {
    // We need a repository with the Spock dependencies.
    mavenCentral()
}

testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            // Define we want to use Spock.
            useSpock()
        }
    }
}    

When we check the dependencies in the testCompileClasspath configuration we see the following output:

$ gradle dependencies --configuration testCompileClasspath
...
> Task :dependencies

------------------------------------------------------------
Root project 'spock-testsuite' - Sample project for using Spock
------------------------------------------------------------

testCompileClasspath - Compile classpath for source set 'test'.
\--- org.spockframework:spock-core:2.1-groovy-3.0
        +--- org.codehaus.groovy:groovy:3.0.9
        +--- org.junit:junit-bom:5.8.1
        |    +--- org.junit.platform:junit-platform-engine:1.8.1 (c)
        |    \--- org.junit.platform:junit-platform-commons:1.8.1 (c)
        +--- org.junit.platform:junit-platform-engine -> 1.8.1
        |    +--- org.junit:junit-bom:5.8.1 (*)
        |    +--- org.opentest4j:opentest4j:1.2.0
        |    +--- org.junit.platform:junit-platform-commons:1.8.1
        |    |    +--- org.junit:junit-bom:5.8.1 (*)
        |    |    \--- org.apiguardian:apiguardian-api:1.1.2
        |    \--- org.apiguardian:apiguardian-api:1.1.2
        \--- org.hamcrest:hamcrest:2.2

(c) - dependency constraint
(*) - dependencies omitted (listed previously)
...
$

We can also specify another version we want to use. In the following example we refer to a version in the version catalog when we use the useSpock method:

# File: gradle/libs.versions.toml
[versions]
# We define the Spock version in the version catalog.
spock = "2.3-groovy-4.0"
// File: build.gradle.kts 
plugins {
    // We need the Groovy plugin to run our
    // Spock specifications. 
    // As it includes the Java plugin it also
    // includes the JVM Test Suite plugin.
    groovy 
}

repositories {
    // We need a repository with the Spock dependencies.
    mavenCentral()
}

testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            // Define we want to use Spock
            // and specify a non-default version 
            // using version catalog.
            useSpock(libs.versions.spock)
        }
    }
}    

Let's check the testCompileClasspath configuration again and we see this time our Spock version is 2.3-groovy-4:

$ gradle dependencies --configuration testCompileClasspath
...
> Task :dependencies

------------------------------------------------------------
Root project 'spock-testsuite' - Sample project for using Spock
------------------------------------------------------------

testCompileClasspath - Compile classpath for source set 'test'.
\--- org.spockframework:spock-core:2.3-groovy-4.0
        +--- org.apache.groovy:groovy:4.0.4
        |    \--- org.apache.groovy:groovy-bom:4.0.4
        |         \--- org.apache.groovy:groovy:4.0.4 (c)
        +--- org.junit:junit-bom:5.9.0
        |    +--- org.junit.platform:junit-platform-engine:1.9.0 (c)
        |    \--- org.junit.platform:junit-platform-commons:1.9.0 (c)
        +--- org.junit.platform:junit-platform-engine -> 1.9.0
        |    +--- org.junit:junit-bom:5.9.0 (*)
        |    +--- org.opentest4j:opentest4j:1.2.0
        |    +--- org.junit.platform:junit-platform-commons:1.9.0
        |    |    +--- org.junit:junit-bom:5.9.0 (*)
        |    |    \--- org.apiguardian:apiguardian-api:1.1.2
        |    \--- org.apiguardian:apiguardian-api:1.1.2
        \--- org.hamcrest:hamcrest:2.2

(c) - dependency constraint
(*) - dependencies omitted (listed previously)
...
$

Written with Gradle 7.6.

November 21, 2022

Gradle Goodness: Set Test Framework Version Using Version Catalog With JVM Test Suite Plugin

Since Gradle 7.3 we can use the JVM Test Suite plugin to define in a declarative way tests for our build. For example adding integration tests with a new source set and dependencies becomes easier with this plugin. The plugin is automatically part of the Java plugin so we don't have to define it explicitly in our build. Configuring the default test task can also be done using the syntax of the JVM TestSuite plugin. We can use several methods from the JvmTestSuite class in our configuration. For example if we want to use Spock as testing framework we can simply add the method useSpock in our build script. Or if we want to use the JUnit 5 Jupiter engine we can use useJUnitJupiter. These methods will add dependencies in the testImplementation configuration. There is a default version for the dependencies if we use the method without arguments. But we can also define the version as String argument for these methods. The version catalog for our project is the place to store version for our dependencies, so it would be nice if we could use the version defined in our version catalog as argument for the use<TestFramework> methods. We can reference the version very simple by using libs.versions.<version-key>. This will return the value we defined as version in our version catalog.

In the following example build script we want to use JUnit Jupiter version 5.9.1, instead of the default version of 5.8.2. We use the version as it is defined in our version catalog file libs.versions.toml:

// File: build.gradle.kts
plugins {
    java // The Java plugin also includes the JVM Test Suite plugin.
}

// We need to define a repository so JUnit dependencies can be downloaded.
repositories {
    mavenCentral()
}

testing {
    suites {
        // Using JVM Test Suite feature to configure our test task.
        val test by getting(JvmTestSuite::class) {
            // For JUnit 5 we need to enable JUnit Jupiter.
            // If we don't specify a version the default
            // version is used, which is 5.8.2 with Gradle 7.6.
            // We can use a version as String as argument, but it is even
            // better to refer to a version from the version catalog,
            // so all versions for our dependencies are at the
            // single location of the version catalog.
            // We define the version in libs.versions.toml.
            useJUnitJupiter(libs.versions.junit)
        }
    }
}

In the file gradle/libs.versions.toml we have the following definition:

# gradle/libs.versions.toml
[versions]
junit = "5.9.1"    

When we check our dependencies we see the correct dependencies for JUnit:

$ gradle dependencies --configuration testCompileClasspath
...
------------------------------------------------------------
Root project 'testsuite'
------------------------------------------------------------

testCompileClasspath - Compile classpath for source set 'test'.
\--- org.junit.jupiter:junit-jupiter:5.9.1
     +--- org.junit:junit-bom:5.9.1
     |    +--- org.junit.jupiter:junit-jupiter:5.9.1 (c)
     |    +--- org.junit.jupiter:junit-jupiter-api:5.9.1 (c)
     |    +--- org.junit.jupiter:junit-jupiter-params:5.9.1 (c)
     |    \--- org.junit.platform:junit-platform-commons:1.9.1 (c)
     +--- org.junit.jupiter:junit-jupiter-api:5.9.1
     |    +--- org.junit:junit-bom:5.9.1 (*)
     |    +--- org.opentest4j:opentest4j:1.2.0
     |    +--- org.junit.platform:junit-platform-commons:1.9.1
     |    |    +--- org.junit:junit-bom:5.9.1 (*)
     |    |    \--- org.apiguardian:apiguardian-api:1.1.2
     |    \--- org.apiguardian:apiguardian-api:1.1.2
     \--- org.junit.jupiter:junit-jupiter-params:5.9.1
          +--- org.junit:junit-bom:5.9.1 (*)
          +--- org.junit.jupiter:junit-jupiter-api:5.9.1 (*)
          \--- org.apiguardian:apiguardian-api:1.1.2

(c) - dependency constraint
(*) - dependencies omitted (listed previously)
...

Written with Gradle 7.6.

November 13, 2022

Gradle Goodness: Grouping Version Catalog Dependencies Into Bundles

The version catalog in Gradle is very useful to define a list of dependencies in one single place. In our build script we references dependencies from the version catalog using type safe accessors when we define a dependency for a configuration. Sometimes multiple dependencies belong to each other and are used in combination with each other. In the version catalog we can define bundles of such dependency groups. Instead of referencing each dependency individually we can reference a bundle from the version catalog in our build script. This keeps our build script cleaner and updating a bundle only needs a change in the version catalog.

In the following example version catalog we have several Log4j2 dependencies. We create two bundles that each contain a set of the dependencies for Log4j2:

# File: gradle/libs.versions.toml
[versions]
# We define the log4j2 version for our dependencies.
log4j2 = "2.19.0"

[libraries]
# The api and core modules from log4j2 we need in our project.
# We can use version.ref to refer to version defined in the [versions] section.
log4j2-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j2" }
log4j2-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j2" }

# Bridge to have Java Util Logging (JUL) processed by log4j2.
log4j2-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j2" }
# JSON layout following ECS (Elastic Search) standard.
log4j2-ecs-layout = "co.elastic.logging:log4j2-ecs-layout:1.5.0"
# Support async logging for log4j2.
disruptor = "com.lmax:disruptor:3.4.4"

[bundles]
# Bundle with log4j2 dependencies needed to compile.
log4j2-impl = ["log4j2-api", "log4j2-core"]

# Bundle with log4j2 dependencies only needed at runtime.
log4j2-runtime = ["log4j2-jul", "log4j2-ecs-layout", "disruptor"]    

With the version catalog in place we can now reference the bundles from our build script. In the following example build script we use both bundles for the configurations implementation and runtimeOnly:

plugins {
    java
}

repositories {
    mavenCentral()
}

dependencies {
    // Use log4j2-impl bundle from version catalog.
    implementation(libs.bundles.log4j2.impl)

    // Use log4j2-runtime bundle from version catalog.
    runtimeOnly(libs.bundles.log4j2.runtime)
}

Bundles are very useful to define dependencies that are used together. We can reference than multiple dependencies with one bundle reference in our build scripts.

Written with Gradle 7.5.1.

November 8, 2022

Gradle Goodness: Defining Plugin Versions Using Version Catalog

A version catalog in Gradle is a central place in our project where we can define dependency references with their version or version rules. A dependency reference is defined using an identifier with a corresponding dependency definition containing the coordinates of the dependency. Now we can reference the dependency using the identifier in for example a dependency configuration, e.g. implementation(libs.spring.core). If there is a version change we want to apply we only have to make the change in our version catalog. An added bonus is that Gradle generates type safe accessors for the identifier we use in our version catalog, so we can get code completion in our IntelliJ IDEA when we want to reference a dependency from the version catalog.

Besides dependencies we need to build and test our software we can also include definitions for Gradle plugins including their version. Normally we reference a Gradle plugin using the id and version. For example in the following code block we include 4 Gradle plugins of which 3 are identified by an id:

plugins {
	`java` // Default Gradle Java plugin
	
	// Include 3 third-party Gradle plugins
	id("org.springframework.boot") version "2.7.5"
	id("io.spring.dependency-management") version "1.0.15.RELEASE"
	id("org.asciidoctor.jvm.convert") version "3.2.0"
}

We can replace these plugin references with version catalog defined values. First we must create the file gradle/libs.version.toml in our project directory. We might already have such file with definitions for the dependencies we use in our build and tests. Next we must add a section [plugins] where we can define our plugin dependencies. We can use the full power of the version catalog here, the only thing we need to remember is to use the id property of we use the longer notation option. With the shorthand notation we can simply define a string value with the id of the plugin, a colon (:) and the version.

In the following example libs.versions.toml file we defined our 3 third-party plugins using several notations:

# File: gradle/libs.versions.toml
[versions]
# Define version we can use as version.ref in [plugins]
asciidoctor = "3.2.0" 

[plugins]
# We can use shorthand notation with the plugin id and version.
spring-boot = "org.springframework.boot:2.7.5"

# We can use the longer notation option where we set 
# the id and version for the plugin.
spring-dep-mgmt = { id = "io.spring.dependency-management", version = "1.0.15.RELEASE" }

# Here we use the longer notation and version.ref to reference
# the version defined in the [versions] section.
asciidoctor-jvm = { id = "org.asciidoctor.jvm.convert", version.ref = "asciidoctor" }

We only have to change our plugins block in our build file. We use the method alias to reference our version catalog definitions. In IntelliJ IDEA we even get code completion when start typing. The following code shows how we include the plugins:

plugins {
	`java` // Default Gradle Java plugin
	
	// Using alias we can reference the plugin id and version
	// defined in the version catalog.
	// Notice that hyphens (-) used as separator in the identifier
	// are translated into type safe accessors for each subgroup.
	alias(libs.plugins.spring.boot)
	alias(libs.plugins.spring.dep.mgmt)
	alias(libs.plugins.asciidoctor.jvm)
}

The version catalog is a powerful feature of Gradle. It allows to have a single place in our project where we define dependency coordinates and we get type safe accessors methods to have code completion in IntelliJ IDEA.

Written with Gradle 7.5.1.

March 16, 2021

Gradle Goodness: Create Properties File With WriteProperties Task

If we need to create a Java properties file in our build we could create a custom task and use the Properties class to store a file with properties. Instead of writing our custom task we can use the task WriteProperties that is already part of Gradle. Using this task Gradle will not add a timestamp in the comment to the properties file. Also the properties are sorted by name, so we know the properties are always in the same order in the output file. Finally, a fixed line separator is used, which is \n by default, but can be set via the task property lineSeparator.

To define the properties that need to be in the output file we can use the property method for a single property, or we can use properties method with a Map argument to set multiple properties.

In the following build file we define a new task projectProps of task type WriteProperties and we use task output as dependency for the processResources task. This way our new task will be executed, if there are changes, when the processResources task is executed:

// File: build.gradle.kts
plugins {
    java
}

version = "1.0.0"

tasks {
    val projectProps by registering(WriteProperties::class) {
        description = "Write project properties in a file."

        // Set output file to build/project.properties
        outputFile = file("${buildDir}/project.properties")
        // Default encoding is ISO-8559-1, here we change it.
        encoding = "UTF-8"
        // Optionally we can specify the header comment.
        comment = "Version and name of project"

        // Define property.
        property("project.version", project.version)

        // Define properties using a Map.
        properties(mapOf("project.name" to project.name))
    }

    processResources {
        // Depend on output of the task to create properties,
        // so the properties file will be part of the Java resources.
        from(projectProps)
    }
}

When we invoke the classes task we can see that our new task is executed:

$ gradle --console plain classes
> Task :compileJava UP-TO-DATE
> Task :projectProps
> Task :processResources
> Task :classes

BUILD SUCCESSFUL in 1s
3 actionable tasks: 2 executed, 1 up-to-date

$ cat build/project.properties
#Version and name of project
project.name=write-properties-sample
project.version=1.0.2

Without any changes we see our task was up-to-date and doesn’t have to run:

$ gradle --console plain classes
> Task :compileJava UP-TO-DATE
> Task :projectProps UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE

BUILD SUCCESSFUL in 596ms
3 actionable tasks: 3 up-to-date

Written with Gradle 6.8.3

March 10, 2021

Gradle Goodness: Add Support For "Scratch" Files To Java Project

When working on a Java project, we might want to have a place where we can just play around with the code we write. We need a "scratch" file where we can access the Java classes we write in our main sourceset. The scratch file is actually a Java source file with a main method where we can create instances of the Java code we write and invoke methods on them. This gives back a fast feedback loop, and we can use it to play around with our Java classes without the need to write a test for it. It gives great flexiblity during development. We must make sure the scratch file will not be packed in the JAR file with our production code.

To support this in our Gradle build file we can add a new sourceset that can access all classes we write in the main sourceset. Also we want to have new configurations for this sourceset so we can add dependencies that are only used by our scratch file. And finally we want a new task to run our scratch file. By default our scratch file will not be part of the JAR file with the classes from the main sourceset.

In the following example build script we first define the common configuration for a Java project with a dependency on the Log4j2 library. Notice we use the toolchain feature of Gradle to use Java 15 to compile and run our Java code. Using the toolchain definition Gradle will look for a Java 15 JDK on our computer and if it cannot find one can even download it automatically.

Next we define a new sourceset dev so we can create a Scratch.java file in the directory src/dev/java and we define the compile and runtime classpath to be dependent on the main source set output. As a bonus we also can use the src/dev/resources directory for resource files we want to have in the classpath when we run our Scratch.java file.

If we want to define dependencies that are only used by our Scratch class file we must add extra configurations: devImplementation and devRuntimeOnly. These configurations extend from the implementation and runtimeOnly configurations added by the java-library plugin. So all dependencies needed by classes in the main sourceset will also be available in the configurations for the dev sourceset.

Finally, we add a new task runDev that executes the main method in the Scratch.java file in the src/dev/java directory.

// File: build.gradle.kts
plugins {
    `java-library`
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(platform("org.apache.logging.log4j:log4j-bom:2.14.0"))
    implementation("org.apache.logging.log4j:log4j-api")
    implementation("org.apache.logging.log4j:log4j-core")
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(15))
    }
}

//------------------------------------------------------------------------------
// Configure "dev" sourceset for running Scratch class
//------------------------------------------------------------------------------

// Create new dev sourceset with a compile and runtime classpath dependency
// on the main sourceset. This allows us to use the classes we create in
// the main sourceset in our dev sourceset.
// The directories src/dev/java and src/dev/resources are recognized
// this sourceset.
val dev: SourceSet by sourceSets.creating {
    compileClasspath += sourceSets.main.get().output
    runtimeClasspath += sourceSets.main.get().output
}

// Create implementation and runtimeOnly configurations for the dev sourceset.
// These configurations can be used to define dependencies that only
// apply for the source files in the dev sourceset.
val devImplementation: Configuration by configurations.getting {
    extendsFrom(configurations.implementation.get())
}
val devRuntimeOnly: Configuration by configurations.getting {
    extendsFrom(configurations.runtimeOnly.get())
}

// Create a new task "runDev" that will run the compiled Scratch.java file
// in the root of src/dev/java. The classpath will contains all dependencies
// from the devImplementation and devRuntimeOnly configurations.
val runDev by tasks.registering(JavaExec::class) {
    description = "Run Scratch file."
    group = "dev"
    classpath = dev.runtimeClasspath
    mainClass.set("Scratch")
}

dependencies {
    // Here we add an extra dependency only for the dev sourceset.
    devImplementation("org.apache.commons:commons-lang3:3.12.0")
}

Now we have our build file with scratch file support so it is time to have some sample code.

First we create a simple Java file in our main sourceset together with a Log4j2 configuration properties file:

// File: src/main/java/mrhaki/Sample.java
package mrhaki;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Sample {
    private static Logger log = LogManager.getFormatterLogger(Sample.class);

    public String sayHello(String name) {
        log.info("sayHello(name=%s)", name);
        return "Hello %s".formatted(name);
    }
}
# File: src/main/resource/log4j2.properties
appender.console.type=Console
appender.console.name=STDOUT
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %m%n

rootLogger.level=ERROR
rootLogger.appenderRef.stdout.ref=STDOUT

To play around with our Sample class we add a scratch file and also an extra Log4j2 configuration properties file to change the configuration when we run our scratch file:

// File: src/dev/java/Scratch.java
import mrhaki.Sample;
import org.apache.commons.lang3.SystemUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Scratch {
    private static Logger log = LogManager.getFormatterLogger(Scratch.class);

    public static void main(String[] args) {
        log.info("Running dev with Java %s.", SystemUtils.JAVA_VERSION);
        Sample sample = new Sample();
        sample.sayHello("mrhaki");
    }
}
# File: src/dev/resources/log4j2.properties
rootLogger.level=DEBUG

To execute our scratch file we invoke the runDev task from the command-line:

$ gw runDev

> Task :runDev
Running dev with Java 15.0.2.
sayHello(name=mrhaki)

BUILD SUCCESSFUL in 1s
5 actionable tasks: 5 executed

Written with Gradle 6.8.3.

March 5, 2021

Gradle Goodness: Enabling Preview Features For Java

Java introduced preview features in the language since Java 12. This features can be tried out by developers, but are still subject to change and can even be removed in a next release. By default the preview features are not enabled when we want to compile and run our Java code. We must explicitly specify that we want to use the preview feature to the Java compiler and Java runtime using the command-line argument --enable-preview. In Gradle we can customize our build file to enable preview features. We must customize tasks of type JavaCompile and pass --enable-preview to the compiler arguments. Also tasks of type Test and JavaExec must be customized where we need to add the JVM argument --enable-preview.

In the following Gradle build script written in Kotlin we have a Java project written with Java 15 where we reconfigure the tasks to enable preview features:

plugins {
    java
    application
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.1")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.1")
}

application {
    mainClass.set("mrhaki.Patterns")
}

tasks {
    val ENABLE_PREVIEW = "--enable-preview"

    // In our project we have the tasks compileJava and
    // compileTestJava that need to have the
    // --enable-preview compiler arguments.
    withType<JavaCompile>() {
        options.compilerArgs.add(ENABLE_PREVIEW)

        // Optionally we can show which preview feature we use.
        options.compilerArgs.add("-Xlint:preview")

        // Explicitly setting compiler option --release
        // is needed when we wouldn't set the
        // sourceCompatiblity and targetCompatibility
        // properties of the Java plugin extension.
        options.release.set(15)
    }

    // Test tasks need to have the JVM argument --enable-preview.
    withType<Test>() {
        useJUnitPlatform()
        jvmArgs.add(ENABLE_PREVIEW)
    }

    // JavaExec tasks need to have the JVM argument --enable-preview.
    withType<JavaExec>() {
        jvmArgs.add(ENABLE_PREVIEW)
    }
}

Written with Gradle 6.8.3

February 17, 2021

Gradle Goodness: Setting Plugin Version From Property In Plugins Section

The plugins section in our Gradle build files can be used to define Gradle plugins we want to use. Gradle can optimize the build process if we use plugins {...} in our build scripts, so it is a good idea to use it. But there is a restriction if we want to define a version for a plugin inside the plugins section: the version is a fixed string value. We cannot use a property to set the version inside the plugins section. We can overcome this by using a pluginsManagement section in a settings file in the root of our project. Inside the pluginsManagement section we can use properties to set the version of a plugin we want to use. Once it is defined inside pluginsManagement we can use it in our project build script without having the specify the version. This allows us to have one place where all plugin versions are defined. We can even use a gradle.properties file in our project with all plugin versions and use that in pluginsManagement.

In the following settings file we use pluginsManagement to use a project property springBootPluginVersion to set the version to use for the Spring Boot Gradle plugin.

// File: settings.gradle.kts
pluginManagement {
    val springBootPluginVersion: String by settings // use project property with version
    plugins {
        id("org.springframework.boot") version "${springBootPluginVersion}"
    }
}

Next in our project build file we can simply reference the id of the Spring Boot Gradle plugin without the version. The version is already resolved in our settings file:

// File: build.gradle.kts
plugins {
    java
    application
    id("org.springframework.boot") // no version here: it is set in settings.gradle.kts
}

application {
    mainClass.set("com.mrhaki.sample.App")
}

Finally we can add a gradle.properties file with the project property (or specify it on the command line or environment variable):

# File: gradle.properties
springBootPluginVersion=2.4.2

Written with Gradle 6.8.2.

February 16, 2021

Gradle Goodness: Shared Configuration With Conventions Plugin

When we have a multi-module project in Gradle we sometimes want to have dependencies, task configuration and other settings shared between the multiple modules. We can use the subprojects or allprojects blocks, but the downside is that it is not clear from the build script of the subproject where the configuration comes from. We must remember it is set from another build script, but there is no reference in the subproject to that connection. It is better to use a plugin with shared configuration and use that plugin in the subprojects. We call this a conventions plugin. This way it is explicitly visible in a subproject that the shared settings come from a plugin. Also it allows Gradle to optimize the build configuration.

The easiest way to implement the shared configuration in a plugin is using a so-called precompiled script plugin. This type of plugin can be written as a build script using the Groovy or Kotlin DSL with a filename ending with .gradle or .gradle.kts. The name of the plugin is the first part of the filename before .gradle or .gradle.kts. In our subproject we can add the plugin to our build script to apply the shared configuration. For a multi-module project we can create such a plugin in the buildSrc directory. For a Groovy plugin we place the file in src/main/groovy, for a Kotlin plugin we place it in src/main/kotlin.

In the following example we write a script plugin using the Kotlin DSL to apply the java-library plugin to a project, set some common dependencies used by all projects, configure the Test tasks and set the Java toolchain. First we create a build.gradle.kts file in the buildSrc directory in the root of our multi-module project and apply the kotlin-dsl plugin:

// File: buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories.mavenCentral()

Next we create the conventions plugin with our shared configuration:

// File: buildSrc/src/main/kotlin/java-project-conventions.gradle.kts
plugins {
    `java-library`
}

group = "mrhaki.sample"
version = "1.0"

repositories {
    mavenCentral()
}

dependencies {
    val log4jVersion: String by extra("2.14.0")
    val junitVersion: String by extra("5.3.1")
    val assertjVersion: String by extra("3.19.0")
    
    // Logging
    implementation("org.apache.logging.log4j:log4j-api:${log4jVersion}")
    implementation("org.apache.logging.log4j:log4j-core:${log4jVersion}")

    // Testing
    testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")
    testImplementation("org.assertj:assertj-core:${assertjVersion}")
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(15))
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

The id of our new plugin is java-project-conventions and we can use it in our build script for a subproject as:

// File: rest-api/build.gradle.kts
plugins {
    id("java-project-conventions")  // apply shared config
    application  // apply the Gradle application plugin
}

dependencies {
    val vertxVersion: String by extra("4.0.2")

    implementation(project(":domain"))  // project dependency
    implementation("io.vertx:vertx-core:${vertxVersion}")
}

application {
    mainClass.set("com.mrhaki.web.Api")
}

The rest-api project will have all the configuration and tasks from java-library plugin as configured in the java-project-conventions plugin, so we can build it as a Java project.

Written with Gradle 6.8.2.

October 5, 2020

Gradle Goodness: Replace Files In Archives

Sometimes we might need to replace one or more files in an existing archive file. The archive file could be a zip, jar, war or other archive. Without Gradle we would unpack the archive file, copy our new file into the destination directory of the unpacked archive and archive the directory again. To achieve this with Gradle we can simply create a single task of type Zip. To get the content of the original archive we can use the project.zipTree method. We leave out the file we want to replace and define the new file as replacement. As extra safeguard we can let the tsak fail if duplicate files are in the archive, because of our replacement.

The following code shows an example of a task to replace a README file in an archive sample.zip using Groovy and Kotlin DSL. First the Groovy DSL:

// Register new task replaceZip of type org.gradle.api.tasks.bundling.Zip.
tasks.register("replaceZip", Zip) {
    archiveBaseName = "new-sample"
    destinationDirectory = file("${buildDir}/archives")

    // Include the content of the original archive.
    from(zipTree("${buildDir}/archives/sample.zip")) {
        // But leave out the file we want to replace.
        exclude("README")
    }

    // Add files with same name to replace.
    from("src/new-archive") {
        include("README")
    }

    // As archives allow duplicate file names we want to fail 
    // the build when that happens, because we want to replace
    // an existing file.
    duplicatesStrategy = "FAIL"
}

And the same task but now with Kotlin DSL:

// Register new task replaceZip of type org.gradle.api.tasks.bundling.Zip.
tasks.register<Zip>("replaceZip") {
    archiveBaseName.set("new-sample")
    destinationDirectory.set(project.layout.buildDirectory.dir("archives"))

    // Include the content of the original archive.
    from(project.zipTree("$buildDir/archives/sample.zip")) {
        // But leave out the file we want to replace.
        exclude("README")
    }

    // Add files with same name to replace.
    from(file("src/new-archive")) {
        include("README")
    }

    // As archives allow duplicate file names we want to fail 
    // the build when that happens, because we want to replace
    // an existing file.
    duplicatesStrategy = DuplicatesStrategy.FAIL
}

Written with Gradle 6.6.1.