GraalVM is a JDK distribution, meaning it’s designed to accelerate the execution, minimize the memory footprint, and increase the startup time and throughout JVM-based applications, as well as other apps written in different languages, due to the polyglot capabilities of GraalVM.
Spring Native is a tool that offers the capability to perform ahead-of-time compilation and automates most of the native-required configurations necessary to build binary, executable applications through GraalVM native image integration.
This tutorial will show you how to build a small native image application using Spring Native and GraalVM as the runtime.
Note: This is not an introductory tutorial for GraalVM, so while some concepts will be introduced, others may be skipped in order to keep this tutorial as concise as possible.
This tutorial requires you to have installed GraalVM runtime and Docker. If you haven’t already installed these, I suggest you install them first, before continuing on with the tutorial. Please check this documentation:
If you haven’t already, install Sdkman, then install GraalVM via SdkMan.
no-name$ sdk java list //List the JDK versions available
no-name$ sdk install java 21.0.0.r11-gr
no-name$ sdk use java 21.0.0.r11-grl
Now, install Docker by following the instructions on this page.
Now that we have everything installed, let’s get started!
Step One: Project Setup
Let’s start by creating a basic Maven Project:
1. Add the Parent Project
Note: Spring Native only supports Spring Boot 2.4.5 or greater.
Tip: Spring Initializer makes this process really easy! You can also check to see if the added dependencies are supported by Spring Native by checking the HELP.md.
2. Spring Native Dependency
This contains the native configuration API and utility annotations to define the native configuration via annotation.
Note: Spring Native Hints are out of scope for this tutorial, so for now, it’s enough to know that these provide a type-safe way of declaring the configuration for some of the unsupported JDK features such as reflection, proxying, and more. Please review this page to learn more about Native Hints.
Note: We’re using 0.9.2 version of Spring Native
3. Native Image Configuration
The Spring Maven plugin handles the configuration to set up the buildpack type we’re going to use for the image build step. The “BP_NATIVE_IMAGE” env variable tells buildpack to use the native version that contains the GraalVM and the native-image add-on to compile and build the binary application.
Tip: Check out Paketo documentation to learn more about buildpacks.
4. Spring AOT Maven Plugin
GraalVM native-images uses a set of configuration files to define how to build the native application, as well as to statically declare the classes it should initialize and ship into the binary application. Those files are located in “META-INF/native-image”
Spring AOT is in charge of scanning the Spring projects added to the project and generates the required native configuration files by checking “HINT Annotations”
Step Two: Implementation
Since the main goal of this tutorial is to show the required steps and components of building Spring Native applications, we’re going to implement a very basic endpoint so that we can interact with the application.
Step Three: Image Build
You have two options for building Spring Native applications. In this tutorial, we’re going to explore the buildpack option that gives us the ability to build the application as a container image, using Packto as our buildpack builder. This provides a special type of buildpack called “tiny” that is very suitable for Java native applications.
no-name$ mvn spring-boot:build-image
Note: Be patient during this step. The build-image takes considerably more time than building normal Spring applications because of the static analysis that Sprint AOT and GraalVM does at build time in order to provide a small application image and to minimize the memory footprint.
This indicates that the build has finished successfully, and a new image has been created with the name “simple-web-native-application” and tag “0.0.1-SNAPSHOT”
Note: Please visit the Spring Maven plugin to learn how to customize the image name and tag.
Tip: If you get an OutOfMemoryException during the build, increase the docker memory allocation to at least 8GB for the building process.
Once the build has finished, you can find the native configuration that Spring AOT creates under the target directory.
Step Four: Run Image
Run the following command to confirm that the image was created:
no-name$ docker images –filter reference=”<name-of-image-created:*
Note: The “CREATED” column says “41 years ago.” You’re probably asking, “why?” Unfortunately, by the time this tutorial was written, I’ve yet to find the answer to this.
Once you’ve created the image, you can be impressed by the fast startup time by running the image container of the application. For this, run the following command:
no-name$ docker run –rm -p 8080:8080 simple-web-native-application:0.0.1-SNAPSHOT
It only takes 0.408 seconds to have the application running and able to accept requests. This incredible startup time may be reflected in significant savings in bills, especially for serverless architecture.
Let’s run the same application, but this time without using the Spring and GraalVM native capabilities, and build the application as we usually do for Spring application, in order to compare the startup times between both.
You’ll notice the clear startup time difference between a Spring and Spring Native application.
Spring Native provides an incredible platform to work with GraalVM native image by using a well-defined build model through its core plugin called Spring AOT, as well as the native API, which helps us define custom native configuration programmatically by using native hints annotation.
This integration between Spring Native and GraalVM offers the ability to run Spring applications in an efficient way by compiling only the reachable code that includes a limited JDK and the required Spring code. Moreover, it removes the JVM runtime, which makes the application even more lightweight, with a smaller memory footprint.
Doing a build time analysis and compilation creates a great opportunity to build applications where you need to scale often or in environments with limited resources, such as serverless or functions environments. These can be translated with no downtime during the scaling of an application, helping save on cost for serverless architecture, since the application will start serving requests faster, among many other benefits.
Finally, GraalVM and Spring Native are still new implementations, so it’s important to note that some JDK and Spring features may be challenging to configure, which could lead to repetitive builds and manual configuration until you have the application optimized to be built as a Native application.