An in-depth overview of the many features
Spring Boot 3 comes with support for native images. This is the part for GraalVM. GraalVM transitions from a just-in-time (JIT) compiler built into OpenJDK to an ahead-of-time (AOT) compilation. As a result, it speeds up the startup time and reduces the memory usage of (Micro-)Services, improving the efficiency of cloud environments.
GraalVM encompasses great benefits but also has its challenges and disadvantages. In this article, I will highlight the features and give an in-depth overview.
Getting Started with GraalVM and Spring Boot 3
GraalVM provides a good level of documentation, including many examples for the first steps.
However, let’s start with the prerequisites:
- Install GraalVM: GraalVM — Getting Started
- Install Native Image: Native Image — Getting Started. A short hint for Windows users: Native Image builds are platform dependent. This means it will only work in the platform-specific command line (e.g., not in Git Bash).
- Install Docker to build and run the native images.
Next, it’s helpful to familiarize yourself a bit with GraalVM. There are a few prepared demos of GraalVM in a git repository, and I recommend looking into the Spring Boot example application GraalVM demo — spring native image. It’s a good first step to play a little bit around with GraalVM.
To start with your own Spring Boot 3 application, you need the following plugin, which is also defined and can be copied from the demo.
As a next step, you can try to build the native image with the maven target: mvn clean package -Pnative. As already mentioned, the builds of native images are platform dependent. This is why it is often helpful to use a docker image to build the native image.
Maven has three different goals for AOT processing and building the image:
mvn spring-boot:process-aot mvn spring-boot:process-test-aot mvn spring-boot:build-image
But these three commands are combined in the mvn clean package -Pnative.
Quick tip: the profile native is predefined in Spring Boot 3 for native image creation. The same applies to the profile nativeTest as a testing profile.
With a clean or very small Spring Boot application, it might work out of the box. However, it will not work in this way for most applications because of a GraalVM reflection incompatibility.
In this case, there will be error notifications such as the following:
Warning: Could not resolve org.h2.Driver for reflection configuration. Reason: java.lang.ClassNotFoundException: org.h2.Driver.
This means that the application requires some metadata to resolve this problem. The fix for that single problem would be very easy, but there might be a couple of these warnings. The metadata config for that would be in reflect-config.json:
It would be possible to create a configuration file by yourself and link it to the build. However, the more standard way (at least for larger artifacts) is to generate the configuration. For this case, just run the following command:
java -agentlib:native-image-agent=config-merge-dir=META-INF/native-image -jar target/my-application-1.0.0-SNAPSHOT.jar
This will generate a directory META-INF/native-image (can be replaced by any name) from the application target/my-application-1.0.0-SNAPSHOT.jar (replace by your jar name). When you run the application with this command, you should perform as many use cases as possible to get a complete configuration. Due to the config-merge-dir it is possible to run the application multiple times, and it will be merged. This command will generate in the folder the following files:
To make this metadata available during the build, add it to your classpath in a folder named META-INF/native-image. Alternatively, you can configure the path to the files with the following two properties, depending on if the config is on the classpath but not in META-INF/native-image it’s possible to use -H:ConfigurationResourceRoots=path/to/resources/ or otherwise when it’s outside of the classpath use -H:ConfigurationFileDirectories=/path/to/config-dir/:
With the metadata, run the build-image command again. You should now no longer see any more warnings because of reflection. In case of a reflection warning, the reason might be missing metadata not created by the command. As a fix, you can either run the application again and generate the metadata for the missing part or add the missing config to the metadata files.
An alternative quick fix could be the option –allow-incomplete-classpath. This ensures that the possible linking errors are shifted from build time to run time.
Class Initialization at the Wrong Time
The next challenge that might come up during the build is an error like
ERROR: Classes that should be initialized at run time got initialized during image building.
Most classes are initialized at build time, and GraalVM tries to find out what can be initialized at build time and which classes must be initialized at run time. This error can be fixed with the parameter –initialize-at-run-time. This parameter will force us to initialize this class at runtime. Another way to force a class initialization during the build is to use the parameter –initialize-at-build-time.
With the generated metadata and the fixed initialization time of the classes, the native image build should be successful. Nonetheless, at runtime, there could come up more errors. The most common one is the ClassNotFoundException. That means the configuration in the reflect-config.json is incomplete, and you should add the class. Another similar error is a FileNotFoundException because a file could not be located in the classpath. This means that the required file is missing in the resource-config.json.
Benefits of GraalVM
GraalVM is a very powerful tool with a lot of benefits. In the following section, I want to highlight the most important ones and summarize the main challenges.
Reduce the Startup Time
Using GraalVM makes sense for applications running in the cloud. For autoscaling mechanism on load peaks, it might be important to scale up very fast a new instance. With the significantly shorter startup time, the native image is a huge benefit.
Less Memory Usage
Less memory usage is another benefit of the GraalVM native images since less memory usage can reduce the hosting costs in a significant manner.
The native executables are much smaller than the original Docker images. It only includes the needed and compiled code.
The artifact is compiled during the build, meaning it is immutable, and it is impossible to inject insecure code.
Challenges of Using GraalVM
When managing all the issues with configuration and generating a native image, there will come up more challenges to deal with.
Because of the AOT compilation, the code is compiled during the build process. This will slow down the build process, although it is needed to minimize the startup time on the application run. For instance, the build time for one of my applications has increased from 3.11 minutes to 7.53 minutes.
Dynamic Code Changes
Because of the AOT compilation, there are more challenges with migrating an existing Spring Boot application to GraalVM.
GraalVM does not support the @Profile annotation. The background is that the compilation is done before running the application. Profiles change the application’s behavior, which cannot be handled with the AOT compilation.
The same reason for other configurations that change if a bean is created or not like @ConditionalOnProperty.
So far, Mockito is not supported for tests. This can bring up problems for many existing applications and result in big test refactoring projects. There are two possible ways to get it running: either exclude all mocking tests or skip native tests by setting the configuration skipNativeTests to true:
At first, GraalVM in the first steps is a very complex topic with many challenges to manage. For now, there is no support for GraalVM for many libraries. This support will make it even easier.
I recommend using a Mac or Linux development environment for operating systems. However, in the case of a Windows environment, you should use WSL2 because, for Windows, it is more complicated to get the setup for native images working.
Microservices in cloud environments require a short startup time and minimal memory utilization, so native images are the way forward for Spring Boot applications in this context. For this reason, it makes sense to look into this technology. For new projects, I highly recommend using GraalVM from the start, at least for a microservice or cloud architecture.
But what about existing applications? It depends. Most microservices might be very easy to migrate, test and configure. For larger applications, it would also be very useful, but it requires a lot of complex refactoring and configuring.
Originally published at https://blog.worldline.tech on June 6, 2023.