Using Graal AOT for a realistic project?
TL;DR maybe for some projects, not quite yet for everything
Graal is described as a High-performance polyglot VM. Its homepage describes it as: “GraalVM is a universal virtual machine for running applications written in JavaScript, Python, Ruby, R, JVM-based languages like Java, Scala, Kotlin, and LLVM-based languages such as C and C++.”
That says a lot, but still very little. After reading that you might be tempted to ask “Wait, what does it do?”. The answer to that is, “A lot”. But I’m going to focus only on a single feature, the Ahead-Of-Time compiler for Java.
In case the implications aren’t clear, Graal allows you to take your Java byte code, compile it to a native binary and run it standalone.
And while Graal is busy doing that it also creates a very small binary both in terms of disk space and RAM usage. If that sounds good you can read everything about Graal on its website.
Graal doesn’t have support for everything in Java yet. There is a list of limitations you can read here.
Hello, world!
First download the installation archive from the website and unpack into a directory. I choose the enterprise edition for this evaluation. Then setup your path to include the Graal bin directory. On my mac this is done this way: export PATH=/path/to/graal/Contents/Home/bin:$PATH
The command we are looking for is called native-image. But we will get to that later. First, we create a Maven project with a single hello-world main:
public class Main {
public static void main(String[] args) {
System.out.println(“Hello, world!”);
}
}To make this more real we will configure Maven to use the shader to create a fat-jar and configure the main class:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>${project.mainClass}</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>By default maven includes the version in the name of the final jar. This is just some test code and a version will only get in the way, so we override the finalName setting to make things easier: <finalName>${project.artifactId}</finalName>.
We now type mvn -q package on the command line and we have our shaded jar at target/graaltest.jar.
An error to get around
Before we start running native-image I need to inform you about a rather thorny error I ran into. When you run the standard native-image command
native-image -jar your.jarGraal will use a build server to perform the work. When you run such a build, the server will compile not only the code that exists now but might also include code from previous builds.In other words; stuff you deleted will remain and can cause errors.
There is a non-standard command line option called
--no-serverwhich will tell the compiler not to use a server. I have not run into any left-over code causing errors while using this option.
Finally, we run native-image on our jar:
$ native-image --no-server -jar target/graaltest.jar
classlist: 2,540.67 ms
fatal error: java.lang.UnsupportedClassVersionError: graaltest/Main has been compiled by a more recent version of the Java Runtime (class file version 54.0), this version of the Java Runtime only recognizes class file versions up to 52.0Our first hurdle. Graal support does not extend beyond Java 8, but Maven uses JAVA_HOME which on my machine is configured for Java 10. There are many benefits to using Java 10, but I particularly love local type inference. To use Graal, we will have to give up these benefits and revert to Java 8.
Is this acceptable for production code? Perhaps. We did use Java 8 for a long time and things got done. For now, I’m just going to downgrade and see how far I can get.
We install Java 8 and configure the shell to point to the right installed version. There is a helpful command called /usr/libexec/java_home. The -V flag lists the installed versions:
$ /usr/libexec/java_home -V
Matching Java Virtual Machines (4):
11, x86_64: "Java SE 11-ea" /Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home
10.0.2, x86_64: "Java SE 10.0.2" /Library/Java/JavaVirtualMachines/jdk-10.0.2.jdk/Contents/Home
9.0.1, x86_64: "Java SE 9.0.1" /Library/Java/JavaVirtualMachines/jdk-9.0.1.jdk/Contents/Home
1.8.0_77, x86_64: "Java SE 8" /Library/Java/JavaVirtualMachines/jdk1.8.0_77.jdk/Contents/Home/Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home
Next, we can return the home directory for a given Java version with the -v switch followed by the chosen version. Notice that the first switch used the uppercase V while the second uses the lowercase v.
We set the output into JAVA_HOME and ask java for its version:
$ export JAVA_HOME=$(/usr/libexec/java_home -v 1.8.0_77)
$ java -version
java version "1.8.0_172"
Java(TM) SE Runtime Environment (build 1.8.0_172-b11)
GraalVM 1.0.0-rc5 (build 25.71-b01-internal-jvmci-0.46, mixed mode)The setup is a little messy. Maven will now run using Java 1.8.0_77 because it uses the environment variable, while Graal uses Java 1.8.0_172 because it has that built in. The patch level difference shouldn’t matter and I did not run into problems. Let’s see what happens when we compile the jar now.
$ mvn -q package
$ native-image --no-server -jar target/graaltest.jar
<snip />
$ ll graaltest
-rwxr-xr-x 1 jurgen staff 5502160 Aug 14 16:32 graaltest*
$ ./graaltest
Hello, world!5.5MB for a hello-world is perhaps a little heavy. But remember, we are getting a ton of stuff in this, including a garbage collector and all the Java goodness.
It is great this works, but let’s do something a little tougher than just printing “Hello, world!”.
Serialization
Object serialization and de-serialization is a major part of any modern web app. To test this I will use Gson together with a bare bones POJO.
First, add the dependency:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>And next we add some code. We will put our POJO directly into our Main class:
public static final class Pojo {
public final String name;
public Pojo(final String name) {
this.name = name;
}
}Our main function will now be this:
public static void main(String[] args) {
final Gson gson = new Gson();
System.out.println(gson.toJson(new Pojo("Hello, world!")));
}When we run Maven, the shader will add all the Gson classes to the fat-jar. Graal is then able to compile the whole jar into a binary. All this happened without error. When we run it, we get:
$ ./graaltest
{}Not what we wanted. Serialization depends on reflection and that is a hard problem for an Ahead-of-time compiler such as Graal. However, Graal does have some support for reflection. There is a command line option where you can specify a configuration file that lists the classes you want to allow serialization for. This option looks like this:
native-image -H:ReflectionConfigurationFiles=/path/to/config.json --no-server -jar target/graaltest.jarThe configuration file is basically just a JSON array of objects that describe the classes. Each object has a name field that contains the class name and a number of booleans to include some things you are likely to want:
{
"name": "java.package.ClassName",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true,
}This is a good start but, as you might have noticed, there is no boolean to include all the fields. To make this work with our POJO we will have to include not only its name, but also the names of each of its fields. Like this:
{
"name": "graaltest.Main.Pojo",
"fields": [
{ "name": "name" }
]
}We tell native-image to use this configuration. This time, we get the following output:
classlist: 525.94 ms
(cap): 1,110.76 ms
setup: 1,159.18 ms
error: Error parsing reflection configuration in /Users/jurgen/Projects/GraalTest/src/main/graal-conf/pojo.json:
Class graaltest.Main.Pojo not found
Verify that the configuration matches the schema described in the -H:PrintFlags=+ output for option ReflectionConfigurationFiles.
Error: Processing image build request failedThe error is our fault. Graal works on class files, not on source files. Writing graaltest.Main.Pojoas one would in an import statement is wrong. Instead we have to write graaltest.Main$Pojo. We fix our mistake and we run the command again. This time, we get:
$ ./graaltest
{“name”:”Hello, world!”}Success! Mostly.
In a real project we don’t have just one single class with one single field. And remember that the compilation worked without the configuration. If we just add a field in a POJO somewhere we will not see the error. Our unit tests will pass because they run on the normal Java code. And integration tests might pass depending on the technology used.
The only way to see the error is to compile with graal and run a test on a running instance. Of course you have to trust that your tests cover every field, including the recently added ones. This method will be too labor intensive and too error prone to be usable. Fortunately, we can make things easier for ourselves; automation with a maven plugin.
Conceptually not complicated:
- Run after the compile phase
- Go to target/classes and walk the tree looking for .class files
- Filter out any files that don’t have a to-be-defined annotation on it
- Analyze the class files and find the class name and field names for each class
- Write a JSON file into
target/graal/pojos.json
I’ll call it GraalHelper and name the annotation @IncludeInGraalConfig.
<plugin>
<groupId>com.github.codemonstur</groupId>
<artifactId>graalhelper</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<goals><goal>generate-graal-configuration</goal></goals>
</execution>
</executions>
</plugin><dependency>
<groupId>com.github.codemonstur</groupId>
<artifactId>graalhelper-annotations</artifactId>
<version>1.0.0</version>
</dependency>
Next, we tag the POJO with the necessary annotation and run Maven:
$ mvn -q package
$ cat target/graal/pojos.json
[{"name":"graaltest.Pojo","allDeclaredConstructors":true,"allPublicConstructors":true,"allDeclaredMethods":true,"allPublicMethods":true,"fields":[{"name":"name"}]}]The plugin will now generate the necessary configuration. All we have to do is include this file when we run Graal, and serialization will work.
HTTP server
The next major part of a real-world application is an HTTP server.
There are many to choose from, both small and large. I personally like Undertow and use it whenever I can. We start a new blank project as before using the shader to create a fat-jar. Then we include the Undertow dependency and write a main function.
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId>
<version>2.0.13.Final</version>
</dependency>public static void main(final String... args) {
Undertow.builder()
.addHttpListener(8080, "0.0.0.0")
.setHandler(exchange -> exchange.getResponseSender().send("Hello, world!\n"))
.build().start();
}
Now we can compile, run and test the Java code:
$ mvn -q package
$ java -jar target/graaltest.jar
<snip>Xnio advertising</snip>$ curl http://localhost:8080/
Hello, world!
Works great. Time to compile it to a native binary.
$ native-image --no-server -jar target/graaltest.jar
classlist: 1,000.96 ms
(cap): 709.20 ms
setup: 949.84 ms
Detected unnecessary RecomputeFieldValue.ArrayBaseOffset com.oracle.svm.core.jdk.Target_java_nio_DirectByteBuffer.arrayBaseOffset substitution field for java.nio.DirectByteBuffer.arrayBaseOffset. The annotated field can be removed. This ArrayBaseOffset computation can be detected automatically. Use option -H:+UnsafeAutomaticSubstitutionsLogLevel=2 to print all automatically detected substitutions.
analysis: 2,394.68 ms
error: Error loading a referenced type: java.lang.NoClassDefFoundError: org/osgi/framework/FrameworkUtilThat’s unfortunate. Undertow appears to have a dependency on an OSGi framework. It is not immediately clear why this dependency is necessary and a reason for failing the compilation is hard to know without reading the code.
Undertow is considered ‘small’ but it is also full-featured and contains a lot of functionality. Let’s move on from Undertow and try another; Vert.x.
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>3.5.3</version>
</dependency>public static void main(final String... args) {
Vertx.vertx()
.createHttpServer()
.requestHandler(request -> request.response().end("Hello, World!\n"))
.listen(8080);
}
Compile, run and test.
$ mvn -q package
$ java -jar target/graaltest.jar$ curl http://localhost:8080/
Hello, World!
And now compile with Graal:
$ native-image --no-server -jar target/graaltest.jar
classlist: 1,421.78 ms
(cap): 693.13 ms
setup: 880.85 ms
analysis: 2,422.45 ms
error: Error loading a referenced type: java.util.ServiceConfigurationError: io.vertx.core.spi.VertxFactory: Error reading configuration file
Detailed message:
Error: Error loading a referenced type: java.util.ServiceConfigurationError: io.vertx.core.spi.VertxFactory: Error reading configuration file
Trace:
at parsing graaltest.Main.main(Main.java:10)
Call path from entry point to graaltest.Main.main(String[]):
at graaltest.Main.main(Main.java:10)
Original exception that caused the problem: java.util.ServiceConfigurationError: io.vertx.core.spi.VertxFactory: Error reading configuration file
at java.util.ServiceLoader.fail(ServiceLoader.java:232)
Caused by: java.io.FileNotFoundException: JAR entry META-INF/services/io.vertx.core.spi.VertxFactory not found in /Users/jurgen/Projects/GraalTest/target/graaltest.jar
... 40 moreError: Processing image build request failed
That’s not very hopeful. The error implies that a file is missing from the jar. But this is not the case.
$ unzip -l target/graaltest.jar | grep “META-INF/services/io.vertx.core.spi.VertxFactory”
35 08–15–2018 10:47 META-INF/services/io.vertx.core.spi.VertxFactoryThe people behind Vert.x have tried to get it to work, and they appear to have succeeded. In fact they went so far as to create a project setup builder that will create an empty project for you that will compile with Graal. The configuration necessary to get it to work is extensive.
Perhaps Undertow and Vert.x are still just too full featured to make this work. There is another HTTP server we can try; NanoHTTPd. This server bills itself as “Tiny, easily embeddable HTTP server in Java”. It is perhaps the smallest you can find. If we are going to have success compiling anything out-of-the-box this will be it.
We start a new project, add the dependency and write the minimal main function.
<dependency>
<groupId>org.nanohttpd</groupId>
<artifactId>nanohttpd</artifactId>
<version>2.3.1</version>
</dependency>public static void main(final String... args) throws IOException {
new NanoHTTPD(8080) {
public Response serve(final IHTTPSession session) {
return newFixedLengthResponse("Hello, world!\n");
}
}.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
}
Compile, run and test.
$ mvn -q package
$ java -jar target/graaltest.jar$ curl http://localhost:8080/
Hello, World!
Finally, we run Graal.
<snip time />
[total]: 9,411.00 msThe code compiled. .. Does it run?
$ ./graaltest$ curl http://localhost:8080/
Hello, World!
It works!
NanoHTTPd is not my favorite but I may be tempted to use it if all my other code works with Graal. There is also one more option; Netty. It appears people have been able to get Netty to work with Graal as well.
NanoHTTPd, Netty and a modified Vert.x, I call this a partial success.
Database querying
For a basic app, we need communication with a database. Let’s use MariaDB and perform a select on a test schema and table. We add the database dependency and code:
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.2.5</version>
</dependency>public static void main(final String... args) throws ClassNotFoundException, SQLException {
Class.forName("org.mariadb.jdbc.Driver");
try (final Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","")) {
final ResultSet rs = con.createStatement().executeQuery("SELECT * FROM test");
while (rs.next()) {
System.out.println(rs.getInt(1)+". "+rs.getString(2)+" "+rs.getString(3));
}
}
}
We do a Maven package and run the app.
$ mvn -q package
$ java -jar target/graaltest.jar
0. Hello, world!And again a compilation with Graal.
$ native-image --no-server -jar target/graaltest.jar
<snip time/>
[total]: 8,920.86 msThat is very hopeful. But it fails when we try to run it.
$ ./graaltest
Exception in thread "main" java.lang.ClassNotFoundException: org.mariadb.jdbc.Driver
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:934)
at graaltest.Main.main(Main.java:11)
at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:177)It looks like the driver was never included in the binary. Our shaded jar works fine because it isn’t minified, but Graal only includes those things it believes you use. The way we chose to use the driver doesn’t qualify.
Instead of loading the class using Class.forName(). We rewrite the code as:
org.mariadb.jdbc.Driver.class.getName();Both the compilation with Java and running the jar still work. Next, we compile the jar with Graal.
$ native-image --no-server -jar target/graaltest.jar
classlist: 1,647.45 ms
(cap): 1,844.17 ms
setup: 3,068.78 ms
analysis: 18,016.56 ms
error: unsupported features in 5 methods
Detailed message:Error: Bytecode parsing error: java.lang.NoClassDefFoundError: org/mariadb/jdbc/internal/util/PidFactory$CLibrary
Trace:
at parsing org.mariadb.jdbc.internal.com.send.SendHandshakeResponsePacket.writeConnectAttributes(SendHandshakeResponsePacket.java:218)
Original exception that caused the problem: java.lang.NoClassDefFoundError: org/mariadb/jdbc/internal/util/PidFactory$CLibrary
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)Error: Detected a started Thread in the image heap. This is not supported. The object was reached from a static initializer. All static class initialization is done during native image construction, thus a static initializer cannot contain code that captures state dependent on the build machine. Write your own initialization methods and call them explicitly from your main entry point.
Trace: object sun.awt.AWTAutoShutdown
method sun.awt.AWTAutoShutdown.getInstance()Error: Detected a started Thread in the image heap. This is not supported. The object was reached from a static initializer. All static class initialization is done during native image construction, thus a static initializer cannot contain code that captures state dependent on the build machine. Write your own initialization methods and call them explicitly from your main entry point.
Trace: object sun.java2d.opengl.OGLRenderQueue
field sun.java2d.opengl.OGLRenderQueue.theInstance
Error: Error loading a referenced type: java.lang.NoClassDefFoundError: com/sun/jna/Platform
Trace:
at parsing org.mariadb.jdbc.internal.com.send.SendGssApiAuthPacket.getAuthenticationMethod(SendGssApiAuthPacket.java:119)
Original exception that caused the problem: java.lang.NoClassDefFoundError: com/sun/jna/PlatformError: Processing image build request failed
The actual full error is much longer. Clearly the MariaDB driver doesn’t work with Graal. The MariaDB people are aware of the problem and are working on it, but no solution just yet.
What about PostgreSQL? Add a dependency and a main.
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.4</version>
</dependency>public static void main(final String... args) throws SQLException {
org.postgresql.Driver.class.getName();
try (final Connection con = DriverManager.getConnection("jdbc:postgresql://localhost:5432/test","root","")) {
final ResultSet rs = con.createStatement().executeQuery("SELECT * FROM test");
while (rs.next()) {
System.out.println(rs.getInt(1)+". "+rs.getString(2)+" "+rs.getString(3));
}
}
}
Compiling the Java code and running it works fine. What about compiling with Graal?
classlist: 2,327.36 ms
(cap): 1,471.33 ms
setup: 3,155.10 ms
analysis: 16,387.35 ms
error: unsupported features in 3 methods
Detailed message:
Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Unsupported method java.lang.ref.Reference.enqueue() is reachable: The declaring class of this element has been substituted, but this element is not present in the substitution class
To diagnose the issue, you can add the option -H:+ReportUnsupportedElementsAtRuntime. The unsupported element is then reported at run time when it is accessed the first time.
Trace:
at parsing org.postgresql.core.v3.Portal.close(Portal.java:30)
Call path from entry point to org.postgresql.core.v3.Portal.close(): Error: Processing image build request failed
Postgresql doesn’t work yet., howver there is active development on this issue by the developers of the driver.
Relational databases are one thing. What about something a bit more modern; Redis? We grab the latest Jedis and write a main.
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>public static void main(final String... args) {
try (final Jedis jedis = new Jedis("localhost", 6379)) {
jedis.set("message", "Hello, world!");
System.out.println(jedis.get("message"));
}
}
Compile, run and test.
$ mvn -q clean package
$ java -jar target/graaltest.jar
Hello, world!
$ native-image --no-server -jar target/graaltest.jar
<snip />
$ target/graaltest
Hello, world!It works! And surprisingly painless. Now we have a database, although maybe not the database we actually need for our project.
Conclusion
Is Graal production ready? Almost. We have an HTTP server, but perhaps not the one we wanted, we have serialization, using a little help, and we have a database, but perhaps not the one we need. We also do not have the latest version of Java. We are back to using Java 8.
Many features worked pretty much out-of-the-box. Regular Java code fully compiles, Gson and Jedis worked without issue. This is quite far already.
The annotation trick would be a nice enhancement to come with standard Graal. The annotation could be used during compilation, eliminating the need for a configuration file.