GraalVM, Reflection and Native Image

4
0
Send lab feedback

GraalVM, Reflection and Native Image

Introduction

This lab is for developers looking to understand more about how reflection works within GraalVM Native Image .

Native Image is a technology to compile Java code ahead-of-time to a binary—a native executable. With Native Image only the code that is required by the application at run time gets added to the executable.

These native executables have a number of important advantages, in that they:

  • Uses a fraction of the resources required by the JVM, so cheaper to run
  • Starts in milliseconds
  • Deliver peak performance immediately, no warmup
  • Can be packaged into lightweight container images for faster and more efficient deployments
  • Reduced attack surface

Many of the leading microservice frameworks support ahead-of-time compilation with GraalVM Native Image, including Micronaut, Spring, Helidon, and Quarkus.

There are Maven and Gradle plugins for Native Image to make building, testing, and running Java applications as native executables easy.

Note: Oracle Cloud Infrastructure (OCI) provides Oracle GraalVM at no cost.

Estimated lab time: 30 minutes

Lab Objectives

In this lab you will perform the following tasks:

  • Learn how to build Java code that uses reflection into standalone executables, using the native-image build tool.
  • Learn about the assisted configuration tooling available with GraalVM.

NOTE: Whenever you see the laptop icon, this is somewhere you will need to do something. Watch out for these.

# This is where we you will need to do something

STEP 1: Connecting to a Virtual Host

Your development environment is provided by a remote host: an OCI Compute Instance with Oracle Linux 8, 4 CPU, and 48GB of memory. The desktop environment will display before the remote host is ready, which can take up to two minutes.

Visual Studio Code (VS Code) will open and automatically connect to the VM instance that has been provisioned for you. Click Continue to accept the machine fingerprint.

VS Code Accept

If you do not click Continue, VS Code will popup a dialog box, shown below. Click Retry. VS Code will ask you to accept the machine fingerprint. Then click Continue.

VS Code Retry Connection

Issues With Connecting to the Remote Development Environment

If you encounter any other issues in which VS Code fails to connect to the remote development environment that are not covered above, try the following:

  • Close VS Code
  • Double-click the "Luna-Lab.html" icon on your desktop
  • Copy the "Configure Script" from the Resources tab and paste it into the Luna Desktop Terminal again
  • Repeat the above instructions to connect to the remote development environment

When you see the OS Keyring message for storing the encryption related data not being identified, choose "Use weaker encryption":

Use weaker encryption

Congratulations, you are now connected to a remote host in Oracle Cloud!

Next, open a Terminal within VS Code. The Terminal enables you to interact with the remote host. A terminal can be opened in VS Code via the menu: Terminal > New Terminal.

Note on the Development Environment

You will use Oracle GraalVM for JDK 24 as the Java environment for this lab.

You can easily check that by running these commands in your Terminal:

java -version

native-image --version

You can proceed to the next step.

STEP 2: Understanding Native Image Building

Building standalone executables with the native-image tool is different from building Java applications. Native Image makes use of what is known as the "closed-world assumption".

The closed-world assumption means all the bytecode in the application that can be called at run time must be known (observed and analysed) at build time, i.e., when the native-image tool is building a standalone executable.

The build / run model for GraalVM Native Image is the following:

  1. Compile your Java source code into Java bytecode.
  2. Using the native-image tool, build those Java classes into a native executable.
  3. Run the native executable.

What happens during step 2?

Firstly, the native-image tool performs an analysis to see which classes within your application are reachable.

Secondly, found classes that are known to be "safe" are initialised. The class data of the initialised classes is loaded into the image heap which then, in turn, gets saved into the standalone executable (into the text section).

NOTE: : Classes initialisation isn't the same as objects initialisation. Object initialisation happens during the runtime of a native executable.

As mentioned earlier, the analysis determines which classes, methods, and fields need to be included in the standalone executable. The analysis is static, that is it doesn't run the code. The analysis can determine some case of dynamic class loading and uses of reflection, but there are cases that it won't be able to pick up.

In order to deal with the dynamic features of Java, native-image needs to be told about what classes use reflection, or what classes are dynamically loaded.

Lets take a look at an example.

STEP 3: Running an Example Using Reflection

Imagine you have the following Java application, ReflectionExample.java (it is provisioned for you in VS Code):

import java.lang.reflect.Method;

class StringReverser {
    static String reverse(String input) {
        return new StringBuilder(input).reverse().toString();
    }
}

class StringCapitalizer {
    static String capitalize(String input) {
        return input.toUpperCase();
    }
}

public class ReflectionExample {
    public static void main(String[] args) throws ReflectiveOperationException {
        String className = args[0];
        String methodName = args[1];
        String input = args[2];

        Class<?> clazz = Class.forName(className);
        Method method = clazz.getDeclaredMethod(methodName, String.class);
        Object result = method.invoke(null, input);
        System.out.println(result);
    }
}

In the VS Code terminal, compile it:

javac ReflectionExample.java

The main method in the class ReflectionExample loads a class whose name has been passed in as an argument, a very dynamic use case! The second argument to the class is the method name on the dynamically loaded class that should be invoked.

Run it and see what it does.

java ReflectionExample StringReverser reverse "hello"

As expected, the method reverse on the class StringReverser was found, via reflection. The method was invoked and it reversed the input String of "hello".

But what happens if you try to build a native image for this application? Run the build command:

native-image ReflectionExample

Now run the generated native executable and see what it does:

./reflectionexample StringReverser reverse "hello"

You get a ClassNotFoundException:

Exception in thread "main" java.lang.ClassNotFoundException: StringReverser
  at org.graalvm.nativeimage.builder/com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:215)
  at org.graalvm.nativeimage.builder/com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:183)
	at java.lang.Class.forName(DynamicHub.java:1214)
	at ReflectionExample.main(ReflectionExample.java:21)

What happened here? It seems that the native executable was not able to find the class, StringReverser.

During the analysis, native-image was not able to determine that the class StringReverser was ever used. Therefore it removed the class from the native executable it generated. Note: By removing unwanted classes from the standalone executable, the tool shrinks the code by only including classes that are known to be used. This can cause issues with reflection, but luckily there is a way to deal with this.

STEP 4: Introducing Native Image Reflection Config

Reflection is a core feature of Java, so how can you use reflection and still take advantage of GraalVM Native Image? You can tell the native-image tool about instances of reflection through a special configuration file, reachability-metadata.json. This file is written in the JSON format and can be passed to the native-image tool using command-line options. The tool supports reflection, resource files, JNI, dynamic proxies, and serialization configuration.

This lab is only looking at how to deal with reflection.

The following is an example of reflection configuration:

[
  {
    "name" : "java.lang.Class",
    "queryAllDeclaredConstructors" : true,
    "queryAllPublicConstructors" : true,
    "queryAllDeclaredMethods" : true,
    "queryAllPublicMethods" : true,
    "allDeclaredClasses" : true,
    "allPublicClasses" : true
  },
  {
    "name" : "java.lang.String",
    "fields" : [
      { "name" : "value" },
      { "name" : "hash" }
    ],
    "methods" : [
      { "name" : "<init>", "parameterTypes" : [] },
      { "name" : "<init>", "parameterTypes" : ["char[]"] },
      { "name" : "charAt" },
      { "name" : "format", "parameterTypes" : ["java.lang.String", "java.lang.Object[]"] }
    ]
  },
  {
    "name" : "java.lang.String$CaseInsensitiveComparator",
    "queriedMethods" : [
      { "name" : "compare" }
    ]
  }
]

From this you can see that classes and methods accessed through the Reflection API need to be configured. To learn more, see Specifying Metadata with JSON .

STEP 5: Generating Configuration with the Agent

You can write the configuration by hand, but the most convenient and recommended way is to generate the configuration file using the assisted technology provided with GraalVM: javaagent. This Java agent will generate the configuration for you, automatically, when you run your application on the JVM.

Run the application with the agent enabled in the same terminal:

java -agentlib:native-image-agent=config-output-dir=META-INF/native-image ReflectionExample StringReverser reverse "hello"

Have a look at the configuration created:

cat META-INF/native-image/reachability-metadata.json
{
  "reflection": [
    {
      "type": "StringReverser",
      "methods": [
        {
          "name": "reverse",
          "parameterTypes": [
            "java.lang.String"
          ]
        }
      ]
    }
  ]

You can run this process multiple times. The new entries will be merged if you specify native-image-agent=config-merge-dir, as is shown in the example below:

java -agentlib:native-image-agent=config-merge-dir=META-INF/native-image ReflectionExample StringCapitalizer capitalize "hello"

The native-image should now make use of the provided configuration. Run the build again:

native-image ReflectionExample

Let's see if it works any better:

./reflectionexample StringReverser reverse "hello"

It does!

Learn more how to generate configuration with the Tracing agent from the documentation .

Conclusion

Building standalone executables with GraalVM Native Image relies on the closed-world assumption. Native Image needs to know in advance about any cases of reflection that can occur in the code.

GraalVM provides a way to discover uses of reflection and other dynamic Java features through the Tracing agent and can automatically generate the configuration needed by the native-image tool.

There are a few things you should bear in mind when using the Tracing agent:

  • Run your test suites. You need to exercise as many paths in your code as you can.
  • You may need to review and edit your config files.

Learn More

SSR