This guide shows how to get started with the Maven 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.

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 Maven in your favorite IDE, called Fortune, in the package named demo. Make sure to choose JUnit Jupiter as the test engine. The pom.xml file should look similar to the following.

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>demo</groupId>
        <artifactId>fortune</artifactId>
        <version>1.0.0</version>
        <name>Fortune Teller GraalVM Demo</name>
    
        <dependencies>
            <dependency>
                <groupId>org.junit.jupiter</groupId>
                <artifactId>junit-jupiter</artifactId>
                <version>5.10.1</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
    </project>
  2. Create the Fortune.java (src/main/java/demo/Fortune.java) class.

    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();
        }
    
        private void printRandomFortune() throws InterruptedException {
            //Pick a random fortune
            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();
        }
    
        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);
        }
    
        /**
        * @param args the command line arguments
        * @throws java.lang.InterruptedException
        * @throws com.fasterxml.jackson.core.JsonProcessingException
        */
        public static void main(String[] args) throws InterruptedException, JsonProcessingException {
            Fortune fortune = new Fortune();
            fortune.printRandomFortune();
        }
    }
  3. Copy and paste the following file, fortunes.json under the resources directory (src/main/resources/fortunes.json). Your project tree should be:

    .
    ├── pom.xml
    └── src
        └── main
            ├── java
            │   └── demo
            │       └── Fortune.java
            └── resources
                └── fortunes.json
  4. Add the FasterXML Jackson dependency that provide functionality to read and write JSON, data bindings (used in the demo application). Open the pom.xml file (a Maven configuration file), and insert the following in the <dependencies> section:

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.16.0</version>
    </dependency>

    There should be two dependencies, the FasterXML Jackson dependency and the JUnit 5 dependency as shown below.

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.16.0</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
  5. Add regular Maven plugins for building and assembling a Maven project into an executable JAR. Insert the following into the build section in the pom.xml file:

        <build>
            <plugins>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>3.1.1</version>
                    <executions>
                        <execution>
                            <id>java</id>
                            <goals>
                                <goal>java</goal>
                            </goals>
                            <configuration>
                                <mainClass>${mainClass}</mainClass>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
    
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.11.0</version>
                    <configuration>
                        <source>${maven.compiler.source}</source>
                        <target>${maven.compiler.source}</target>
                    </configuration>
                </plugin>
    
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.3.0</version>
                    <configuration>
                        <archive>
                            <manifest>
                                <addClasspath>true</addClasspath>
                                <mainClass>${mainClass}</mainClass>
                            </manifest>
                        </archive>
                    </configuration>
                </plugin>
    
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-assembly-plugin</artifactId>
                    <version>3.6.0</version>
                    <executions>
                        <execution>
                            <phase>package</phase>
                            <goals>
                                <goal>single</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <archive>
                            <manifest>
                                <addClasspath>true</addClasspath>
                                <mainClass>${mainClass}</mainClass>
                            </manifest>
                        </archive>
                        <descriptorRefs>
                            <descriptorRef>jar-with-dependencies</descriptorRef>
                        </descriptorRefs>
                    </configuration>
                </plugin>
            </plugins>
        </build>
  6. Replace the default <properties> section in the pom.xml file with this content:

        <properties>
            <native.maven.plugin.version>0.10.4</native.maven.plugin.version>
            <maven.compiler.source>${java.specification.version}</maven.compiler.source>
            <maven.compiler.target>${java.specification.version}</maven.compiler.target>
            <imageName>fortune</imageName>
            <mainClass>demo.Fortune</mainClass>
        </properties>

    The statements "hardcoded" plugin versions and the entry point class to your application. The next steps demonstrate what you should do to enable the Maven plugin for GraalVM Native Image.

  7. Register the Maven plugin for GraalVM Native Image, native-maven-plugin, within a Maven profile, native, by adding the following to the pom.xml file:

        <profiles>
            <profile>
                <id>native</id>
                <build>
                    <plugins>
                        <plugin>
                            <groupId>org.graalvm.buildtools</groupId>
                            <artifactId>native-maven-plugin</artifactId>
                            <version>${native.maven.plugin.version}</version>
                            <executions>
                                <execution>
                                    <id>build-native</id>
                                    <goals>
                                        <goal>compile-no-fork</goal>
                                    </goals>
                                    <phase>package</phase>
                                </execution>
                                <execution>
                                    <id>test-native</id>
                                    <goals>
                                        <goal>test</goal>
                                    </goals>
                                    <phase>test</phase>
                                </execution>
                            </executions>
                            <configuration>
                                <fallback>false</fallback>
                            </configuration>
                        </plugin>
                    </plugins>
                </build>
            </profile>
        </profiles>

    It pulls the latest plugin version. Replace ${native.maven.plugin.version} 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. With this plugin you can already build a native executable directly with Maven by running mvn -Pnative package (if your application does not call any methods reflectively at run time).

Build a Native Executable by Detecting Resources with the Agent

This demo application and requires metadata before building a native executable. You do not have to configure anything manually: the plugin can generate the required configuration for you by injecting the tracing agent at package time. The agent is disabled by default, and can be enabled in project’s pom.xml file or via the command line.

  1. To enable the agent via the pom.xml file, specify <enabled>true</enabled> in the native-maven-plugin plugin configuration:

    <configuration>
        <fallback>false</fallback>
        <agent>
            <enabled>true</enabled>
        </agent>
    </configuration>

    To enable the agent via the command line, pass the -Dagent=true option when running Maven.

  2. Before running with the agent, register a separate Mojo execution in the native profile which allows forking the Java process. It is required to run your application with the agent.

    <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>3.1.1</version>
        <executions>
            <execution>
                <id>java-agent</id>
                <goals>
                    <goal>exec</goal>
                </goals>
                <configuration>
                    <executable>java</executable>
                    <workingDirectory>${project.build.directory}</workingDirectory>
                    <arguments>
                        <argument>-classpath</argument>
                        <classpath/>
                        <argument>${mainClass}</argument>
                    </arguments>
                </configuration>
            </execution>
            <execution>
                <id>native</id>
                <goals>
                    <goal>exec</goal>
                </goals>
                <configuration>
                    <executable>${project.build.directory}/${imageName}</executable>
                    <workingDirectory>${project.build.directory}</workingDirectory>
                </configuration>
            </execution>
        </executions>
    </plugin>

    Now you are all set to to build a native executable.

  3. Compile the project on the JVM to create a runnable JAR with all dependencies. Open a terminal window and, from the root application directory, run:

    mvn clean package
  4. Run your application with the agent enabled:

    mvn -Pnative -Dagent exec:exec@java-agent

    The agent collects the metadata and generates the configuration files in a subdirectory of target/native/agent-output. Those files will be automatically used by the native-image tool if you pass the appropriate options.

  5. Now build a native executable with the Maven profile:

    mvn -DskipTests=true -Pnative -Dagent package

    When the command completes, a native executable, fortune, is created in the target directory of the project and ready for use.

    The executable’s name is derived from the artifact ID, but you can specify any custom name in native-maven-plugin by providing the <imageName>fortuneteller</imageName> within a <configuration> node:

    <configuration>
        <imageName>fortuneteller</imageName>
        <fallback>false</fallback>
        <agent>
            <enabled>true</enabled>
        </agent>
    </configuration>
  6. Run the demo directly or with the Maven profile:

    ./target/fortune

    The application starts and prints a random quote.

Add JUnit Testing

The Maven plugin for GraalVM Native Image can run JUnit Platform tests on a native executable. This means that tests will be compiled and executed as native code.

This plugin requires JUnit Platform 1.10 or higher and Maven Surefire 2.22.0 or higher to run tests on a native executable.

  1. Enable extensions in the plugin’s configuration, <extensions>true</extensions>:

    <plugin>
        <groupId>org.graalvm.buildtools</groupId>
        <artifactId>native-maven-plugin</artifactId>
        <version>${native.maven.plugin.version}</version>
        <extensions>true</extensions>
        ...
    </plugin>
  2. Add an explicit dependency on the junit-platform-launcher artifact in the dependencies section of your native profile configuration as in the following example:

    <profile>
        <id>native</id>
        <dependencies>
            <dependency>
                <groupId>org.junit.platform</groupId>
                <artifactId>junit-platform-launcher</artifactId>
                <version>1.10.0</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
        ...
    </profile>
  3. Create the following test in the src/test/java/demo/FortuneTest.java file:

    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.assertFalse;
    
    class FortuneTest {
        @Test
        @DisplayName("Returns a fortune")
        void testItWorks() throws JsonProcessingException {
            Fortune fortune = new Fortune();
            assertFalse(fortune.randomFortune().isEmpty());
        }
    }
  4. Run JUnit tests:

    mvn -Pnative -Dagent test

    The -Pnative profile will then build and run JUnit tests.

Summary

The Maven plugin for GraalVM Native Image adds support for building and testing native executables using Apache Maven™. The plugin has many features, described in the plugin reference documentation.