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

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.

The plugin requires that you setup GraalVM and 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)

It will then make use of Maven profiles to enable building and testing of native executables.

Prepare a Demo Application

  1. Create a new Java project with Maven in your favorite IDE, called "Fortune", in the demo package. Make sure to choose JUnit Jupiter as the test engine. The application should contain a sole Java file with the following content:

    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 number
            int r = RANDOM.nextInt(fortunes.size());
            //Use the random number to pick a random fortune
            String f = fortunes.get(r);
            // 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
        * @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();
        }
    }
  2. Copy and paste the following file, fortunes.json under resources/. Your project tree should be:

    .
    ├── pom.xml
    └── src
        └── main
            ├── java
            │   └── demo
            │       └── Fortune.java
            └── resources
                └── fortunes.json
  3. Add explicit FasterXML Jackson dependencies 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:

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.12.6.1</version>
        </dependency>
    </dependencies>
  4. 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.0.0</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.8.1</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.2.2</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>
                <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>
  5. Replace the default <properties> section in the pom.xml file with this content:

    <properties>
        <native.maven.plugin.version>0.9.23</native.maven.plugin.version>
        <junit.jupiter.version>5.8.1</junit.jupiter.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.

  6. Register the Maven plugin for GraalVM Native Image, native-maven-plugin, in the profile called 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>
                        <extensions>true</extensions>
                        <executions>
                            <execution>
                                <id>build-native</id>
                                <goals>
                                    <goal>build</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).

    This demo application is a little more complicated than HelloWorld, and requires metadata before building a native executable. You do not have to configure anything manually: the plugin can generate the required metadata 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.

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

      <configuration>
      <agent>
          <enabled>true</enabled>
      </agent>
      </configuration>
    • To enable the agent via the command line, pass the -Dagent=true option when running Maven.

      So your next step is to run with the agent.

  7. 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.0.0</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 from a Java application the plugin.

Build a Native Executable

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

  3. 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 within a <configuration> node:

    <configuration>
        <imageName>fortuneteller</imageName>
    </configuration>
  4. Run the demo directly or with the Maven profile:

    ./target/fortune
    mvn -Pnative exec:exec@native

To see the benefits of running your application as a native executable, time how long it takes and compare the results with running on the JVM.

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.8 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>
  2. Add an explicit dependency on the junit-platform-launcher artifact to the dependencies section of your native profile configuration as in the following example:

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

    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);
        }
    }
  4. Run native tests:

    mvn -Pnative test

    Run -Pnative profile will then build and run native 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.

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 mvn clean -Pnative package.