DEV Community

Cover image for Spring Boot 3 and GraalVM
Max Beckers
Max Beckers

Posted on • Originally published at blog.worldline.tech

Spring Boot 3 and GraalVM

Spring Boot 3 comes with the 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 for cloud environments.

GraalVM encompasses up a great benefit but has also it’s challenges and disadvantages. In this blog post, 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 a lot of examples for the first steps.

However, let’s start with the prerequisites:

  • Install GraalVM: GraalVM - Getting Started
  • Install Native Image: Native Image - Getting Started. Short hint for Windows users: Native Image builds are platform dependent. This means that will only work in the platform specific command line (e.g. it will not work in Git Bash).
  • Install Docker to build and run the native images.

Next, it’s helpful to familiarize a bit with GraalVM. There are a few prepared demos of GraalVM in a git repository and I recommend to have a look 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.

In order to start with an own Spring Boot 3 application you just need the following plugin, which is also defined and can be copied from the demo.

<build>
  <plugins>
    ...
    <plugin>
      <groupId>org.graalvm.buildtools</groupId>
      <artifactId>native-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>
Enter fullscreen mode Exit fullscreen mode

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 the reason 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 the native image creation. The same applies to the profile nativeTest as a testing profile.

Generate metadata

With a clean or very small Spring Boot application it might work out of the box. However for most applications it will not work in this way 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 there is some metadata required for the application 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:

[
  {
    "name":"org.h2.Driver"
  }
]
Enter fullscreen mode Exit fullscreen mode

It would obviously 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:

  • jni-config.json
  • predefined-classes-config.json
  • proxy-config.json
  • reflect-config.json
  • resource-config.json
  • serialization-config.json

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 like 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/:

<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
    <buildArgs>
        <buildArg>-H:ConfigurationResourceRoots=path/to/resources/</buildArg>
    </buildArgs>
</configuration>
Enter fullscreen mode Exit fullscreen mode

With the metadata, run the build-image command again. You should now no longer see any more warnings because of reflection. In case of 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 by yourself 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:…

The 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 to initialize this class at runtime. Another way to force to initialize a class during build is to use the parameter --initialize-at-build-time.

<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
    <buildArgs>
        <buildArg>--initialize-at-build-time=my.build.package</buildArg>
        <buildArg>--initialize-at-build-time=my.other.build.package.SpecificClass</buildArg>
        <buildArg>--initialize-at-run-time=my.run.package</buildArg>
        <buildArg>--initialize-at-run-time=my.other.run.package.SpecificClass</buildArg>
    </buildArgs>
</configuration>
Enter fullscreen mode Exit fullscreen mode

Runtime errors

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 that 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 just 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 significant 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.

Smaller Images

The native executables are much smaller than the original Docker images. It only includes the needed and compiled code.

Security

The artifact is compiled during build, meaning that the artifact is immutable and that it is not possible to inject insecure code.

Challenges of using GraalVM

When managed all the issues with configuration and generating a native image there will come up more challenges to deal with.

Build time

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 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 some more challenges with migrating an existing Spring Boot application to GraalVM.

GraalVM does not support the @profile annotation. The background of that is that the compilation is done before running the application. Profiles change the behavior of the application what cannot be handled with the AOT compilation.

The same reason for other configurations that change if a bean is created or not like @ConditionalOnProperty.

Testing

So far Mockito is not supported for tests. This can bring up problems for a high number of existing applications and result in big test refactoring projects. There are two possible ways to get it running: either exclude all mocking tests or simply skip native tests with setting the configuration skipNativeTests to true:

<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
    <skipNativeTests>true</skipNativeTests>
</configuration>
Enter fullscreen mode Exit fullscreen mode

Conclusions

At first, GraalVM in the first steps is a very complex topic with a lot of challenges to manage. For now, there is no support for GraalVM for a lot of libraries. This support will make it even easier.

In terms of operating systems, I personally recommend using a mac or linux development environment. However, in 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 have a 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. The most microservices might be very easy to migrate, test and configure.
For larger applications, it would also be very useful, but it probably requires a lot of complex refactoring and configuring.

Top comments (0)