Get Started with GraalVM Native Image
Introduction
This lab takes you step by step through the process of building a cloud native Java application with GraalVM Native Image . It is aimed at developers with a knowledge of Java.
GraalVM Native Image technology compiles Java code ahead-of-time into a self-contained native executable. Only the code that is requiredby the application at run time is packaged into the executable.
A native executable produced by Native Image has several important advantages, in that it:
- Uses a fraction of the resources required by the JVM, so is cheaper to run
- Starts in milliseconds
- Delivers peak performance immediately, with no warmup
- Can be packaged into a lightweight container image for faster and more efficient deployment
- Presents a reduced attack surface (more on this in future labs)
Many of the leading microservice frameworks support ahead-of-time compilation with GraalVM Native Image, including Micronaut, Spring, Helidon, and Quarkus.
In addition, there are Maven and Gradle plugins for Native Image so you can easily build, test, and run Java applications as executable files.
Note: Oracle Cloud Infrastructure (OCI) provides GraalVM Enterprise at no additional cost.
Estimated lab time: 45 minutes
Lab Objectives
In this lab you will perform the following tasks:
- Connect to a remote host in Oracle Cloud - you will develop your application on an Oracle Cloud virtual machine
- Build and run a Java application, using GraalVM JDK
- Build a Java application into a native executable, using GraalVM Native Image
- Build a native executable that works with the dynamic features of Java
- Use Maven plugin for GraalVM Native Image to generate a native executable
NOTE: If you see the laptop icon in the lab, you need to do something such as enter a command. Keep an eye out for it.
# This is where we you will need to do something
STEP 1: Connect to a Remote Host and Check the Development Environment
Your development environment is provided by a remote host: an OCI Compute Instance with
Oracle Linux 8, 1 CPU, and 32GB of memory.
The Luna Labs desktop environment will display before the remote host is ready, which can take up to two minutes.
Connecting to your remote host is done though running a setup script in your Luna Desktop environment. This script is available through the resources tab.
In the desktop, double-click the Luna-Lab.html icon. The page opens to display Oracle Cloud Infrastructure credentials and information specific to your lab.
The Resources Tab will be displayed. Note that the cog shown next to the Resources title will spin whilst the compute instance is being provisioned in the cloud.
When the instance is provisioned, this may take up to 2 minutes, you will see the following displayed on the Resources tab
Copy the configuration script, that sets up your VS Code environment, from the resources tab. Click on the View Details link to reveal the configuration. Copy this as shown in the screen shot below.
Open a Terminal, as shown in the screenshot below:
Paste the configuration code into the terminal, which will open VS Code for you.
And you are done! Congratulations, you are now successfully connected to a remote host in Oracle Cloud!
Note on the Development Environment
You will use GraalVM Enterprise 22 , as the Java platform for this lab. GraalVM is a high-performance JDK distribution from Oracle built on Oracle Java SE.
Your development environment comes preconfigured with GraalVM and the Native Image tool required for this lab.
You can easily check that by running these commands in the terminal - you can create a terminal from within VS Code, Terminal > New Terminal
:
java -version
native-image --version
STEP 2: Build and Run a Demo Application
For the demo, you will use a command-line Java application that counts the number of files in the current directory and its subdirectories. As a nice extra, the application also calculates the total size of the files.
The source code for the application is available in your remote host.
Note on the Demo Application
The application consists of two Java files that can be found in the src
directory:
App.java
: a wrapper for theListDir
class.ListDir.java
: this does all the work. It counts the files and summarizes the output.
The application can be built by hand, or by using Maven profiles. The Maven build configuration is provided by
the pom.xml
file. Maven Profiles are a great way to have different build configurations within a single pom.xml
file. You can find out more about Maven Profiles here .
You can browse through the files within the open VS Code.
Several profiles will be used in this lab, each of which has a particular purpose:
native
: This profile builds an executable file using GraalVM Native Image.java_agent
: This profile builds the Java application with a tracing agent that tracks all usages of the dynamic code in your application and captures this information into configuration files. More on this later.
You use a particular Maven profile by passing it as a parameter to the mvn
command. The name of the profile is
appended to the -P
flag. You can run the following commands from within the Terminal within VS Code.
The example below shows how you could call a native
profile, when building with Maven:
mvn clean package -Pnative
Now that you have a basic understanding of what the application does run it to see how it works.
Build the project and run it, from within the Terminal we opened in VS Code:
mvn clean package exec:exec
The command above does the following:
- Cleans the project to remove any generated or compiled artifacts.
- Creates a runnable JAR file containing the application. This JAR file will be later used by Native Image.
- Runs the application by running the
exec
plugin.
You should see the following included in the generated output (the number of files reported by the application may vary):
Counting directory: . Total: 15 files, total size = 511.9 KiB [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------
STEP 3: Turn a Java Application into an Executable File
Next, you are going to build an executable version of the application using GraalVM Native Image. As a quick reminder, GraalVM Native Image is an ahead-of-time compilation technology that converts your Java application into a self-contained executable file that does not require a JDK to run, is fast to start and efficient.
GraalVM Native Image is pre-installed on the remote host.
To begin, check that you have a compiled JAR file in your
target
dir:ls ./target drwxrwxr-x 1 krf krf 4096 Mar 4 11:12 archive-tmp drwxrwxr-x 1 krf krf 4096 Mar 4 11:12 classes drwxrwxr-x 1 krf krf 4096 Mar 4 11:12 generated-sources -rw-rw-r-- 1 krf krf 496273 Mar 4 11:38 graalvmnidemos-1.0-SNAPSHOT-jar-with-dependencies.jar -rw-rw-r-- 1 krf krf 7894 Mar 4 11:38 graalvmnidemos-1.0-SNAPSHOT.jar drwxrwxr-x 1 krf krf 4096 Mar 4 11:12 maven-archiver drwxrwxr-x 1 krf krf 4096 Mar 4 11:12 maven-status
The file you will need is
graalvmnidemos-1.0-SNAPSHOT-jar-with-dependencies.jar
.Generate an executable file from the command line. You do not need to use the Maven plugin to use GraalVM Native Image, but it can help. Run the following from the root directory of the project,
demo
:native-image -jar ./target/graalvmnidemos-1.0-SNAPSHOT-jar-with-dependencies.jar --no-fallback -H:Class=oracle.App -H:Name=file-count
This will generate an executable file called
file-count
within the current directory.Run this executable file as follows:
./file-count
Now time the application. First by running it as an executable file and then by using the regular
java
command:time ./file-count
time java -cp ./target/graalvmnidemos-1.0-SNAPSHOT-jar-with-dependencies.jar oracle.App
The executable file, generated by the
native-image
command, runs significantly faster than the corresponding Java application.
Let's dig a bit deeper into how you created the executable file.
What do the parameters you passed to the native-image
command in step 2 specify?
-jar
: Specify the location of the JAR file containing the Java application. (You can also specify the classpath with-cp
.)--no-fallback
: Do not generate a fallback image. (A fallback image requires a JVM to run it, and you do not need this.)-H:Class
: Specify the class that provides the entry point method (themain
method).-H:Name
: Specify the name of the output executable file.
The full documentation can be found here .
You can also run the native-image
tool using the GraalVM Native Image Maven plugin. The
project pom.xml
(Maven configuration) file contains the following snippet that demonstrates how to build
an executable file using the plugin:
<!-- Native Image -->
<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>
</executions>
<configuration>
<skip>false</skip>
<imageName>${exe.file.name}</imageName>
<mainClass>${app.main.class}</mainClass>
<buildArgs>
<buildArg>--no-fallback</buildArg>
<buildArg>--report-unsupported-elements-at-runtime</buildArg>
</buildArgs>
</configuration>
</plugin>
The Native Image Maven plugin performs the heavy lifting of building the executable file. You can disable it
using the <skip>true</skip>
tag. Note also that you can pass parameters to native-image
through the <buildArgs/>
tags.
The full documentation on the GraalVM Native Image plugin can be found here .
To build the executable file using the Maven profile, run:
mvn clean package -Pnative
The Maven build places the executable file, file-count
, into the target
directory.
You can run the executable file as follows:
./target/file-count
STEP 4: Using Reflection - Adding a Dependency to Log4J
In this step, you will build an executable file that that works with the dynamic features of Java.
Say you want to add a library, or some code, to your application that relies upon reflection. A good candidate for
testing reflection is the Log4J logging framework. It is already added as a dependency in the project
pom.xml
file:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
To change the application so that it uses log4j
, edit the ListDir.java
file and uncomment
a few lines.
Open the
ListDir.java
file, using VS CodeUncomment the line that declares the
log4j
import and then uncomment the following lines://import org.apache.log4j.Logger;
//final static Logger logger = Logger.getLogger(ListDir.class);
/* // Add some logging if(logger.isDebugEnabled()){ logger.debug("Processing : " + dirName); } */
/* // Add some logging if(logger.isDebugEnabled()){ logger.debug("Processing : " + f.getAbsolutePath()); } */
Save the file
Now that you have added logging to your application, you can view the result of your changes by rebuilding and running.
mvn clean package exec:exec
You should see the same kind of output as you saw earlier, but with the addition of more logging.
Next, build an executable file using the Maven profile:
mvn clean package -Pnative
Run the executable file you built, that now contains logging:
./target/file-count
This generates an error:
Exception in thread "main" java.lang.NoClassDefFoundError at org.apache.log4j.Category.class$(Category.java:118) at org.apache.log4j.Category.<clinit>(Category.java:118) at java.lang.Class.ensureInitialized(DynamicHub.java:552) at oracle.ListDir.<clinit>(ListDir.java:75) at oracle.App.main(App.java:63) Caused by: java.lang.ClassNotFoundException: org.apache.log4j.Category at java.lang.Class.forName(DynamicHub.java:1433) at java.lang.Class.forName(DynamicHub.java:1408) ... 5 more
What just happened here?
Working with the Dynamic Features of Java
This exception is caused by the addition of the Log4J library because it relies on reflection.
The native-image
tool performs an aggressive static analysis to see which classes are used within the application.
For any classes not used, the tool will assume that they are not needed. This is called the "closed world" assumption -
everything that needs to be loaded must be known when building an executable file. If it is not findable by static analysis, then
it will not be included in the executable file.
Reflection is a core feature of Java, so how can you use reflection and take advantage of the speed-ups offered by
GraalVM Native Image? You need a way to let the native-image
tool know about any uses of reflection.
Luckily, the native-image
tool is able to read in configuration files that specify all
classes that are referenced through reflection.
You can do this manually, or the Java tracing agent that comes with the GraalVM Java runtime can do this for you. The agent generates JSON files that record all instances of reflection, JNI, proxies, and resources access that it can locate while your application is running.
Note : It is important to exercise all the code paths in your application when running the tracing agent in order to ensure that all cases of reflection are identified.
The complete documentation for the tracing agent can be found here .
STEP 5: Using the Tracing Agent
Now use the tracing agent to generate the reflection configuration while you run your application.
Run the application with the tracing agent:
java -agentlib:native-image-agent=config-output-dir=./src/main/resources/META-INF/native-image -cp ./target/graalvmnidemos-1.0-SNAPSHOT-jar-with-dependencies.jar oracle.App
Have a look at the configuration files that the tracing agent created:
ls -l src/main/resources/META-INF/native-image/
You should see the following included in the generated output:
total 56 -rw-r--r-- 1 kfoster staff 4B Dec 2 19:13 jni-config.json -rw-r--r-- 1 kfoster staff 86B Nov 9 20:46 native-image.properties -rw-r--r-- 1 kfoster staff 65B Dec 2 19:13 predefined-classes-config.json -rw-r--r-- 1 kfoster staff 4B Dec 2 19:13 proxy-config.json -rw-r--r-- 1 kfoster staff 521B Dec 2 19:13 reflect-config.json -rw-r--r-- 1 kfoster staff 101B Dec 2 19:13 resource-config.json -rw-r--r-- 1 kfoster staff 4B Dec 2 19:13 serialization-config.json
Note: The project contains a Maven profile that can do this for you. Run the following command to use the tracing agent:
mvn clean package exec:exec -Pjava_agent
Now re-build the executable file again. This time the configuration files produced by the tracing agent will be applied:
mvn package -Pnative
Finally, execute the generated file:
time ./target/file-count
The executable file works and produces log messages to the output, as expected.
This works because the files generated by the tracing agent have recorded the classes that are referenced by reflection.
The native-image
tool now knows that they are used within the application and therefore does not exclude them from the
generated executable file.
Note on the position of the -agentlib
param
Note that the agent parameters must come before any -jar
or -classpath
parameters. You should also specify a
directory into which to write the files. The recommended location is under src/main/resources/META-INF/native-image
.
Files placed in this location are picked up automatically by the native-image
tool.
Note on Configuring the Generation of the Native Executable
You can also pass parameters to the native-image
tool using a Java properties file (by default src/main/resources/META-INF/native-image/native-image.properties
). There is an example file in the demo
directory to give you an idea of what you can do with it.
Conclusions
In this lab, you have tried out several GraalVM Native Image features:
- How to build a native executable for a Java application
- How to use Maven to build a native executable
- How to use the tracing agent to automate the process of tracking and recording reflection
Write efficient, more secure, and instantly-scalable cloud native Java applications with GraalVM Native Image!