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.
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 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(); } }
-
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
-
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>
-
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>
-
Replace the default
<properties>
section in the pom.xml file with this content:<properties> <native.maven.plugin.version>0.9.27</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.
-
Register the Maven plugin for GraalVM Native Image,
native-maven-plugin
, in the profile callednative
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 thenative-image
builder and what the executable main class should be. With this plugin you can already build a native executable directly with Maven by runningmvn -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 thenative-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.
-
-
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
-
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
-
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 thenative-image
tool if you pass the appropriate options. -
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>
-
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.
-
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>
-
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>
-
Create the following test in the
src/test/java/demo/FortuneTest.java
file: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 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
.