Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

The Gradle 6.0 release brings several new features that will help make our builds more efficient and robust. These features include improved dependency management, module metadata publishing, task configuration avoidance, and support for JDK 13.

In this tutorial, we’ll introduce the new features available in Gradle 6.0. Our example build files will use Gradle’s Kotlin DSL.

2. Dependency Management Improvements

With each release in recent years, Gradle has made incremental improvements to how projects manage dependencies. These dependency improvements culminate in Gradle 6.0. Let’s review dependency management improvements that are now stable.

2.1. API and Implementation Separation

The java-library plugin helps us to create a reusable Java library. The plugin encourages us to separate dependencies that are part of our library’s public API from dependencies that are implementation details. This separation makes builds more stable because users won’t accidentally refer to types that are not part of a library’s public API.

The java-library plugin and its api and implementation configurations were introduced in Gradle 3.4. While this plugin is not new to Gradle 6.0, the enhanced dependency management capabilities it provides are part of the comprehensive dependency management realized in Gradle 6.0.

2.2. Rich Versions

Our project dependency graphs often have multiple versions of the same dependency. When this happens, Gradle needs to select which version of the dependency the project will ultimately use.

Gradle 6.0 allows us to add rich version information to our dependencies. Rich version information helps Gradle make the best possible choice when resolving dependency conflicts.

For example, consider a project that depends on Guava. Suppose further that this project uses Guava version 28.1-jre, even though we know that it only uses Guava APIs that have been stable since version 10.0.

We can use the require declaration to tell Gradle that this project may use any version of Guava since 10.0, and we use the prefer declaration to tell Gradle that it should use 28.1-jre if no other constraints are preventing it from doing so. The because declaration adds a note explaining this rich version information:

implementation("com.google.guava:guava") {
    version {
        require("10.0")
        prefer("28.1-jre")
        because("Uses APIs introduced in 10.0. Tested with 28.1-jre")
    }
}

How does this help make our builds more stable? Suppose this project also relies on a dependency foo that must use Guava version 16.0. The build file for the foo project would declare that dependency as:

dependencies {
    implementation("com.google.guava:guava:16.0")
}

Since the foo project depends on Guava 16.0, and our project depends on both Guava version 28.1-jre and foo, we have a conflict. Gradle’s default behavior is to pick the latest version. In this case, however, picking the latest version is the wrong choice, because foo must use version 16.0.

Prior to Gradle 6.0, users had to solve conflicts on their own. Because Gradle 6.0 allows us to tell Gradle that our project may use Guava versions as low as 10.0, Gradle will correctly resolve this conflict and choose version 16.0.

In addition to the require and prefer declarations, we can use the strictly and reject declarations. The strictly declaration describes a dependency version range that our project must use. The reject declaration describes dependency versions that are incompatible with our project.

If our project relied on an API that we know will be removed in Guava 29, then we use the strictly declaration to prevent Gradle from using a version of Guava greater than 28. Likewise, if we know there is a bug in Guava 27.0 that causes problems for our project, we use reject to exclude it:

implementation("com.google.guava:guava") {
    version {
        strictly("[10.0, 28[")
        prefer("28.1-jre")
        reject("27.0")
        because("""
            Uses APIs introduced in 10.0 but removed in 29. Tested with 28.1-jre.
            Known issues with 27.0
        """)
    }
}

2.3. Platforms

The java-platform plugin allows us to reuse a set of dependency constraints across projects. A platform author declares a set of tightly coupled dependencies whose versions are controlled by the platform.

Projects that depend on the platform do not need to specify versions for any of the dependencies controlled by the platform. Maven users will find this similar to a Maven parent POM’s dependencyManagement feature.

Platforms are especially useful in multi-project builds. Each project in the multi-project build may use the same external dependencies, and we don’t want the versions of those dependencies to be out of sync.

Let’s create a new platform to make sure our multi-project build uses the same version of Apache HTTP Client across projects. First, we create a project, httpclient-platform, that uses the java-platform plugin:

plugins {
    `java-platform`
}

Next, we declare constraints for the dependencies included in this platform. In this example, we’ll pick the versions of the Apache HTTP Components that we want to use in our project:

dependencies {
    constraints {
        api("org.apache.httpcomponents:fluent-hc:4.5.10")
        api("org.apache.httpcomponents:httpclient:4.5.10")
    }
}

Finally, let’s add a person-rest-client project that uses the Apache HTTP Client Fluent API. Here, we’re adding a dependency on our httpclient-platform project using the platform method. We’ll also add a dependency on org.apache.httpcomponents:fluent-hc. This dependency does not include a version because the httpclient-platform determines the version to use:

plugins {
    `java-library`
}

dependencies {
    api(platform(project(":httpclient-platform")))
    implementation("org.apache.httpcomponents:fluent-hc")
}

The java-platform plugin helps to avoid unwelcome surprises at runtime due to misaligned dependencies in the build.

2.4. Test Fixtures

Prior to Gradle 6.0, build authors who wanted to share test fixtures across projects extracted those fixtures to another library project. Now, build authors can publish test fixtures from their project using the java-test-fixtures plugin.

Let’s build a library that defines an abstraction and publishes test fixtures that verify the contract expected by that abstraction.

In this example, our abstraction is a Fibonacci sequence generator, and the test fixture is a JUnit 5 test mix-in. Implementors of the Fibonacci sequence generator may use the test mix-in to verify they have implemented the sequence generator correctly.

First, let’s create a new project, fibonacci-spi, for our abstraction and test fixtures. This project requires the java-library and java-test-fixtures plugins:

plugins {
    `java-library`
    `java-test-fixtures`
}

Next, let’s add JUnit 5 dependencies to our test fixtures. Just as the java-library plugin defines the api and implementation configurations, the java-test-fixtures plugin defines the testFixturesApi and testFixturesImplementation configurations:

dependencies {
    testFixturesApi("org.junit.jupiter:junit-jupiter-api:5.8.1")
    testFixturesImplementation("org.junit.jupiter:junit-jupiter-engine:5.8.1")
}

With our dependencies in place, let’s add a JUnit 5 test mix-in to the src/testFixtures/java source set created by the java-test-fixtures plugin. This test mix-in verifies the contract of our FibonacciSequenceGenerator abstraction:

public interface FibonacciSequenceGeneratorFixture {

    FibonacciSequenceGenerator provide();

    @Test
    default void whenSequenceIndexIsNegative_thenThrows() {
        FibonacciSequenceGenerator generator = provide();
        assertThrows(IllegalArgumentException.class, () -> generator.generate(-1));
    }

    @Test
    default void whenGivenIndex_thenGeneratesFibonacciNumber() {
        FibonacciSequenceGenerator generator = provide();
        int[] sequence = { 0, 1, 1, 2, 3, 5, 8 };
        for (int i = 0; i < sequence.length; i++) {
            assertEquals(sequence[i], generator.generate(i));
        }
    }
}

This is all we need to do to share this test fixture with other projects.

Now, let’s create a new project, fibonacci-recursive, which will reuse this test fixture. This project will declare a dependency on the test fixtures from our fibonacci-spi project using the testFixtures method in our dependencies block:

dependencies {
    api(project(":fibonacci-spi"))
    
    testImplementation(testFixtures(project(":fibonacci-spi")))
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1")
}

Finally, we can now use the test mix-in defined in the fibonacci-spi project to create a new test for our recursive fibonacci sequence generator:

class RecursiveFibonacciUnitTest implements FibonacciSequenceGeneratorFixture {
    @Override
    public FibonacciSequenceGenerator provide() {
        return new RecursiveFibonacci();
    }
}

The Gradle 6.0 java-test-fixtures plugin gives build authors more flexibility to share their test fixtures across projects.

3. Gradle Module Metadata Publishing

Traditionally, Gradle projects publish build artifacts to Ivy or Maven repositories. This includes generating ivy.xml or pom.xml metadata files respectively.

The ivy.xml and pom.xml models cannot store the rich dependency information that we’ve discussed in this article. This means that downstream projects do not benefit from this rich dependency information when we publish our library to a Maven or Ivy repository.

Gradle 6.0 addresses this gap by introducing the Gradle Module Metadata specification. The Gradle Module Metadata specification is a JSON format that supports storing all the enhanced module dependency metadata introduced in Gradle 6.0.

Projects can build and publish this metadata file to Ivy and Maven repositories in addition to traditional ivy.xml and pom.xml metadata files. This backward compatibility allows Gradle 6.0 projects to take advantage of this module metadata if it is present without breaking legacy tools.

To publish the Gradle Module Metadata files, projects must use the new Maven Publish Plugin or Ivy Publish Plugin. As of Gradle 6.0, these plugins publish the Gradle Module Metadata file by default. These plugins replace the legacy publishing system.

3.1. Publishing Gradle Module Metadata to Maven

Let’s configure a build to publish Gradle Module Metadata to Maven. First, we include the maven-publish in our build file:

plugins {
    `java-library`
    `maven-publish`
}

Next, we configure a publication. A publication can include any number of artifacts. Let’s add the artifact associated with the java configuration:

publishing {
    publications {
        register("mavenJava", MavenPublication::class) {
            from(components["java"])
        }
    }
}

The maven-publish plugin adds the publishToMavenLocal task. Let’s use this task to test our Gradle Module Metadata publication:

./gradlew publishToMavenLocal

Next, let’s list the directory for this artifact in our local Maven repository:

ls ~/.m2/repository/com/baeldung/gradle-6/1.0.0/
gradle-6-1.0.0.jar	gradle-6-1.0.0.module	gradle-6-1.0.0.pom

As we can see in the console output, Gradle generates the Module Metadata file in addition to the Maven POM.

4. Configuration Avoidance API

Since version 5.1, Gradle encouraged plugin developers to make use of new, incubating Configuration Avoidance APIs. These APIs help builds avoid relatively slow task configuration steps when possible. Gradle calls this performance improvement Task Configuration Avoidance. Gradle 6.0 promotes this incubating API to stable.

While the Configuration Avoidance feature mostly affects plugin authors, build authors who create any custom Configuration, Task, or Property in their build are also affected. Plugin authors and build authors alike can now use the new lazy configuration APIs to wrap objects with the Provider type, so that Gradle will avoid “realizing” these objects until they’re needed.

Let’s add a custom task using lazy APIs. First, we register the task using the TaskContainer.registering extension method. Since registering returns a TaskProvider, the creation of the Task instance is deferred until Gradle or the build author calls the TaskProvider.get(). Lastly, we provide a closure that will configure our Task after Gradle creates it:

val copyExtraLibs by tasks.registering(Copy::class) {
    from(extralibs)
    into(extraLibsDir)
}

Gradle’s Task Configuration Avoidance Migration Guide helps plugin authors and build authors migrate to the new APIs. The most common migrations for build authors include:

  • tasks.register instead of tasks.create
  • tasks.named instead of tasks.getByName
  • configurations.register instead of configurations.create
  • project.layout.buildDirectory.dir(“foo”) instead of File(project.buildDir, “foo”)

5. JDK 13 Support

Gradle 6.0 introduces support for building projects with JDK 13. We can configure our Java build to use Java 13 with the familiar sourceCompatibility and targetCompatibility settings:

sourceCompatibility = JavaVersion.VERSION_13
targetCompatibility = JavaVersion.VERSION_13

Some of JDK 13’s most exciting language features, such as Raw String Literals, are still in preview status. Let’s configure the tasks in our Java build to enable these preview features:

tasks.compileJava {
    options.compilerArgs.add("--enable-preview")
}
tasks.test {
    jvmArgs.add("--enable-preview")
}
tasks.javadoc {
    val javadocOptions = options as CoreJavadocOptions
    javadocOptions.addStringOption("source", "13")
    javadocOptions.addBooleanOption("-enable-preview", true)
}

6. Conclusion

In this article, we discussed some of the new features in Gradle 6.0.

We covered enhanced dependency management, publishing Gradle Module Metadata, Task Configuration Avoidance, and how early adopters can configure their builds to use Java 13 preview language features.

As always, the code for this article is over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.