GraalVM, Reflection and Native Image

3
0
Send lab feedback

Understanding Reflection and GraalVM Native Image

Introduction

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

GraalVM Native Image allows the ahead-of-time compilation of a Java application into a self-contained native executable. With GraalVM Native Image only the code that is required by the application at run time gets added into the native executable.

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

  • Use 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 (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.

Plus, 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 GraalVM Enterprise at no additional 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

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.

STEP 1: Connect to a Virtual Host and Check the Development Environment

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 teh 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!

STEP 2: The Closed World Assumption

Building standalone executable with the native-image tool that comes with GraalVM is a little 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 runtime must be known (observed and analysed) at build time, i.e., when the native-image tool is building the standalone executable.

Before we continue it is worthwhile going over the build / run model for applications that are built with GraalVM Native Image.

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

But, what really happens during step 2?

Firstly, the native-image tool performs an analysis to see which classes within your application are reachable. We will look at this in more detail shortly.

Secondly, found classes, that are known to be safe to be initialised (Automatic Initialization of Safe Classes ), are initialised. The class data of the initialised classes is loaded into the image heap which then, in turn, gets saved into standalone executable (into the text section). This is one of the features of the GraalVM native-image tool that can make for such fast starting applications.

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

We said we would return to the topic of reachability. As was 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 (see ), but there are cases that it won't be able to pick up.

In order to deal with the dynamic features of Java the analysis needs to be told about what classes use reflection, or what classes are dynamicaly loaded.

Lets take a look at an example.

STEP 3: An Example Using Reflection

Imagine you have the following class, ReflectionExample.java (a copy of this can be found in the directory, demo/ReflectionExample.java):

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);
    }
}

Firstly, create a Terminal within VS Code. This is done from, Terminal > New Terminal

Next, let's build the code. In your shell, from within VS Code, run the following command:

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.

Let's run it and see what it does.

java ReflectionExample StringReverser reverse "hello"

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

OK, but what happens if we try to build a native image out of program? Let's try it. In your shell run the following command:

native-image --no-fallback ReflectionExample

NOTE: The --no-fallback option to native-image causes the build to fail if it can not build a stand-alone native executabale.

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

./reflectionexample StringReverser reverse "hello"

Exception in thread "main" java.lang.ClassNotFoundException: StringReverser
	at com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:60)
	at java.lang.Class.forName(DynamicHub.java:1214)
	at ReflectionExample.main(ReflectionExample.java:21)

What happened here? It seems that our native executable was not able to find the class, StringReverser. How did this happen? By now, I think we probably have an idea why. The Closed World assumption.

During the analysis that the native-image tool performed, it 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 to shrink the code that is built by only including classes that are known to be used. As we have just seen, this can casue issues with reflection, but luckily there is a way to deal with this.

STEP 4: Introducing Native Image Reflection Config

We can tell the native-image build tool about instances of reflection through special configuration files. These files are written in JSON and can be passed to the native-image tool through the use of flags.

So, what other types of configuration information can we pass to the native-image build tool? The tooling currently supports reading files that contain details on:

  • Reflection
  • Resources - resource files that will be required by the application
  • JNI
  • Dynamic Proxies
  • Serialisation

We are only looking at how to deal with reflection in this lab, so we will focus on that.

The following is an example of what these files look like (taken from here ):

[
  {
    "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 we can see that classes and methods accessed through the Reflection API need to be configured. we can do this by hand, but the most convenient way to generate these configuration files is through use of the assisted configuration javaagent.

STEP 5: Native Image, Assisted Configuration : Enter The Java Agent

Writing a complete reflection configuration file from scratch is certainly possible, but the GraalVM Java runtime provides a java tracing agent, the javaagent, that will generate this for you automatically when you run your application.

Let's try this.

Run the application with the tracing agent enabled. In our shell run the following:

# Note: the tracing agent parameter must come before the classpath and jar params on the command ine
java -agentlib:native-image-agent=config-output-dir=META-INF/native-image ReflectionExample StringReverser reverse "hello"

Tracing Agent Config

Let's look at the configuration created:

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

You can run this process mutiple times and the runs are 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"

Building the standalone executable will now make use of the provided configuration. Let's build it:

native-image --no-fallback ReflectionExample

And let's see if it works any better:

./reflectionexample StringReverser reverse "hello"

It does!

Conclusions

Building standalone executables with GraalVM Native Image relies on the Closed World assumption, that is we need to know in advance, when building standalone executables, about any cases of reflection that can occur in our code.

The GraalVM platform provides a way to specify, to the native-image build tool, when reflection is used. Note: For some simple cases, the native-image tool can discover these for itself.

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

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

  • Use 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

We hope you have enjoyed this tutorial and have learnt something about how we can deal with reflection when using Native Image.

Learn More

SSR