Native Image (Spring Boot 3+) - Paketo Buildpacks Route
This guide details the process of migrating a Spring Boot application to GraalVM Native Image.
The application integrates Spring Cloud and Spring Cloud AWS. Be prepared for potential runtime errors and the need for manual adjustments during the image-building process.
For this guide, we will be using the native Paketo buildpacks to build the native image.
Upgrading to a Native-Compatible Setup
First, upgrade to the latest versions of Spring Boot 3.x.x, Spring Cloud, and Spring Cloud AWS on which the application depends.
Make sure to check the compatibility matrix for these dependencies:
Dependency | Used version | Compatibility Matrix | Comments |
---|---|---|---|
Java | 23.0.2+9 | N/A | |
Liberica-NIK (Native Image Builder) | Liberica-NIK-24.1.2-1 | N/A | Native Image Builder |
GraalVM (Trace Agent Metadata Collection) | GraalVM for JDK 23.0.1 | N/A | Used as a JVM for instrumenting with Trace Agent |
Spring Boot | 3.4.2 | N/A | |
Spring Cloud | 2024.0.0 | ||
Spring Cloud AWS | 3.3.0 |
In this case, upgrading was straightforward since the Spring Boot 2 to 3 upgrade was already completed.
Simply update the versions and run integration and regression tests to ensure the service remains intact.
Enable Native Support
Now that you're running on compatible versions, set up your project for Ahead-of-Time (AOT) processing and native image building.
If you are using the Spring Boot parent starter, a native profile is already included. The following setup demonstrates manually adding the profile in case you have a custom parent POM.
Spring AOT
Introduction
The goal of Spring AOT Compilation is to provide GraalVM with the necessary metadata for Spring's dynamic features.
These features, previously processed at runtime, need to be initialized during compile time for native images.
AOT processing provides:
Source code for bean definitions at build time.
Bytecode for dynamic proxies.
Hint files for:
Reflective access metadata.
Serialization metadata.
Resources to include.
Proxy configuration.
Handling Profiles and Conditional Bean Creation
Feeding all dynamic aspects into GraalVM during native image build would undermine the goal of maintaining a clean runtime:
It increases size, negating benefits like reduced footprint and optimization.
It compromises a reduced attack surface, as the runtime wouldn't be restricted to a profile or tailored for specific properties.
As a result, building native images introduces restrictions on profiles and conditional bean loading (e.g., @Conditional
). The application context during AOT compilation mirrors the build-time configuration.
In Practice
Since we covered in detail the Spring AOT in Road to Native Image for Spring Boot 3
Let's jump directly to the profile that we are using for this build.
As with the previous build, we will be manually providing the profile in the POM because we use a custom parent.
Default profile from the Spring parent project:
You can run only Spring AOT with:
Inspect:
GraalVM hints under
target/spring-aot/main/resources/META-INF/native-image/com.myapp.business/myapp/*
:

Generated source code under
target/spring-aot/main/sources
:

These hints will be part of our final native image build.
Paketo Buildpacks (Native Run)
Dry Run
Cloud Native Buildpacks are a standard way to package applications for deployment. They provide a consistent and secure way to build OCI images.
Spring Boot 3 applications can be built into native images using Paketo Buildpack for Spring Boot
With the native profile in place, we can now move on to building the native image through an initial dry run.
We can see the buildpack for native image being used:
First hurdle, the build fails with the following error:
Now that we have the initial dry run, let's move on to debugging, providing the right hints, and configuration to Spring Boot Paketo Buildpack for Native Image.
Customizing the Spring Boot Maven Plugin
Let's start by customizing the Maven Spring Boot plugin to suit our needs for the native image build: and try to get it to work with Logback, etc.
In the native profile, let's pass environment variables to the buildpack:
Debugging the Build
After correcting the reachability metadata issue, the build is now successful.
Now the context is failing to launch due to missing reflection configuration.
Let's start debugging the built image and provide the necessary hints to get the application context to start.
Trace Agent with Paketo Buildpacks JVM Build
The trace agent is a tool that can be used to collect runtime information from the application.
Let's configure a regular JVM profile for a source-to-image build and instrument it with the trace agent:
JVM Profile
JVM AOT Profile
Even better, let's make an AOT-instrumented Spring Boot Docker image to get metadata that is tailored to the application's runtime active Spring profiles:
Let's go through some of the XML tags used in the profile:
These tags will allow us to use GraalVM as a JVM rather than a native image builder as described in
Build Spring Boot App into Native Executable
This setup will allow us to collect metadata from the instrumented Spring Boot AOT-enabled app.
At the time of writing, the default Paketo Jammy builders only supported ARM64 for the tiny variant. I am using the base variant to stick to amd64 architecture.
I had issues with the tiny variant and the Mac's ARM64 architecture that seem linked to:
Enable the Trace Agent
To append to the JAVA_TOOL_OPTIONS environment variable at runtime, use the tags above.
Docker Exec
By default, the buildpack uses the tiny variant, which strips out bash
& sh
from the image.
We'll switch the builder to the base variant to be able to run the image and debug it.
References
Check customization documentation for regular JVM builds: Spring Boot Build Image Documentation
Build with the JVM AOT profile:
Run the Docker image to expose the port for running a couple of queries against your build and collect metadata from the trace agent.
In this example, we mounted the collected data from the container on the host machine under $(pwd)/tmp
:
You can now inspect the trace agent output in the /tmp
directory:

Trace Agent Output
You can add the generated reachability-metadata.json
to the native image build just by adding it under the src/main/resources/META-INF/native-image
directory.
You can use a namespace of your choice, as in this example from-jvm-aot
:

Similar to the previous article where we used the Native Build Tools plugin, I still needed to supplement custom hints to get to a successful context launch and weed out runtime errors.
I had to cherry-pick some reflection and resource configurations from the agent run and include them in the native image build for some custom POJOs as well as JAXB.
It looks like XSDs that reference others are not being picked up by the agent, which you can resolve by providing, for example:
and some POJOs which the agent effectively picked up, but I still had to loosen up the reflection configuration for them:
Here is an example for this case:
Note
There has been a change in the way the agent collects metadata.
You can see the difference between the previous article Road to Native Image for Spring Boot 3 and this one.
You can combine all dynamic elements now under a single reachability-metadata.json
file that has a section for reflection, resources, proxies, JNI, and serialization.
Another point to note is that the process is less tedious and support has improved a lot between this article and the previous one, so much so these were the only changes I had to make to get to a full context launch.
References
Check the previous article for more details on supplementing custom hints: Road to Native Image for Spring Boot 3 Debugging Section
Spring Context Launch Succeeds
After adding the necessary hints, the context launches successfully:
We'll cover more practical debugging and runtime issues next.
Debugging the Native Image
Getting to a context launch is half the battle.
Though some POJOs used in JSON serialization/deserialization were picked up by the trace agent, I still had to loosen reflection configurations in bulk for serialization and deserialization to work.
At this time, unfortunately, there is no support for wildcards for packages in the reachability-metadata.json
file, so you have to list each class individually.
Let's make use of IntelliJ's JSONPath Plugin to bulk change reflection configuration:
For instance, you can pull all classes ending in Impl
under the package com.myapp.x.y
:
and add looser reflection configurations for them:
References
Wildcard support for packages is being discussed in the GraalVM community: GraalVM Wildcard Package Support Discussion