Spring Boot 3.0 AOT Compilation with GraalVM: A Step‑by‑Step Guide
This article explains how to prepare the environment, configure GraalVM, use Maven plugins, and apply Spring Boot 3.0 AOT and RuntimeHints to compile a Spring Boot application into a fast‑starting native executable, including Docker packaging and underlying AOT principles.
Prerequisites
Before starting, review the new features of Spring 6 ( https://github.com/spring-projects/spring-framework/wiki/What%27s-New-in-Spring-Framework-6.x ) and Spring Boot 3.0 documentation ( https://docs.spring.io/spring-boot/docs/current/reference/html/ ). Install GraalVM matching your JDK version (JDK 17+ is required for Spring Boot 3.0) from the official releases ( https://github.com/graalvm/graalvm-ce-builds/releases ) and verify the installation with java -version . Maven should also be installed (preferably with Alibaba Cloud acceleration).
The motivation is to address the Serverless environment by reducing Spring Boot startup time: compiling Java bytecode directly to native machine code eliminates the JVM warm‑up overhead, while RuntimeHints preserve reflection, dynamic proxies, and other features.
Packaging Spring Boot 3.0
1. Project preparation
Add the following dependencies and plugins to your pom.xml :
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>Sample application code:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class Controller {
@GetMapping("/demo1")
public String demo1() {
return "hello world";
}
}2. Build the native image
Run the Maven command with the native profile (requires GraalVM and a C toolchain, preferably on Linux):
# Note: use GraalVM environment and C toolchain
mvn -Pnative native:compileThe process takes longer than a regular JAR build but produces both a native executable and a JAR. The native binary starts in a few tens of milliseconds, while the JAR starts noticeably slower.
3. Docker packaging
# Build Docker image with native binary
mvn -Pnative spring-boot:build-image
docker run --rm -p 8080:8080 demo
# Pass parameters via environment variable
docker run --rm -p 8080:8080 -e methodName=test demo
# Retrieve the parameter in code
String methodName = System.getenv("methodName");
// or using Spring Environment
environment.getProperty("methodName");Docker build downloads a Java base image, which may be slow, but the resulting container starts in milliseconds.
Understanding AOT and RuntimeHints
AOT (Ahead‑of‑Time) compilation moves bean scanning from runtime to compile time. Spring provides several mechanisms to inform GraalVM about reflective access, dynamic proxies, and other features that would otherwise be lost:
RuntimeHints – programmatic API to register reflection, proxy, and resource hints.
RuntimeHintsRegistrar – implement this interface and register it with @ImportRuntimeHints .
@RegisterReflectionForBinding – annotation to declare classes whose reflective members should be compiled.
@Reflective – annotate constructors or methods of beans that need reflection.
Example of a RuntimeHintsRegistrar implementation:
@Component
@ImportRuntimeHints(UserService.MyServiceRuntimeHints.class)
public class UserService {
public String test() {
// reflective call example
Method m = MyService.class.getMethod("test");
return (String) m.invoke(MyService.class.newInstance());
}
static class MyServiceRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
try {
hints.reflection().registerConstructor(MyService.class.getConstructor(), ExecutableMode.INVOKE);
hints.reflection().registerMethod(MyService.class.getMethod("test"), ExecutableMode.INVOKE);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
}When method names are supplied via environment variables, the same RuntimeHints approach must be used to register the dynamically chosen method.
For JDK dynamic proxies, register the proxy interface:
hints.proxies().registerJdkProxy(UserInterface.class);AOT compilation process
The Maven goal mvn -Pnative native:compile invokes the native-maven-plugin , which runs ProcessAotMojo.executeAot() . This triggers the SpringApplicationAotProcessor and ContextAotProcessor to generate Java source files under spring-aot/main/sources , compile them into target/classes , and copy GraalVM configuration files ( reflect-config.json , proxy-config.json ) into the same output directory. The generated classes contain pre‑computed BeanDefinition objects, allowing the native image to skip bean scanning at startup.
Finally, the native image can be executed directly or inside a Docker container, providing dramatically faster startup times compared with a traditional JVM‑based Spring Boot application.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.