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, build a native executable, and run JUnit tests.

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

  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 {
      id 'org.graalvm.buildtools.native' version '0.10.3'
    }
    plugins {
      id("org.graalvm.buildtools.native") version "0.10.3"
    }

    The 0.10.3 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.

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

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

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)
}

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.