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, I used the GraalVM Native Build Tool directly. Other approaches, such as converting the JAR directly or using Paketo Buildpacks, are not covered.
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:
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 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.
I only collected up to context launch, but you keep running it exercising different paths according to which the application runs in prod.
the agent generates files in the specified directory which you can copy under src/main/resources/META-INF/ for the plugin to pick them up during the next build:
Debugging the native image
With this initial setup, Spring's application context fails to launch due to :
missing reflection configuration
missing resources
missing jni config
I had to cherry-pick reflection and resource configurations from the agent run and include it in native image builds for some custom pojos as well as JaxB :
you can use JSONPath to only get the metadata you need
The AOT process is not flawless, here is how to provide your own hints
XML Linked Issues
```javastacktrace
2025-01-01T10:36:15.879+01:00 WARN 39400 --- [ main] w.s.c.ServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'annotationActionEndpointMapping': Instantiation of supplied bean failed
Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'annotationActionEndpointMapping': Instantiation of supplied bean failed
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1245)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1182)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:563)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:523)
...
at com.app.myApplication.main(myApplication.java:28)
at java.base@22.0.2/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH)
Caused by: org.springframework.xml.xpath.XPathParseException: Could not compile [normalize-space(wsa:To)] to a XPathExpression: javax.xml.transform.TransformerException: com.sun.org.apache.xpath.internal.functions.FuncNormalizeSpace.<init>()
at org.springframework.xml.xpath.Jaxp13XPathExpressionFactory.createXPathExpression(Jaxp13XPathExpressionFactory.java:82)
at org.springframework.xml.xpath.XPathExpressionFactory.createXPathExpression(XPathExpressionFactory.java:72)
at org.springframework.ws.soap.addressing.version.AbstractAddressingVersion.createNormalizedExpression(AbstractAddressingVersion.java:115)
at org.springframework.ws.soap.addressing.version.AbstractAddressingVersion.<init>(AbstractAddressingVersion.java:89)
at org.springframework.ws.soap.addressing.version.Addressing200408.<init>(Addressing200408.java:38)
...
... 18 more
Caused by: javax.xml.xpath.XPathExpressionException: javax.xml.transform.TransformerException: com.sun.org.apache.xpath.internal.functions.FuncNormalizeSpace.<init>()
at java.xml@22.0.2/com.sun.org.apache.xpath.internal.jaxp.XPathImpl.compile(XPathImpl.java:174)
at org.springframework.xml.xpath.Jaxp13XPathExpressionFactory.createXPathExpression(Jaxp13XPathExpressionFactory.java:78)
... 44 more
Caused by: javax.xml.transform.TransformerException: com.sun.org.apache.xpath.internal.functions.FuncNormalizeSpace.<init>()
at java.xml@22.0.2/com.sun.org.apache.xpath.internal.compiler.FunctionTable.getFunction(FunctionTable.java:353)
at java.xml@22.0.2/com.sun.org.apache.xpath.internal.compiler.Compiler.compileFunction(Compiler.java:1060)
at java.xml@22.0.2/com.sun.org.apache.xpath.internal.compiler.Compiler.compile(Compiler.java:204)
at java.xml@22.0.2/com.sun.org.apache.xpath.internal.compiler.Compiler.compile(Compiler.java:152)
at java.xml@22.0.2/com.sun.org.apache.xpath.internal.compiler.Compiler.compileExpression(Compiler.java:126)
at java.xml@22.0.2/com.sun.org.apache.xpath.internal.XPath.<init>(XPath.java:215)
at java.xml@22.0.2/com.sun.org.apache.xpath.internal.jaxp.XPathImpl.compile(XPathImpl.java:166)
... 45 more
I've added a custom hint under src/main/resources/META-INF/native-image/custom-hints/reflect-config.json
Caused by: java.lang.ExceptionInInitializerError
at org.glassfish.jaxb.runtime.v2.ContextFactory.createContext(ContextFactory.java:241)
at org.glassfish.jaxb.runtime.v2.JAXBContextFactory.createContext(JAXBContextFactory.java:58)
at jakarta.xml.bind.ContextFinder.find(ContextFinder.java:322)
at jakarta.xml.bind.JAXBContext.newInstance(JAXBContext.java:392)
... 69 more
Caused by: java.util.MissingResourceException: Can't find bundle for base name org.glassfish.jaxb.core.v2.Messages, locale en_GB
at java.base@22.0.2/java.util.ResourceBundle.throwMissingResourceException(ResourceBundle.java:2052)
... 95 more
You can provide the missing bundle in your custom resource-config.json:
Caused by: java.lang.IllegalStateException: Could not load 'class path resource [org/springframework/ws/transport/http/MessageDispatcherServlet.properties]': class path resource [org/springframework/ws/transport/http/MessageDispatcherServlet.properties] cannot be opened because it does not exist
at org.springframework.ws.support.DefaultStrategiesHelper.<init>(DefaultStrategiesHelper.java:78)
at org.springframework.ws.support.DefaultStrategiesHelper.<init>(DefaultStrategiesHelper.java:88)
at org.springframework.ws.transport.http.MessageDispatcherServlet.<init>(MessageDispatcherServlet.java:175)
at org.springframework.ws.transport.http.MessageDispatcherServlet.<init>(MessageDispatcherServlet.java:129)
at org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration.messageDispatcherServlet(WebServicesAutoConfiguration.java:72)
at org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration__BeanDefinitions.lambda$getMessageDispatcherServletInstanceSupplier$0(WebServicesAutoConfiguration__BeanDefinitions.java:32)
This one was straightforward, let's include the missing resource :
I've added a custom hint under src/main/resources/META-INF/native-image/custom-hints/resource-config.json
2025-01-03T14:42:02.819+01:00 INFO 33628 --- [ main] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'lettuceClientResources': Instantiation of supplied bean failed
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1245)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1182)
...
at com.app.MainApplication.main(MainApplication.java:28)
at java.base@23.0.1/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH)
Caused by: java.lang.ExceptionInInitializerError
at io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder$1.provider(DnsServerAddressStreamProviders.java:150)
at io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder$1.<init>(DnsServerAddressStreamProviders.java:130)
at io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder.<clinit>(DnsServerAddressStreamProviders.java:128)
at io.netty.resolver.dns.DnsServerAddressStreamProviders.unixDefault(DnsServerAddressStreamProviders.java:117)
at io.netty.resolver.dns.DnsServerAddressStreamProviders.platformDefault(DnsServerAddressStreamProviders.java:113)
at io.netty.resolver.dns.DnsNameResolverBuilder.<init>(DnsNameResolverBuilder.java:71)
at io.lettuce.core.resource.AddressResolverGroupProvider$DefaultDnsAddressResolverGroupWrapper.<clinit>(AddressResolverGroupProvider.java:56)
...
... 20 more
Add to native image plugin to force initializing until runtime (see Native profile):
Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'lettuceClientResources': Instantiation of supplied bean failed
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1245)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1182)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:563)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:523)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:336)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:289)
...
at com.app.MainApplication.main(MainApplication.java:28)
at java.base@23.0.1/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH)
Caused by: java.lang.NoSuchFieldError: sun.net.dns.ResolverConfigurationImpl.os_searchlist
at org.graalvm.nativeimage.builder/com.oracle.svm.core.jni.functions.JNIFunctions$Support.getFieldID(JNIFunctions.java:1867)
at org.graalvm.nativeimage.builder/com.oracle.svm.core.jni.functions.JNIFunctions.GetStaticFieldID(JNIFunctions.java:467)
at java.base@23.0.1/sun.net.dns.ResolverConfigurationImpl.init0(Native Method)
...