The next innovation in Spring Boot: GraalVM Native Images

For many years, the Spring Boot framework has made it easy to develop and deploy java web applications. The ease of configuring spring beans through auto-configuration, the addition of active profiles and application properties and deploying jar files with embedded web containers have made life simple for java developers. But on the other hand, Spring web applications often start slowly due to the number of auto-configurations, many of which are unused or unnecessary. The memory footprint of such applications is often large; web servers or containers with a memory usage upwards to 1GB are quite common these days. GraalVM gives us a solution to these problems.

Erwin Manders
Erwin Manders
Solution Architect
February 9, 2023

GraalVM and Spring Boot 3.0

GraalVM gives us a solution to this problem by offering a JDK distribution that includes a native image builder to compile our java code into a native executable image. This image contains all classes and dependencies, but does not require a Java runtime to run. This image is platform-dependent; it needs to be created specifically for the system we want it to run on.

GraalVM requires static metadata to create a native image. This means that frameworks such as Spring Boot, which use auto-configuration technologies to start up, require extra steps to make the native image builder aware of these auto-configurations in advance. We call this process-aot. Spring Boot 3.0 introduces support for native images by default, which we will use to create Spring native images.

Getting started with GraalVM

The first step in getting started with GraalVM is installing a GraalVM JDK. For this purpose, we will use the Liberica GraalVM 22.3 distribution for JDK17. For more information about the Liberica Native Image Kit (NIK) visit: https://bell-sw.com/liberica-native-image-kit/

I recommend using an SDK manager to easily switch between GraalVM and other JDKs. For example:

sdk install java 22.3.r17-nik
JAVA

Create a fresh Spring Boot project from this link with parent version 3.0.0 or later. You can choose any language or build project you like. However, I will be using maven in the examples. With Spring Boot 3, the spring boot parent will have the native maven plugin included to help us build native images:

<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>build-native</id>
                            <goals>
                                <goal>compile-no-fork</goal>
                            </goals>
                            <phase>package</phase>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>
JAVA

This plugin will provide us with a profile that executes the two steps that we need: process-aot and build-image. If you have never used this native plugin before, you might need to add a version management in your project, to prevent maven from being unable to find the plugin.

Building the native image is made simple by this plugin; just run:

./mvnw clean package -Pnative
JAVA

This will run the process-aot and build-image steps and generate a binary image for your OS. Depending on your OS, run the native executable. For example, on MacOS:

$ target/springbootnativedemo
JAVA
Figure 1: Start-up of a MacOS native executable spring-boot application.
Figure 1: Start-up of a MacOS native executable spring-boot application.

Tip: if start-up is still slow on MacOS, MacOS might have issues resolving the connection to localhost. Make sure to add the name of your machine to /etc/hosts. For example, in my case Erwins-MacBook-Pro.local.

To run tests on a native image, the native maven plugin also supports running process-test-aot by using:

mvn clean package -Pnative,nativeTest
JAVA

Be aware that since building the native image can take upwards of several minutes depending on additional configuration you might have for the process-aot stage, running tests locally on a native image might not be optimal.

You have now learned how to generate a native image for your project. Unfortunately, this native executable works only on your OS, so of course this is not useful for deployment servers.

Limitations of native images

Since processing everything the application could do ahead of time would be incredibly slow, there are some limitations to native images that the ahead-of-time processor needs to be made aware of before using them, including but not limited to:

  • Reflection needs to be registered with runtime hints before being usable within a native application
  • Resource files need to be registered with runtime hints to be accessible within a native application
  • Active spring profiles cannot be changed at runtime anymore, so the advice is to stop using them, or process them ahead of time in the application.properties
  • Conditional beans cannot be changed at runtime, so conditional beans must be set ahead of time and properties that affect such auto-configurations need to be set ahead of time in application properties

To learn how to register these or other unmentioned limitations in the process-aot step for spring boot applications, check the official spring boot reference docs.

Building native images for deployment servers

If we want to utilize native images on our deployment servers or (cloud) containers, we will need to generate the native binary on a docker image or a build server. To show how to get started with that, we can use a dockerfile to create a container with maven, GraalVM and build-essentials for native images installed. For example, if you are using Ubuntu linux:

FROM ubuntu:latest

COPY . /app

WORKDIR /app

RUN apt-get -y update

RUN apt-get -y install maven

RUN apt-get -y install build-essential

RUN tar -xzf liberica-openjdk17.0.5-graalvm22.3.0.tar.gz
JAVA

You will need to download the liberica tar.gz file for GraalVM 22.3+ on JDK17 here.

Build and start the container:

Docker build –t springbootnative .

docker run -it springbootnative
JAVA

This will copy the app into a docker container and install necessary command line tools. The last line will unzip the GraalVM that we need. Last step is to set the JAVA_HOME and PATH variables to work with the GraalVM:

export JAVA_HOME=/app/bellsoft-liberica-vm-core-openjdk17-22.3.0

export PATH=/app/bellsoft-liberica-vm-core-openjdk17-22.3.0/bin/:$PATH
JAVA

This should allow you to run the build once more:

./mvnw clean package -Pnative
JAVA

You can also run the shell script that was imported into the container to run these steps.

Run the native Ubuntu image like:

$ ./target/springbootnativedemo
JAVA
Figure 2: Start-up of an Ubuntu native executable spring-boot application on a Docker container
Figure 2: Start-up of an Ubuntu native executable spring-boot application on a Docker container

You have now generated a native image for Ubuntu Linux! We can run this binary on any comparable Linux OS system without the need for any other libraries, JDK or any content of our docker container. You can export this native binary to be executed on an Ubuntu deployment server. To do this, you must look up the name of the container and copy the executable to the host:

Docker ps

docker cp <containerId>:/app/target/springbootnativedemo /host/path/target
JAVA

Shipping libraries with static images

Unfortunately, this native binary will only work on Ubuntu distributions that are compatible with the Ubuntu version used in the dockerfile. This means that with this type of image, you need to have control over the OS version of your deployment server or have build servers with identical OS as the deployment server.

If this is not the case, we must export the OS libraries as part of the native binary we create. We call this mostly static, or static images. A mostly static image is created by adding an optional parameter to the arguments in the maven plugin (overriding the parent is required to do this):

native-image -H:+StaticExecutableWithDynamicLibC
JAVA

This will not work for some libraries such as zlib. If your native executable fails to run due to incompatibility of zlib libraries, you must create the native image using a toolchain.

For more information about static images, click here.

Conclusions

If start-up times, memory footprint or processing times of spring-boot or java apps, have been holding you back on delivering non-functional requirements to your customers, GraalVM native images can be a solution for your application.

However, the time invested in building and maintaining the right native image compatible with your environment might vary depending on what sort of operations your team uses. If you control the OS yourself, building the right native binary is easy, but if this isn't the case, more time is needed to create static images.

Depending on the complexity of your application, you might need to investigate which parts to stop using. One example is the active profiles, since they are only registered at compile time and cannot be changed. Other limitations such as resource files or auto-configuration must be configured ahead of time.

Additional resources

Discover more articles

About the author

Erwin Manders
Erwin Manders
Solution Architect
Erwin has been working for Rabobank for 3 years. For years Erwin has worked on various cloud technologies and likes to dedicate his time to improving himself and the people around him. Erwin is a real 'tech guy' with knowledge of various backend and frontend technologies and is enthusiastic about software architecture as well. In his spare time, he likes to go for a run and participates in various semi marathons and trail runs.