This guide shows how to get started with the Gradle plugin for GraalVM Native Image and build a native executable for a Java application.
You will create a sample application, enable the plugin, add support for dynamic features, run JUnit tests, and build a native executable.
Two ways of building a native executable using the plugin will be demonstrated:
The plugin requires that you setup GraalVM.
The easiest way to install GraalVM is to use the SDKMAN!.
For other installation options, go to GraalVM Downloads.
Prepare a Demo Application
You start by creating a Fortune Teller sample application that simulates the traditional fortune Unix program. The data for the fortune phrases is provided by YourFortune.
-
Create a new Java project with Gradle using the following command (alternatively, you can use your IDE to generate a project):
gradle init --project-name fortune-parent --type java-application --package demo --test-framework junit-jupiter --dsl groovy
-
Rename the default app directory to fortune, edit the settings.gradle file to replace
app
withfortune
, then rename the default filename App.java to Fortune.java, and replace its contents with the following:package demo; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Iterator; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; public class Fortune { private static final Random RANDOM = new Random(); private final ArrayList<String> fortunes = new ArrayList<>(); public Fortune() throws JsonProcessingException { // Scan the file into the array of fortunes String json = readInputStream(ClassLoader.getSystemResourceAsStream("fortunes.json")); ObjectMapper omap = new ObjectMapper(); JsonNode root = omap.readTree(json); JsonNode data = root.get("data"); Iterator<JsonNode> elements = data.elements(); while (elements.hasNext()) { JsonNode quote = elements.next().get("quote"); fortunes.add(quote.asText()); } } private String readInputStream(InputStream is) { StringBuilder out = new StringBuilder(); try (InputStreamReader streamReader = new InputStreamReader(is, StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader(streamReader)) { String line; while ((line = reader.readLine()) != null) { out.append(line); } } catch (IOException e) { Logger.getLogger(Fortune.class.getName()).log(Level.SEVERE, null, e); } return out.toString(); } public String randomFortune() { // Pick a random number int r = RANDOM.nextInt(fortunes.size()); // Use the random number to pick a random fortune return fortunes.get(r); } private void printRandomFortune() throws InterruptedException { String f = randomFortune(); // Print out the fortune s.l.o.w.l.y for (char c : f.toCharArray()) { System.out.print(c); Thread.sleep(100); } System.out.println(); } /** * @param args the command line arguments */ public static void main(String[] args) throws InterruptedException, JsonProcessingException { Fortune fortune = new Fortune(); fortune.printRandomFortune(); } }
-
Delete the fortune/src/test/java directory, you will add tests in a later stage.
-
Copy and paste the following file, fortunes.json under fortune/src/main/resources/. Your project tree should be:
. ├── fortune │ ├── build.gradle │ └── src │ ├── main │ │ ├── java │ │ │ └── demo │ │ │ └── Fortune.java │ │ └── resources │ │ └── fortunes.json │ └── test │ └── resources ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle
-
Open the Gradle configuration file build.gradle, and update the main class in the
application
section:application { mainClass = 'demo.Fortune' }
-
Add explicit FasterXML Jackson dependencies that provide functionality to read and write JSON, data bindings (used in the demo application). Insert the following three lines in the
dependencies
section of build.gradle:implementation 'com.fasterxml.jackson.core:jackson-core:2.13.2' implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2' implementation 'com.fasterxml.jackson.core:jackson-annotations:2.13.2'
Also, remove the dependency on
guava
that will not be used.The next steps demonstrate what you should do to enable the Gradle Plugin for GraalVM Native Image.
-
Register the plugin. Add the following to
plugins
section of your project’s build.gradle file:plugins { // ... // Apply GraalVM Native Image plugin id 'org.graalvm.buildtools.native' version '0.9.28' }
plugins { // ... // Apply GraalVM Native Image plugin id("org.graalvm.buildtools.native") version "0.9.28" }
The
0.9.28
block pulls the latest plugin version. Replace it with a specific version if you prefer. The plugin discovers which JAR files it needs to pass to thenative-image
builder and what the executable main class should be. -
The plugin is not yet available on the Gradle Plugin Portal, so declare an additional plugin repository. Open the settings.gradle file and replace the default content with this:
pluginManagement { repositories { mavenCentral() gradlePluginPortal() } } rootProject.name = 'fortune-parent' include('fortune')
pluginManagement { repositories { mavenCentral() gradlePluginPortal() } } rootProject.name = "fortune-parent" include("fortune")
Note that the
pluginManagement {}
block must appear before any other statements in the file.
Build a Native Executable with Resources Autodetection
You can already build a native executable by running ./gradlew nativeCompile
or run it directly by invoking ./gradlew nativeRun
.
However, at this stage, running the native executable will fail because this application requires additional metadata: you need to provide it with a list of resources to load.
-
Instruct the plugin to automatically detect resources to be included in the native executable. Add this to your build.gradle file:
graalvmNative { binaries.all { resources.autodetect() } toolchainDetection = false }
graalvmNative { binaries.all { resources.autodetect() } toolchainDetection.set(false) }
Another thing to note here: the plugin may not be able to properly detect the GraalVM installation, because of limitations in Gradle. If you want to use Oracle GraalVM, or a particular version of GraalVM and Java, you need to explicitly tell this in plugin’s configuration. For example:
graalvmNative { binaries { main { javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(11) vendor = JvmVendorSpec.matching("Oracle GraalVM") } } } }
graalvmNative { binaries { main { javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(11) vendor = JvmVendorSpec.matching("Oracle GraalVM") } } } }
The workaround to this is to disable toolchain detection with this command
toolchainDetection = false
.
-
Compile the project and build a native executable at one step:
./gradlew nativeRun
The native executable, named fortune, is created in the /fortune/build/native/nativeCompile directory.
-
Run the native executable:
./fortune/build/native/nativeCompile/fortune
The application starts and prints a random quote.
Configuring the graalvmNative
plugin to automatically detect resources (resources.autodetect()
) to be included in a binary is one way to make this example work.
Using resources.autodetect()
works because the application uses resources (fortunes.json) which are directly available in the src/main/resources
location.
In the next section, the guide shows that you can use the tracing agent to do the same.
Build a Native Executable by Detecting Resources with the Agent
The Native Image Gradle plugin simplifies generation of the required metadata by injecting the
tracing agent automatically for you at compile time.
To enable the agent, just pass the -Pagent
option to any Gradle tasks that extends JavaForkOptions
(for example, test
or run
).
The following steps illustrate how to collect metadata using the agent, and then build a native executable using that metadata.
-
To demonstrate this approach, remove the
resources.autodetect()
block from your build.gradle file:binaries.all { resources.autodetect() }
-
Run your application with the agent enabled:
./gradlew -Pagent run
It runs your application on the JVM with the agent, collects the metadata, and generates configuration files in the ${buildDir}/native/agent-output/${taskName} directory.
-
Copy the configuration files into the project’s /META-INF/native-image directory using the
metadataCopy
task:./gradlew metadataCopy --task run --dir src/main/resources/META-INF/native-image
-
Build a native executable using metadata acquired by the agent:
./gradlew nativeCompile
The native executable, named fortune, is created in the build/native/nativeCompile directory.
-
Run the native executable:
./fortune/build/native/nativeCompile/fortune
The application starts and prints a random quote.
To see the benefits of running your application as a native executable, time
how long it takes and compare the results with running as a Java application.
Plugin Customization
You can customize the plugin. For example, change the name of the native executable and pass additional parameters to the plugin in the build.gradle file, as follows:
graalvmNative {
binaries {
main {
imageName.set('fortuneteller')
buildArgs.add('--verbose')
}
}
}
graalvmNative {
binaries {
main {
imageName.set("fortuneteller")
buildArgs.add("--verbose")
}
}
}
The native executable then will be called fortuneteller
.
Notice how you can pass additional arguments to the native-image
tool using the buildArgs.add
syntax.
Add JUnit Testing
The Gradle plugin for GraalVM Native Image can run JUnit Platform tests on your native executable. This means that the tests will be compiled and run as native code.
-
Create the following test in the fortune/src/test/java/demo/FortuneTest.java file:
fortune/src/test/java/demo/FortuneTest.javapackage demo; import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertTrue; class FortuneTest { @Test @DisplayName("Returns a fortune") void testItWorks() throws JsonProcessingException { Fortune fortune = new Fortune(); assertTrue(fortune.randomFortune().length()>0); } }
-
Run JUnit tests:
./gradlew nativeTest
The plugin runs tests on the JVM prior to running tests from the native executable. To disable testing support (which comes by default), add the following configuration to the build.gradle file:
graalvmNative {
testSupport = false
}
graalvmNative {
testSupport.set(false)
}
Run Tests with the Agent
If you need to test collecting metadata with the agent, add the -Pagent
option to the test
and nativeTest
task invocations:
-
Run the tests on the JVM with the agent:
./gradlew -Pagent test
It runs your application on the JVM with the agent, collects the metadata and uses it for testing on
native-image
. The generated configuration files (containing the metadata) can be found in the ${buildDir}/native/agent-output/${taskName} directory. In this case, the plugin also substitutes{output_dir}
in the agent options to point to this directory. -
Build a native executable using the metadata collected by the agent:
./gradlew -Pagent nativeTest
Summary
The Gradle plugin for GraalVM Native Image adds support for building and testing native executables using the Gradle. The plugin has many features, described in the plugin reference documentation.
Note that if your application does not call any classes dynamically at run time, the execution with the agent is needless.
Your workflow, in that case, is just ./gradlew nativeRun
.