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 to build a native executable using the plugin are demonstrated:

Sample 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.

To use the Native Build Tools, install GraalVM with Native Image. The easiest way to install GraalVM with Native Image is to use the GraalVM JDK Downloader:

bash <(curl -sL https://get.graalvm.org/jdk)

Prepare a Demo Application

  1. 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
  2. Rename the default app directory to fortune, edit the settings.gradle file to replace app with fortune, 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();
        }
    }
  3. Delete the fortune/src/test/java directory, you will add tests in a later stage.

  4. 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
  5. Open the Gradle configuration file build.gradle, and update the main class in the application section:

    application {
        mainClass = 'demo.Fortune'
    }
  6. 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.

  7. 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.21'
    }
    plugins {
      // ...
    
      // Apply GraalVM Native Image plugin
      id("org.graalvm.buildtools.native") version "0.9.21"
    }

    The 0.9.21 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 the native-image builder and what the executable main class should be.

  8. 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.

  1. 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. By default, the plugin selects a Java 11 GraalVM Community Edition. If you want to use GraalVM Enterprise, 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("GraalVM Community")
                }
            }
        }
    }
    graalvmNative {
        binaries {
            main {
                javaLauncher = javaToolchains.launcherFor {
                    languageVersion = JavaLanguageVersion.of(11)
                    vendor = JvmVendorSpec.matching("GraalVM Community")
                }
            }
        }
    }

    The workaround to this is to disable toolchain detection with this command toolchainDetection = false.

  1. 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.

  2. 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.

  1. To demonstrate this approach, remove the resources.autodetect() block from your build.gradle file:

    binaries.all {
        resources.autodetect()
    }
  2. 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.

  3. 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
  4. 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.

  5. 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.

  1. Create the following test in the fortune/src/test/java/demo/FortuneTest.java file:

    fortune/src/test/java/demo/FortuneTest.java
    package 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);
        }
    }
  2. 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:

  1. 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.

  2. 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.