GraalVM Native Images – Dependency Injection (part 3)

This post is part 3 of a series about GraalVM. In the previous post, I explained how you can create a simple REST API using GraalVM and native images. The first post in the series talks about setting up your environment and getting started.

Dependency Injection

For this section, we wanted to move more towards the functionality you’d expect from a Java EE application. We already have the basics for Webservices covered and for working with Spark. Let’s try to add Dependency Injection as a next step. We have two options here:

  1. Add a DI framework on top of Spark.
  2. Switch to another framework that includes DI.

Option one proved to be quite a challenge. Not going into details, I ran into a lot of issues related to the fact that GraalVM does not support reflection. I’ve had the most success using Google’s Dagger 2 framework, but did not get the native-image image running.

As for switching to a different framework, there are two large constraints that we needed to keep in mind. First off, we cannot use reflection easily. Secondly, we need to be able to create a fat-jar. And thirdly, we cannot rely on an external application servers. The combination of those three constraints already rules out both Spring and CDI, which are the most popular Java frameworks at the moment, both of which rely quite heavily on reflection.

There are some reports of success when using CDI with Standalone Weld (Weld SE) or Thorntail, both of which can create a fat-jar, but we couldn’t reproduce those results. The people at Pivotal are working on GraalVM compatibility with Spring 5, but this is still in an early development phase and seems to be mainly focused on the serverless and functional parts of Spring, Spring FU and JaFu/KoFu.

One of the newer frameworks and one that explicitly status GraalVM compatibility, is Micronaut. While still relatively new, it already offers almost the same functionality as Spring. A quick comparison between the two can be found here.

Micronaut

Micronaut uses it’s own cli tool to generate a starter project for you. You can grab the cli from their website or use SDKman to install it. You might need to fiddle a bit with the versions of GraalVM to get it working. I managed to roll back to rc8 and have it running stable. Again, you can use SDK to install and use a specific version of GraalVM:

$ sdk install java 1.0.0-rc8-graal$ sdk use java 1.0.0-rc8-graal

Make sure to also grab the correct version of substratevm with maven to match your GraalVM version. You’d expect this to be included in a pom file, but you might end up with a newer or incompatible version. Running the command below will make sure to manually grab the correct version:

$ mvn install:install-file -Dfile=${JAVA_HOME}/jre/lib/svm/builder/svm.jar     -DgroupId=com.oracle.substratevm -DartifactId=svm -Dversion=GraalVM-1.0.0-rc8 -Dpackaging=jar

Next you can create the sample app using the mvn command:

$ mn create-app hello-world --features graal-native-image 

This will generate a Gradle project for you that includes the basics for a Micronaut application. A few interesting things can be noted in your starter application.

Substitutions

In the source tree, you will find a MicronautSubstitutions.java file. This file includes a number of final classes with that are annotated with svm annotations. These are here to redirect or recompute specific fields in these classes when called by substratevm. Graal uses substratevm to build native images. Using these annotations allows a developer to work around the limitations of substratevm by manually pointing it in the right direction, or simply overwriting methods or fields. Micronaut includes the basics for their own applications in this java file so we don’t have to worry about it.

Dockerfiles

There are also two Dockerfiles present in the root directory. The file called Dockerfile will create a single docker image based on the oracle/graalvm-ce:1.0.0-rc8 base image, and includes the steps to create a working native-image for this image. The DockerfileAllInOne does roughly the same, but uses an intermediate builder image and will move the native-image to a small alpine based image to reduce the size of the final docker image. Let’s dive a bit deeper in these images, as they’re quite interesting.

The Docker Images

You will also notice that there are quite some more steps in the dockerfiles in order to build the native-image.

First off, we have the gradle build. Nothing special about that. We will use the shadowJar plugin to create our fat-jar in our ./gradlew build step. After some moving of files, we have the following line:

$ RUN java -cp hello-world-basics.jar io.micronaut.graal.reflect.GraalClassLoadingAnalyzer

This is one of the reasons Micronaut’s GraalVM compatibility is better then Spring. This step will generate a ReflectionConfigurationFile for us that we can pass to the native-image command as a parameter. The GraalVM compiler does not know which classes are needed when we use dynamic reflection calls in our code. For example, a Class.forName(someVariable) call can only be evaluated when we know what someVariable is. Being developers, we usually know this, so we can add those classes to the config file. That way the compiler knows what to expect and which classes to compile ahead of time. More configuration options are available for these files, but this should suffice for this example.

Once we’ve run the command you will find a reflect.json file in the build folder. If you want to do this by hand, you need to execute it as:

$ java -cp build/libs/hello-world-0.1-all.jar io.micronaut.graal.reflect.GraalClassLoadingAnalyzer

A quick look at it shows that it contains quite a number of entries that would be quite difficult or cumbersome to have configured by hand.

Next up it the actual command to build the native-image:

$ native-image --no-server --class-path ./build/libs/*-all.jar
-H:ReflectionConfigurationFiles=./build/reflect.json
-H:EnableURLProtocols=http
-H:IncludeResources='logback.xml|application.yml|META-INF/services/*.*'     
-H:+ReportUnsupportedElementsAtRuntime
-H:+AllowVMInspection
--rerun-class-initialization-at-runtime='sun.security.jca.JCAUtil$CachedSecureRandomHolder',javax.net.ssl.SSLContext                          
--delay-class-initialization-to-runtime=io.netty.handler.codec.http.HttpObjectEncoder,io.netty.handler.codec.http.websocketx.WebSocket00FrameEncoder,io.netty.handler.ssl.util.ThreadLocalInsecureRandom                          
-H:-UseServiceLoaderFeature
-H:Name=hello-world
-H:Class=hello.world.Application

That’s a mouthful. Let’s go through it step by step.

  • –no-server This will not start a build server and will instead build in the current session. Running it without this option will require a call to –server-shutdown to stop the build server and is more useful in a actual build setup, where you can re-use the resources of the build server.
  • –class-path Indicates what files to include in the build. Use -H:Class to point the class containing the main method. Alternately, we could use the -jar option and make sure we have a valid MANIFEST.mf file in our jar to point to the main method.
  • -H:ReflectionConfigurationFiles=/home/gradle/hello-world-basics/reflect.json Point to the reflection config file as discussed above.
  • -H:EnableURLProtocols=http Substratevm supports a number of protocols, a number of which are disabled by default. This makes sure we enable http for the image build. Only ‘file’ is enabled by default. The other options, ‘http’ and ‘https’, are disabled.
  • -H:IncludeResources=’logback.xml|application.yml|META-INF/services/.‘** Indicates what files to include in the build. We have a number of config files that we want included in our build. We can either make sure these are on our jar classpath by using gradle, or import them using this config option.
  • -H:+ReportUnsupportedElementsAtRuntime A debugging option that will report on unsupported features when encountered at runtime.
  • -H:+AllowVMInspection Another debugging option that will allow for heap dumps to be made.
  • –rerun-class-initialization-at-runtime Re-initializes the classes that are passed on runtime. Note that this will increase startup time. Might be useful if initialization doesn’t work compile time.
  • –delay-class-initialization-to-runtime Delays the initialization of passed classes until runtime. Note that this will increase startup time. Can be used if certain classes break the build.
  • -H:-UseServiceLoaderFeature GraalVM can automatically load services from the META-INF directory. This is disabled when using this option.
  • -H:Name The name of the native image executable
  • -H:Class The class containing the main method.

As you can see, there are quite the number of tuning and debugging options present in order to get a relatively simple Hello-world example running.

Finally, we can execute the command above to create our image. We can then run it as:

$ ./hello-world> 
15:16:14.827 [main] INFO  io.micronaut.runtime.Micronaut - 
Startup completed in 22ms. Server Running: http://localhost:8080

Now that we have the basics running, We can start adding our DI.

Finally, Injection!

Let’s try to keep this section simple. Start with adding two classes:

1. HelloWorldService
2. RestController

We’ll be keeping as close as possible to the options we get in Micronaut, without adding any additional frameworks.

Our RestController will look like:

package hello.world;
 
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
 
import javax.inject.Inject;
 
@Controller
public class RestController {
 
    @Inject
    HelloWorldService helloWorldService;
 
    @Get
    public String hello(){
        return helloWorldService.hello();
    }
 
}

12345678910111213141516171819

Our HelloWorldService will just contain the single hello() method:

package hello.world;
import javax.inject.Singleton;
@Singleton public class HelloWorldService {
  public String hello(){
    return "Hello from service";
    }
}

We can now build it the same way we did the previous example and run it again. We now should be able to do a curl to localhost:8080

$ curl localhost:8080> Hello from service

Let’s do some performance testing again. When we run the java app with java -jar app.jar, we can see to following numbers:

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       1
Processing:     0    1   0.4      0      12
Waiting:        0    1   0.4      0      12
Total:          0    1   0.4      1      12

Not bad for Java. Lets test the native-image executable:

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       1
Processing:     1    1   0.7      1      43
Waiting:        0    1   0.7      1      43
Total:          1    1   0.8      1      44

Some conclusions

It’s not as great as I would have liked to see. We’ve added complexity and this seems to have decreased performance to a point that it responds slower then its java counterpart. Keep in mind that we set quite a large number of classes to be (re-)initialized on runtime with our build parameters. Even when doing more tests, we did not see the large performance gain we saw on our simple Spark application and instead see quite the performance drop.

Another thing to keep in mind is that the setup is becoming a bit more cumbersome. We needed to add quite some parameters to generate the native image. While Micronaut will do some of the heavy lifting for you in generating the reflection config and classes, it can get a bit fiddly to get the entire thing to run.

In the next part of this series, I’ll try and see if I can add some more ‘enterprise functionality’ by adding a database to the setup. Personally, I’m quite interested in how deep this rabbit hole will take me!

References

This blog was posted earlier on rubix.nl here and on Thomas his personal blog here