You can build a native executable from a Java application with Maven. For that, use the GraalVM Native Image Maven plugin provided as part of the Native Build Tools project.
A “real-world” Java application likely requires some Java reflection objects, or it calls some native code, or accesses resources on the class path - dynamic features that the native-image
tool must be aware of at build time, and provided in the form of metadata.
(Native Image loads classes dynamically at build time, and not at run time.)
Depending on your application dependencies, there are three ways to provide the metadata:
This guide demonstrates how to build a native executable using the GraalVM Reachability Metadata Repository, and with the Tracing agent. The goal of this guide is to illustrate the difference between the two approaches, and demonstrate how the use of reachability metadata can simplify your development tasks.
We recommend that you follow the instructions and create the application step-by-step. Alternatively, you can go right to the completed example.
Make sure you have installed a GraalVM JDK. The easiest way to get started is with SDKMAN!. For other installation options, visit the Downloads section.
Create a new Java project with Maven in your favorite IDE or from the command line, called “H2Example”, in the org.graalvm.example
package.
package org.graalvm.example;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class H2Example {
public static final String JDBC_CONNECTION_URL = "jdbc:h2:./data/test";
public static void main(String[] args) throws Exception {
// Cleanup
withConnection(JDBC_CONNECTION_URL, connection -> {
connection.prepareStatement("DROP TABLE IF EXISTS customers").execute();
connection.commit();
});
Set<String> customers = Set.of("Lord Archimonde", "Arthur", "Gilbert", "Grug");
System.out.println("=== Inserting the following customers in the database: ");
printCustomers(customers);
// Insert data
withConnection(JDBC_CONNECTION_URL, connection -> {
connection.prepareStatement("CREATE TABLE customers(id INTEGER AUTO_INCREMENT, name VARCHAR)").execute();
PreparedStatement statement = connection.prepareStatement("INSERT INTO customers(name) VALUES (?)");
for (String customer : customers) {
statement.setString(1, customer);
statement.executeUpdate();
}
connection.commit();
});
System.out.println("");
System.out.println("=== Reading customers from the database.");
System.out.println("");
Set<String> savedCustomers = new HashSet<>();
// Read data
withConnection(JDBC_CONNECTION_URL, connection -> {
try (ResultSet resultSet = connection.prepareStatement("SELECT * FROM customers").executeQuery()) {
while (resultSet.next()) {
savedCustomers.add(resultSet.getObject(2, String.class));
}
}
});
System.out.println("=== Customers in the database: ");
printCustomers(savedCustomers);
}
private static void printCustomers(Set<String> customers) {
List<String> customerList = new ArrayList<>(customers);
customerList.sort(Comparator.naturalOrder());
int i = 0;
for (String customer : customerList) {
System.out.println((i + 1) + ". " + customer);
i++;
}
}
private static void withConnection(String url, ConnectionCallback callback) throws SQLException {
try (Connection connection = DriverManager.getConnection(url)) {
connection.setAutoCommit(false);
callback.run(connection);
}
}
private interface ConnectionCallback {
void run(Connection connection) throws SQLException;
}
}
Delete the H2Example/src/test/java/ directory (if it exists).
<?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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.graalvm.buildtools.examples</groupId>
<artifactId>maven</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<h2.version>2.2.220</h2.version>
<!-- Replace with your Java version -->
<java.version>22</java.version>
<imageName>h2example</imageName>
<mainClass>org.graalvm.example.H2Example</mainClass>
</properties>
<dependencies>
<!-- 1. H2 Database dependency -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
</dependencies>
<!-- 2. Native Image Maven plugin within a Maven profile -->
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.1</version>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<buildArgs>
<!-- 3. Quick build mode -->
<buildArg>-Ob</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>22</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.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>
<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>
</plugins>
</build>
</project>
1 Add a dependency on the H2 Database, an open source SQL database for Java. The application interacts with this database through the JDBC driver.
2 Enable the Native Image Maven plugin within a Maven profile, attached to the package
phase.
(You are going to build a native executable using a Maven profile.) A Maven profile allows you to decide whether to just build a JAR file, or a native executable.
The plugin discovers which JAR files it needs to pass to native-image
and what the executable main class should be.
3 You can pass parameters to the underlying native-image
build tool using the <buildArgs>
section. In individual <buildArg>
tags you can pass parameters exactly the same way as you do from a command line. The -Ob
option to enable quick build mode (recommended during development only) is used as an example. Learn about other configuration options from the plugin’s documentation.
mvn clean package
This generates an “executable” JAR file, one that contains all of the application’s dependencies and also a correctly configured MANIFEST file.
The Native Image Maven plugin provides support for the GraalVM Reachability Metadata repository. This repository provides GraalVM configuration for libraries which do not support GraalVM Native Image by default. One of these is the H2 Database this application depends on. The support needs to be enabled explicitly.
<configuration>
element of the native
profile to enable the GraalVM Reachability Metadata Repository:
<metadataRepository>
<enabled>true</enabled>
</metadataRepository>
The configuration block should resemble this:
<configuration>
<buildArgs>
<buildArg>-Ob</buildArg>
</buildArgs>
<metadataRepository>
<enabled>true</enabled>
</metadataRepository>
</configuration>
The plugin automatically downloads the metadata from the repository.
-P
flag):
mvn package -Pnative
This generates a native executable for the platform in the target/ directory, called h2example
.
Run the application from the native executable:
./target/h2example
The application returns a list of customers stored in the H2 Database.
The second way to provide the medatata configuration for native-image
is by injecting the Tracing agent (later the agent) at compile time.
The agent is disabled by default, but it can be enabled within your pom.xml file or via the command line.
The agent can run in three modes:
See below how to collect metadata with the tracing agent, and build a native executable applying the provided configuration.
<configuration>
element of the native
profile:
<agent>
<enabled>true</enabled>
</agent>
The configuration block should resemble this:
<configuration>
<agent>
<enabled>true</enabled>
</agent>
<buildArgs>
<buildArg>-Ob</buildArg>
</buildArgs>
<metadataRepository>
<enabled>true</enabled>
</metadataRepository>
</configuration>
native
Maven profile section, add the exec-maven-plugin
plugin:
<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>
<phase>test</phase>
<configuration>
<executable>java</executable>
<workingDirectory>${project.build.directory}</workingDirectory>
<arguments>
<argument>-classpath</argument>
<classpath/>
<argument>${mainClass}</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
mvn -Pnative -Dagent=true -DskipTests -DskipNativeBuild=true package exec:exec@java-agent
The agent captures and records calls to the H2 Database and all the dynamic features encountered during a test run into multiple *-config.json files in the target/native/agent-output/main/ directory.
mvn -Pnative -Dagent=true -DskipTests package exec:exec@native
It generates a native executable for the platform in the target/ directory, called h2example.
./target/h2example
mvn clean
, and delete the directory META-INF/ with its contents.This guide demonstrated how to build a native executable using the GraalVM Reachability Metadata Repository and with the Tracing agent. The goal was to show the difference, and prove how using the reachability metadata can simplify the work.
Note that if your application does not call any dynamic features at run time, enabling the GraalVM Reachability Metadata Repository is needless. Your workflow in that case would just be:
mvn package -Pnative