17 March 2021

Tags: gradle java16

Java 16 is out and I’m seeing a number of folks trying to figure out how to use Java 16 with Gradle. Often they would try to run Gradle with JDK 16 and see it fail. There’s a ticket about Java 16 support in Gradle but in most cases you can already work with JDK 16 without waiting for official support.

The happy path

Gradle 7, which is due soon, will provide official support for Java 16. If you have an existing build that you want to try on Java 16, you can update the wrapper to use the latest Gradle 7.0 milestone release:

gradle/wrapper/gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-milestone-3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

If you are lucky this is all you need to do.

However, Gradle 7 is a major release, and as such it brings a number of changes which may break your build (deprecated methods being removed, or, in particular for the Java 16 support, upgrading to Groovy 3 internally). It may be a bit involved to migrate to Gradle 7 just to try Java 16.

Decouple the Java version used for Gradle itself from the version you need!

It’s actually better to decouple the version of Java required to run Gradle from the version of Java your application requires. In general, it’s actually considered the best practice to use whatever version of the JDK Gradle officially supports to run Gradle itself, and configure the build to use a different JDK.

Configuring Java toolchains

In Gradle terminology, this is called activating Java Toolchains.

Let’s get started with a sample project running on latest stable Gradle, which is 6.8.3. Make sure that you have Gradle 6.8.3 on your PATH to get started. I’m personally recommending to use sdkman! to install Gradle:

$ sdk install gradle 6.8.3

At the same time, we want to make sure we run Gradle with a supported version, which is anything between Java 8 and 15:

$ java -version
java -version
openjdk version "11.0.9.1" 2020-11-04
OpenJDK Runtime Environment 18.9 (build 11.0.9.1+1)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.9.1+1, mixed mode)

If it outputs something else than 8 to 15, please make sure to update your PATH to point to such a JDK. Again you can do this with sdkman!:

$ sdk install java 11.0.9.open

Demo application

Now, let’s create a sample Gradle project:

$ mkdir demo-app
$ cd demo-app
$ gradle init

Then select:

Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4] 2

Select implementation language:
  1: C++
  2: Groovy
  3: Java
  4: Kotlin
  5: Scala
  6: Swift
Enter selection (default: Java) [1..6] 3

Split functionality across multiple subprojects?:
  1: no - only one application project
  2: yes - application and library projects
Enter selection (default: no - only one application project) [1..2] 1

Select build script DSL:
  1: Groovy
  2: Kotlin
Enter selection (default: Groovy) [1..2] 1

Select test framework:
  1: JUnit 4
  2: TestNG
  3: Spock
  4: JUnit Jupiter
Enter selection (default: JUnit 4) [1..4] 4

Project name (default: demo-app):
Source package (default: demo.app):

and confirm the default name and packages.

Then let’s run our app:

$ ./gradlew run

> Task :app:run
Hello World!

BUILD SUCCESSFUL in 4s
2 actionable tasks: 2 executed

Migrating the application to Java 16

All good! Now let’s configure Gradle to use Java 16 to build and run our app instead. Let’s open the build script, found under app:

app/build.gradle
plugins {
    // Apply the application plugin to add support for building a CLI application in Java.
    id 'application'
}

// Add this under the `plugins` section:
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(16)
    }
}

The java.toolchain block lets us configure the toolchain that Gradle is going to use to build and run your application. We’re setting 16, which means that we’re going to compile the main and test sources as well as execute the with a Java 16 JDK. Gradle will automatically try to find if you have a Java 16 installation in a conventional location. If it cannot find one you will see something like this happening:

Provisioning toolchain adoptopenjdk-16-x64-linux.tar.gz > adoptopenjdk-16-x64-linux.tar.gz > 66 MiB/195.8 MiB

which means that Gradle is downloading the JDK for you!

Let’s check:

./gradlew run

Dang! The build fails!. To some extent, it’s good news, it means that Gradle is really using Java 16, but why is it failing?

Disabling incremental compilation

Well, you’re facing one of the bugs we fixed in 7, which is that our incremental compiler isn’t compatible with Java 16 because we’re using classes which have been made "hidden" by the module system in Java 16.

There’s an easy fix: let’s disable incremental compilation!

Again, let’s open our app/build.gradle file and add this:

tasks.withType(JavaCompile).configureEach {
	// disable incremental compilation
    options.incremental = false
}

And let’s run the build again:

./gradlew run

Yay! This time the build passed! Congrats, you have your first Java 16 app running!

Alternatively to disabling incremental compilation, you might just want to let Gradle access the JDK internals. This solution is better for performance, even if a bit "hackish":

tasks.withType(JavaCompile).configureEach {
    options.forkOptions.jvmArgs.addAll( ['--add-opens', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED'] )
}

In case you want to use one of the experimental features that Java 16 provides, the setup I’ve described in a previous post about Java Feature Previews still hold and is a good follow-up to this post!