How to Build Fast Spring Boot 3 Apps with GraalVM Native Images and AOT
This guide walks through the prerequisites, GraalVM installation, Maven setup, and step‑by‑step packaging of a Spring Boot 3 application into a native executable using AOT compilation, runtime hints, and Docker, demonstrating dramatically faster startup times.
Prerequisite Knowledge
Familiarize yourself with Spring Framework 6 new features ( link ) and Spring Boot 3 documentation ( link ).
Install GraalVM
Download the appropriate GraalVM CE build for your JDK version (Spring Boot 3 requires JDK 17+):
https://github.com/graalvm/graalvm-ce-builds/releases. Installation is the same as a regular JDK. Verify with
java -versionto see the GraalVM VM.
GraalVM Limitations
When compiling to a native binary, GraalVM must know which classes, methods, and fields are used. Dynamically generated code requires configuration, e.g., via
reflect-config.json, to declare reflective accesses.
Install Maven
Standard Maven installation, preferably using an Alibaba Cloud mirror for faster dependency resolution.
Background
Native compilation removes the JVM startup overhead by compiling Java bytecode directly to machine code, greatly improving startup speed. Spring Boot 3 AOT moves bean scanning to compile time, further reducing launch time.
Packaging Spring Boot 3
Project Preparation
<code><parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</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></code> <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";
}
}</code>Build Native Image
<code># Ensure GraalVM environment and a C compiler (Linux recommended)
mvn -Pnative native:compile</code>The build produces both an executable binary and a regular JAR. The native binary starts in just a few dozen milliseconds, whereas the JAR takes noticeably longer.
Docker Image
<code># Build Docker image from native binary
mvn -Pnative spring-boot:build-image
# Run container
docker run --rm -p 8080:8080 demo
# Pass environment variable to the container
docker run --rm -p 8080:8080 -e methodName=test demo
</code>Running the native container also starts within tens of milliseconds.
Understanding AOT and RuntimeHints
RuntimeHints Example
<code>@Component
@ImportRuntimeHints(UserService.MyServiceRuntimeHints.class)
public class UserService {
public String test() {
String result = "";
try {
Method m = MyService.class.getMethod("test");
result = (String) m.invoke(MyService.class.newInstance());
} catch (Exception e) {
throw new RuntimeException(e);
}
return result;
}
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);
}
}
}
}
</code>Other Annotations
<code>@RegisterReflectionForBinding(MyService.class)
public String test() { /* ... */ }
</code> <code>@Component
@ImportRuntimeHints(MyService.MyServiceRuntimeHints.class)
public class UserService {
// method that reads method name from system property
public String test() {
String methodName = System.getProperty("methodName");
// reflection logic ...
}
}
</code>JDK Proxy Configuration
<code>public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.proxies().registerJdkProxy(UserInterface.class);
}
</code>@Reflective Example
<code>@Component
public class MyService {
@Reflective
public MyService() {}
@Reflective
public String test() { return "hello"; }
}
</code>AOT Plugin Execution Logic
Running
mvn -Pnative native:compileinvokes the
native-maven-plugin, which calls
ProcessAotMojo.executeAot(). This method triggers
SpringApplicationAotProcessor, which processes the application context, generates Java source files under
spring-aot/main/sources, compiles them, and copies the resulting classes and GraalVM configuration files into
target/classes.
Generated AOT Classes
The AOT process pre‑creates
BeanDefinitionclasses, allowing Spring to skip runtime bean scanning. Diagrams illustrate the flow from
SpringApplication.runto the generated classes.
Principle Diagram
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.