GraalVM Native Image Quick Start

9
0
Send lab feedback

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.

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

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

  3. When the instance is provisioned, this may take up to 2 minutes, you will see the following displayed on the Resources tab

    Luna Resources Tab

  4. 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.

    Copy Configuration Script

  5. Open a Terminal, as shown in the screenshot below:

    Open Terminal

  6. Paste the configuration code into the terminal, which will open VS Code for you.

    Paste Terminal 1

    Paste Terminal 2

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 the ListDir 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:

  1. native : This profile builds an executable file using GraalVM Native Image.
  2. 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.

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

    1. Cleans the project to remove any generated or compiled artifacts.
    2. Creates a runnable JAR file containing the application. This JAR file will be later used by Native Image.
    3. 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.

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

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

  3. Run this executable file as follows:

     ./file-count
  4. 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 (the main 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.

  1. Open the ListDir.java file, using VS Code

  2. Uncomment 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());
    }
    */
  3. 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.

  4. Next, build an executable file using the Maven profile:

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

  1. 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
  2. Now re-build the executable file again. This time the configuration files produced by the tracing agent will be applied:

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

  1. How to build a native executable for a Java application
  2. How to use Maven to build a native executable
  3. 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!

Learn More

SSR