GraalVM – Native Images – REST API

This port is part of the GraalVM series I’m writing. In the previous post, which can be found HERE, I talked about what GraalVM is and gave a basic example on how to get started. In this post we’ll investigate the possibility to create a simple REST API in Java using GraalVM.

Please make sure you’ve installed GraalVM correctly if you want to follow along. The previous post from this series contains the details to get everything setup.

Getting started and limitations

When creating a normal Java application, we would have a few options on getting a quick REST API setup. GraalVM however, still has it’s limitations. As listed in the last post, the main issue is Reflection and dynamic classloading. As GraalVM is being constantly developed, there can be some issues with regression, where a newer GraalVM version will not work with your chosen framework. Unless stated otherwise, I’ll be using GraalVM 1.0.0-rc8.

Well then, let look at the usual candidates for our application and quickly explore why we cannot use them.

JavaEE (or Jakarta EE)

Plain JavaEE will just not work. JavaEE applications require an implementation of the JavaEE API and can thus by default not be ran as a standalone Jar.

We could try to use Thorntail to provide this JavaEE implementation and to provide an embedded application server to run it as a standalone fat-jar. We will quickly run into an issue though, as Thorntail relies quite heavily on Reflection and dynamic class loading. There are option to work around this in GraalVM, but this gets messy quite quickly. I’ll come back to this workaround a bit later in the post.

Spring

Spring is the usual analog to JavaEE that does not require the implementation of the JavaEE api to work. I personally love Spring and Spring Boot, but again we will run into an issue when trying to use Spring in GraalVM’s native image. Just like Thorntail, or even more so, Spring relies on Reflection for most of it’s internal workings. Pivotal, the company behind Spring, does promise compatibly with GraalVM in the latest Spring 5 release. At the moment of writing, spring-fu and SpringR are a few examples that make a simple Spring application work in GraalVM. More complex applications will however quickly run into issues.

Micronaut and Quarkus

Both Micronaut and Quarkus are relatively new Frameworks in the block that promiss to be a Spring Boot and Thorntail analog, without using Reflection.

I’ve had some success with Micronaut in the past and I’ll likely get back to it in a later post. Micronaut offers a number of tools that make creating a GraalVM compatible application quite easy. The GraalCloassLoadingAnalyzer will generate your reflection-config.json. This file indicates GraalVM’s JVM what classes to load and how to wire reflection ahead of time in order to circumvent dynamic classloading.

Quarkus promises to work out-of-the box with GraalVM without any workaround of fixes. Quarkus is even new then Micronaut, but has already gained sponsorship from RedHat and might be a the way forward. I’ll investigate Quarkus in a later post.

Lets start small: Javalin

Looking at our options, we have some larger frameworks like Micronaut and Quarkus that can be used. Both cover more then just a simple REST API. For this first foray into GraalVM native-image, it might be a bit to much.

I’ll start small and use a framework that does one thing and does it well: Javalin. Javalin is a lightweight web framework that allows you to create a REST API with only a few lines of code. They offer a nice tutorial on how to use Javalin with GraalVM that you can find HERE.

As you can see, we’re still running into issues with Reflection and we still need to generate or create our reflection-config.json file. While workable for the small application we created, it’ll get more and more complex if we start to add more functionality.

Luckily, we have an alternative: Spark

Spark to the rescue

After some searching the webs for ways to create a simpel REST application with just a simple framework, I ran into Spark. Spark is a mico-framework and is even smaller in scope then Javalin.

We can get started by adding just the following two lines as a dependency in our pom.xml:

<dependencies>
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-core</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>

Don’t forget to also add the maven-assemply plugin, so we can pack everything into a fat-jar:

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<finalName>${project.artifactId}</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>nl.rubix.graalvm.graalvmrest.rest.Main</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

Adjust the mainClass element to fit your project. You’ll likely only have a single .java file. Make sure this file contains the following main method:


import static spark.Spark.get;
import static spark.Spark.port;

public class Main {
    public static void main(String[] args) {
        port(8080);
        get("/", (request, result) -> "Hello world!");
    }
}

And that’s it! Run a mvn package or mvn install to create your .jar file and run it. You should be able to call localhost:8080 and our “Hello world!” back.

Now, let’s also create our native image. I want to run both on the same machine at the same time, so I adjusted the port() call in our main method to use port 8081.

$ native-image -jar ./target/graalvm-rest.jar

This should give you an executable called “native-image” that you can call.

Let’s compare Performance!

I like to use apache’s ab tool, or ApacheBench, to benchmark performance. If it’s not already on your system, just install apache2-utils to get it. If you’re running on Windows, you can grab it as part of XAMPP. Find the executable in xamppapachebinab.exe

Now, let’s compare our jar file and native image. Note that both where compiled using GraalVM’s JDK. I’ll use ab to make 2500 requests to both endpoints like so:

$ ab -n 2500 http://localhost:8080/

$ ab -n 2500 http://localhost:8081/

Our Fat-Jar

The application is started under a ms. The first batch of ab testing gave me the following results:

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 0 1 2.7 1 132
Waiting: 0 1 2.6 0 130
Total: 0 1 2.7 1 132
Percentage of the requests served within a certain time (ms) 50% 1
66% 1
75% 1
80% 1
90% 1
95% 2
98% 2
99% 3
100% 132 (longest request)

While 99% of the requests where make under 3 ms, the longest still took over full second. A did a new batch on the already running application:

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 0 0 0.2 0 5
Waiting: 0 0 0.2 0 5
Total: 0 0 0.2 0 6
Percentage of the requests served within a certain time (ms) 50% 0
66% 0
75% 0
80% 0
90% 1
95% 1
98% 1
99% 1
100% 6 (longest request)

Well, that’s interesting. Looks like the first request after a cold boot will take some time, but any request afterwards will be quite fast. On to our native image

Native Image

Again, the application starts quite fast. Lets do our little test:

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 0 0 0.1 0 1
Waiting: 0 0 0.1 0 1
Total: 0 0 0.1 0 1
Percentage of the requests served within a certain time (ms) 50% 0
66% 0
75% 0
80% 0
90% 0
95% 0
98% 1
99% 1
100% 1 (longest request)

That’s on a cold boot! No additional wait time for the first result, just fast from the get-go!

Some conclusions

It looks like our native-image variant is comparable to our Fat Jar in response times, with both responding under 1 ms for most of the requests.

The Fat Jar variant requires some warmup and will take some additional time on the first request. The native image executable is noticeably larger in file size, but that’s to be expected with GraalVM. We need to include all relevant parts of the JDK in our native image for it to run on any system.

Just for fun, take the graalvm-rest executable from our source system and copy it to a different system without Java installed. You’ll need to use the same architecture and base OS on the new system as the source system. So no copying a Linux native-image to Windows (yet). You’ll notice you can run it without having a JDK installed.

Let’s see how far we can take this! In the next few posts I’ll try to expand on this first application and add some functionality. I’ll also have a look at Micronaut and Qaurkus, and see what new possibilities they bring to the table.