I’ve been following GraalVM with a lot of interest. One of the interesting areas is its ability to compile bytecode Ahead-Of-Time, and create a native image. Such images have a lot of advantages, including small size, no dependency on a JRE, etc.
However, AOT has some limitations. In particular, the native image executable cannot compile what it doesn’t know about. This post aims to describe how to configure the compilation process when code is using reflection.
Let’s start with a trivial use-case:
a simple Class.forName()
call.
AOT compilation has no way of knowing about the class being loaded dynamically.
Hence, this class won’t be included in the native image, and launching the application will fail.
The output will be akin to the following:
java.lang.ClassNotFoundException: java.awt.Button at java.lang.Throwable.<init>(Throwable.java:287) at java.lang.Exception.<init>(Exception.java:84) at java.lang.ReflectiveOperationException.<init>(ReflectiveOperationException.java:75) at java.lang.ClassNotFoundException.<init>(ClassNotFoundException.java:82) at com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:51) at com.oracle.svm.core.hub.DynamicHub.forName(DynamicHub.java:906) at ch.frankel.blog.graal.Reflection.classForName(Reflection.java:6) at ch.frankel.blog.graal.Main.main(Main.java:7)
Fortunately, it’s possible to create a JSON file to explicitly let the compiler know:
[
{
"name" : "java.awt.Button"
}
]
To point the AOT compiler to the JSON file, use the -H:ReflectionConfigurationFiles
flag:
native-image -H:ReflectionConfigurationFiles=graal.json -jar target/graal-stubbing-1.0-SNAPSHOT.jar
At this point, the dynamic load works as expected.
Trying to dynamically create a new instance - clazz.newInstance()
, will fail as well without further configuration.
The error message is quite informative:
the no-parameter constructor has not been added explicitly to the native image
.
Let’s update the configuration file to make it work:
[
{
"name": "java.awt.Button",
"methods": [
{ "name": "<init>", "parameterTypes": [] }
]
}
]
The configuration syntax also offers some wildcard options:
|
Unfortunately, this also fails. Calling the constructor calls the parent constructor, which in turn starts a whole method call chain:
java.lang.ClassNotFoundException: java.awt.EventQueue at java.lang.Throwable.<init>(Throwable.java:287) at java.lang.Exception.<init>(Exception.java:84) at java.lang.ReflectiveOperationException.<init>(ReflectiveOperationException.java:75) at java.lang.ClassNotFoundException.<init>(ClassNotFoundException.java:82) at com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:51) at com.oracle.svm.core.hub.DynamicHub.forName(DynamicHub.java:906) at sun.awt.SunToolkit.initEQ(SunToolkit.java:120) at sun.awt.SunToolkit.createNewAppContext(SunToolkit.java:303) at sun.awt.AppContext$2.run(AppContext.java:277) at sun.awt.AppContext$2.run(AppContext.java:266) at com.oracle.svm.core.jdk.Target_java_security_AccessController.doPrivileged(SecuritySubstitutions.java:70) at sun.awt.AppContext.initMainAppContext(AppContext.java:266) at sun.awt.AppContext.access$400(AppContext.java:135) at sun.awt.AppContext$3.run(AppContext.java:321) at sun.awt.AppContext$3.run(AppContext.java:304) at com.oracle.svm.core.jdk.Target_java_security_AccessController.doPrivileged(SecuritySubstitutions.java:70) at sun.awt.AppContext.getAppContext(AppContext.java:303) at java.awt.Component.<init>(Component.java:988) at java.awt.Button.<init>(Button.java:151)
Looking at the source code reveals another reflection call.
public abstract class SunToolkit extends Toolkit {
private static void initEQ(AppContext var0) {
String var2 = System.getProperty("AWT.EventQueueClass", "java.awt.EventQueue");
EventQueue var1;
try {
var1 = (EventQueue)Class.forName(var2).newInstance(); (1)
} catch (Exception var4) {
var4.printStackTrace();
System.err.println("Failed loading " + var2 + ": " + var4);
var1 = new EventQueue();
}
var0.put(AppContext.EVENT_QUEUE_KEY, var1);
PostEventQueue var3 = new PostEventQueue(var1);
var0.put("PostEventQueue", var3);
}
}
1 | Aye, there’s the rub! |
The configuration needs to take that additional reflective call into account. The fixed file is the following:
[
{
"name": "java.awt.Button",
"methods": [
{ "name": "<init>", "parameterTypes": [] }
]
},
{
"name": "java.awt.EventQueue",
"methods": [
{ "name": "<init>", "parameterTypes": [] }
]
}
]
The next step is to directly access a field through reflection:
clazz.getDeclaredField("label")
.
The corresponding configuration file becomes:
[
{
"name": "java.awt.Button",
"methods": [
{ "name": "<init>", "parameterTypes": [] }
],
"fields": [
{ "name": "label" }
]
},
{
"name": "java.awt.EventQueue",
"methods": [
{ "name": "<init>", "parameterTypes": [] }
]
}
]
A recent commit named "Intrinsify reflective calls when the argument is a constant" will improve reflection auto-configuration:
At its most basic level, constants will be resolved automatically, with no need to create a configuration file, e.g.:
|
Conclusion
If your application uses reflection, the AOT compiler needs a configuration file to be explicitly told about dynamic loading/access. While the process itself is quite straightforward, the different method call chains need to be followed until the end. Depending on the application, it can be quite tedious and time-consuming.