# GraalVM Native Image with Spring Boot 3 in 2026: AOT Compilation Step by Step
> Complete guide to compile Spring Boot 3 applications to native images with GraalVM. AOT configuration, optimizations, and production deployment.
- Published: 2026-03-21
- Updated: 2026-03-31
- Author: SharpSkill
- Tags: graalvm, spring boot 3, native image, aot compilation, java performance
- Reading time: 14 min
---
Native compilation with GraalVM transforms Spring Boot 3 applications into native executables. Startup time drops from seconds to milliseconds, and memory consumption decreases dramatically. This guide covers every step from AOT configuration to production deployment.
> **Prerequisites**
>
> GraalVM 22.3+ with Native Image installed, Spring Boot 3.2+, and Maven or Gradle. Native compilation requires more RAM (8 GB minimum recommended) and takes several minutes.
## Understanding AOT and Native Image Compilation
### Difference Between JIT and AOT
The traditional JVM uses Just-In-Time (JIT) compilation: bytecode is interpreted and then compiled to machine code during execution. GraalVM Native Image takes the Ahead-Of-Time (AOT) approach: all code is compiled before execution.
```text
┌─────────────────────────────────────────────────────────────┐
│ JIT Compilation │
├─────────────────────────────────────────────────────────────┤
│ │
│ .java → .class → JVM → Interpretation → JIT → Machine │
│ (runtime) (runtime) │
│ │
│ Advantages: Adaptive optimizations, fast class loading │
│ Disadvantages: Slow startup, high memory consumption │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ AOT Compilation │
├─────────────────────────────────────────────────────────────┤
│ │
│ .java → .class → GraalVM Native Image → Native executable │
│ (build time) │
│ │
│ Advantages: Instant startup, low memory footprint │
│ Disadvantages: Long build, no dynamic reflection │
└─────────────────────────────────────────────────────────────┘
```
AOT compilation statically analyzes all code reachable from the entry point. Any code not detected at compile time is excluded from the native image, which explains the constraints on reflection and dynamic class loading.
### Spring AOT Architecture
Spring Boot 3 natively integrates AOT support. The compilation process generates additional source code that replaces dynamic mechanisms with static equivalents.
```java
// ApplicationConfig.java
// Standard Spring configuration
@Configuration
@EnableCaching
public class ApplicationConfig {
@Bean
public CacheManager cacheManager() {
// Bean created dynamically at runtime in JIT mode
// Pre-generated statically in AOT mode
return new ConcurrentMapCacheManager("users", "products");
}
@Bean
@ConditionalOnProperty(name = "app.feature.enabled", havingValue = "true")
public FeatureService featureService() {
// Conditions are evaluated at build time in AOT
return new FeatureServiceImpl();
}
}
```
The Spring AOT process automatically generates files in `target/spring-aot/main`:
```text
target/spring-aot/main/
├── sources/ # Generated Java code
│ └── com/example/
│ └── ApplicationConfig__BeanDefinitions.java
├── resources/
│ └── META-INF/
│ └── native-image/
│ ├── reflect-config.json # Reflection configuration
│ ├── resource-config.json # Included resources
│ └── proxy-config.json # JDK proxies
```
## Spring Boot Project Configuration
### Maven Dependencies
Maven configuration uses the Spring Boot plugin with the native profile. Dependencies must be GraalVM compatible.
```xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.4.2
com.example
native-demo
1.0.0
21
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-jpa
org.postgresql
postgresql
runtime
org.springframework.boot
spring-boot-starter-validation
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
org.graalvm.buildtools
native-maven-plugin
native
org.graalvm.buildtools
native-maven-plugin
-O2
--verbose
--enable-http
--enable-https
-Xmx8g
```
### Equivalent Gradle Configuration
For Gradle projects, native configuration is similar using the GraalVM native plugin.
```kotlin
// build.gradle.kts
// Gradle configuration for Spring Boot Native
plugins {
java
id("org.springframework.boot") version "3.4.2"
id("io.spring.dependency-management") version "1.1.7"
// GraalVM Native plugin
id("org.graalvm.buildtools.native") version "0.10.4"
}
group = "com.example"
version = "1.0.0"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
// Native build configuration
graalvmNative {
binaries {
named("main") {
// Generated executable name
imageName = "native-demo"
// Compilation options
buildArgs.addAll(
"-O2", // Optimization level
"--enable-http", // HTTP support
"--enable-https", // HTTPS support
"--verbose" // Detailed logs
)
// Memory configuration for build
jvmArgs.addAll("-Xmx8g")
}
named("test") {
// Native tests with report
buildArgs.add("--verbose")
}
}
// Tracing agent for automatic discovery
agent {
defaultMode = "standard"
enabled = true
}
}
tasks.withType {
useJUnitPlatform()
}
```
> **GraalVM Tracing Agent**
>
> The tracing agent (`-agentlib:native-image-agent`) automatically discovers reflection calls during execution. Run the application with the agent, exercise all features, then use the generated configuration files.
## Managing Reflection and Resources
### Manual Reflection Configuration
Some libraries use reflection in ways not detectable by static analysis. Manual configuration becomes necessary.
```json
// src/main/resources/META-INF/native-image/reflect-config.json
// Configuration for classes requiring reflection
[
{
"name": "com.example.entity.User",
"allDeclaredConstructors": true,
"allDeclaredMethods": true,
"allDeclaredFields": true
},
{
"name": "com.example.dto.UserDTO",
"allDeclaredConstructors": true,
"allDeclaredMethods": true,
"allDeclaredFields": true
},
{
"name": "com.example.config.DynamicProperties",
"methods": [
{ "name": "getValue", "parameterTypes": [] },
{ "name": "setValue", "parameterTypes": ["java.lang.String"] }
]
}
]
```
### Using Spring RuntimeHints
Spring Boot 3 provides a programmatic API for declaring native hints, more maintainable than JSON files.
```java
// NativeHintsRegistrar.java
// Programmatic registration of native hints
@Configuration
@ImportRuntimeHints(NativeHintsRegistrar.AppRuntimeHints.class)
public class NativeHintsRegistrar {
static class AppRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// Register classes for reflection
hints.reflection()
// JPA entities with all members
.registerType(User.class, MemberCategory.values())
.registerType(Order.class, MemberCategory.values())
// DTOs with constructors and getters/setters
.registerType(UserDTO.class,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS,
MemberCategory.DECLARED_FIELDS
);
// Register resources to include
hints.resources()
// Configuration files
.registerPattern("application*.yml")
.registerPattern("application*.properties")
// Templates and static files
.registerPattern("templates/*")
.registerPattern("static/**/*")
// Validation messages
.registerPattern("ValidationMessages*.properties");
// Register JDK proxies
hints.proxies()
.registerJdkProxy(
UserRepository.class,
Repository.class
);
// Serialization for caching
hints.serialization()
.registerType(User.class)
.registerType(ArrayList.class);
}
}
}
```
```java
// EntityRuntimeHints.java
// Automatic hints for JPA entities
@Component
public class EntityRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// Automatic scan of entities in package
ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(Entity.class));
for (BeanDefinition bd : scanner.findCandidateComponents("com.example.entity")) {
try {
Class> entityClass = Class.forName(bd.getBeanClassName());
// Register each entity for full reflection
hints.reflection().registerType(
entityClass,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS,
MemberCategory.DECLARED_FIELDS
);
} catch (ClassNotFoundException e) {
// Log error without interrupting build
System.err.println("Entity class not found: " + bd.getBeanClassName());
}
}
}
}
```
## Native Image Compilation and Optimization
### Build Commands
Native compilation is performed with Maven or Gradle. The process takes several minutes and consumes significant resources.
```bash
# Maven build with native profile
# Generates executable in target/
mvn -Pnative native:compile
# Gradle build
# Generates executable in build/native/nativeCompile/
./gradlew nativeCompile
# Build with native tests included
mvn -Pnative native:compile -DskipTests=false
# Build with tracing agent enabled
mvn -Pnative -Dagent=true test
mvn -Pnative native:compile
```
### Advanced Optimization Options
Compilation options affect image size, startup time, and runtime performance.
```xml
org.graalvm.buildtools
native-maven-plugin
-O3
--pgo-instrument
-H:+CompressStrings
--gc=serial
--initialize-at-build-time=org.slf4j
-H:-IncludeAllTimeZones
-H:+ReportExceptionStackTraces
--verbose
--enable-monitoring=heapdump,jfr
false
false
```
```java
// BuildTimeInitializer.java
// Build time initialization to reduce startup
@Configuration
public class BuildTimeInitializer {
// These configurations are evaluated at build time
// not at runtime
static {
// Initialize loggers at build time
LoggerFactory.getLogger(BuildTimeInitializer.class);
}
@Bean
@NativeHint(options = "--initialize-at-build-time=com.example.Constants")
public ConstantsProvider constantsProvider() {
// Constants are computed once at build
return new ConstantsProvider();
}
}
```
### Performance Comparison
Performance gains with native compilation are significant.
```text
┌─────────────────────────────────────────────────────────────────────┐
│ JIT vs Native Comparison │
├─────────────────────┬─────────────────┬─────────────────────────────┤
│ Metric │ JIT (JVM) │ Native (GraalVM) │
├─────────────────────┼─────────────────┼─────────────────────────────┤
│ Startup time │ 2.5 - 5 sec │ 50 - 200 ms │
│ RSS Memory │ 200 - 400 MB │ 50 - 100 MB │
│ Executable size │ JAR ~30 MB │ Binary ~80 MB │
│ First request time │ 100 - 500 ms │ < 10 ms │
│ Peak throughput │ Excellent │ Good (85-95% of JIT) │
│ Build time │ 30 sec │ 3 - 10 min │
└─────────────────────┴─────────────────┴─────────────────────────────┘
```
> **Peak Performance**
>
> Maximum throughput in native mode may be slightly lower than JIT mode because JIT adaptive optimizations are unavailable. For high sustained performance workloads, evaluate both modes.
## Troubleshooting Common Issues
### Reflection Errors
The most frequent error involves undeclared reflection. The exception indicates the missing class.
```java
// ReflectionErrorHandler.java
// Diagnosing and resolving reflection errors
@Component
@Slf4j
public class ReflectionErrorHandler {
// Typical error:
// java.lang.ClassNotFoundException: com.example.SomeClass
// when accessing via reflection
// Solution 1: Add manual configuration
// src/main/resources/META-INF/native-image/reflect-config.json
// Solution 2: Use @RegisterReflection annotation
@RegisterReflection(classes = {
SomeClass.class,
AnotherClass.class
})
public void configureReflection() {
// Annotated classes will be available for reflection
}
// Solution 3: Programmatic RuntimeHints
public void registerHints(RuntimeHints hints) {
hints.reflection().registerType(
SomeClass.class,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS
);
}
}
```
### Missing Resources
Resource files must be explicitly declared for inclusion in the native image.
```json
// src/main/resources/META-INF/native-image/resource-config.json
// Configuration for resources to include
{
"resources": {
"includes": [
{"pattern": "application\\.yml"},
{"pattern": "application-.*\\.yml"},
{"pattern": "messages.*\\.properties"},
{"pattern": "templates/.*\\.html"},
{"pattern": "static/.*"},
{"pattern": "db/migration/.*\\.sql"}
],
"excludes": [
{"pattern": ".*\\.java"},
{"pattern": ".*\\.class"}
]
},
"bundles": [
{"name": "messages"},
{"name": "ValidationMessages"}
]
}
```
```java
// ResourceHintsConfig.java
// Programmatic resource configuration
@Configuration
@ImportRuntimeHints(ResourceHintsConfig.ResourceHints.class)
public class ResourceHintsConfig {
static class ResourceHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// YAML/Properties files
hints.resources()
.registerPattern("application*.yml")
.registerPattern("application*.properties");
// Thymeleaf templates
hints.resources().registerPattern("templates/**");
// Flyway SQL scripts
hints.resources().registerPattern("db/migration/*.sql");
// Static files
hints.resources().registerPattern("static/**");
// Message bundles
hints.resources().registerResourceBundle("messages");
hints.resources().registerResourceBundle("ValidationMessages");
}
}
}
```
### Proxy Issues
JDK and CGLIB proxies require specific configuration to work in native mode.
```java
// ProxyConfiguration.java
// Managing proxies for native compilation
@Configuration
public class ProxyConfiguration implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// JDK proxies for Spring Data interfaces
hints.proxies().registerJdkProxy(
UserRepository.class,
Repository.class,
CrudRepository.class
);
// Proxies for service interfaces
hints.proxies().registerJdkProxy(
PaymentService.class,
TransactionalService.class
);
}
// Alternative: force CGLIB proxies
@Bean
public BeanFactoryPostProcessor forceProxyTargetClass() {
return beanFactory -> {
// Use CGLIB instead of JDK proxies
// More compatible with native compilation
};
}
}
```
## Docker and Kubernetes Deployment
### Optimized Multi-Stage Dockerfile
Multi-stage build separates compilation from execution for a minimal image.
```dockerfile
# Dockerfile
# Multi-stage build for Spring Boot Native
# Stage 1: Build with GraalVM
FROM ghcr.io/graalvm/graalvm-community:21 AS builder
# Install Native Image
RUN gu install native-image
WORKDIR /app
# Copy build files
COPY pom.xml .
COPY src ./src
# Install Maven
RUN microdnf install -y maven
# Native build with dependency caching
RUN --mount=type=cache,target=/root/.m2 \
mvn -Pnative native:compile -DskipTests
# Stage 2: Minimal runtime image
FROM gcr.io/distroless/base-debian12
WORKDIR /app
# Copy native executable
COPY --from=builder /app/target/native-demo /app/native-demo
# Exposed port
EXPOSE 8080
# Healthcheck
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s \
CMD ["/app/native-demo", "--health"]
# Execution
ENTRYPOINT ["/app/native-demo"]
```
```dockerfile
# Dockerfile.alpine
# Alternative with Alpine for even smaller image
FROM ghcr.io/graalvm/native-image-community:21-muslib AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
mvn -Pnative native:compile \
-Dspring-boot.aot.jvmArguments="-Dspring.aot.processing.resource.matching.strategy=GLOB" \
-DskipTests
# Minimal Alpine image (< 20 MB)
FROM alpine:3.19
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY --from=builder /app/target/native-demo /app/native-demo
EXPOSE 8080
ENTRYPOINT ["/app/native-demo"]
```
### Kubernetes Deployment with Optimized Resources
Native applications require fewer resources than traditional JVM applications.
```yaml
# kubernetes/deployment.yaml
# Optimized Kubernetes deployment for native
apiVersion: apps/v1
kind: Deployment
metadata:
name: native-demo
spec:
replicas: 3
selector:
matchLabels:
app: native-demo
template:
metadata:
labels:
app: native-demo
spec:
containers:
- name: native-demo
image: registry.example.com/native-demo:1.0.0
ports:
- containerPort: 8080
# Reduced resources thanks to native
resources:
requests:
memory: "64Mi" # vs 256Mi for JVM
cpu: "50m" # vs 200m for JVM
limits:
memory: "128Mi" # vs 512Mi for JVM
cpu: "200m" # vs 500m for JVM
# Fast probes (instant startup)
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 1 # vs 30s for JVM
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 2 # vs 60s for JVM
periodSeconds: 10
failureThreshold: 3
# Environment variables
env:
- name: SPRING_PROFILES_ACTIVE
value: "production"
- name: JAVA_TOOL_OPTIONS
value: "" # No JVM options needed
---
apiVersion: v1
kind: Service
metadata:
name: native-demo
spec:
selector:
app: native-demo
ports:
- port: 80
targetPort: 8080
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: native-demo-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: native-demo
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
```
> **Fast Scaling**
>
> Instant startup time enables very fast horizontal scaling. New pods are ready in seconds, ideal for workloads with traffic spikes.
## Testing and Validating the Native Image
### Native Test Configuration
Tests can also be compiled and run in native mode to validate behavior.
```java
// NativeIntegrationTest.java
// Integration tests for native validation
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:h2:mem:testdb",
"spring.jpa.hibernate.ddl-auto=create-drop"
})
class NativeIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@Test
void shouldCreateAndRetrieveUser() {
// Arrange: create a user
UserDTO request = new UserDTO("John", "john@example.com");
// Act: API call
ResponseEntity createResponse = restTemplate.postForEntity(
"/api/users",
request,
UserDTO.class
);
// Assert: verify creation
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(createResponse.getBody()).isNotNull();
assertThat(createResponse.getBody().getName()).isEqualTo("John");
// Verify retrieval
Long userId = createResponse.getBody().getId();
ResponseEntity getResponse = restTemplate.getForEntity(
"/api/users/{id}",
UserDTO.class,
userId
);
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().getEmail()).isEqualTo("john@example.com");
}
@Test
void shouldHandleReflectionCorrectly() {
// Specific test to validate reflection configuration
User user = new User();
user.setName("Test");
user.setEmail("test@example.com");
// ORM uses reflection to map entities
User saved = userRepository.save(user);
assertThat(saved.getId()).isNotNull();
assertThat(userRepository.findById(saved.getId())).isPresent();
}
}
```
```xml
org.graalvm.buildtools
native-maven-plugin
--verbose
test-native
test
test
```
## Conclusion
Native compilation with GraalVM transforms Spring Boot 3 applications into performant executables. Key takeaways:
**Project configuration:**
- ✅ Spring Boot 3.2+ with GraalVM native plugin
- ✅ RuntimeHints for reflection and resources
- ✅ Tracing agent for automatic discovery
**Build optimizations:**
- ✅ Appropriate compilation options (O2/O3, GC, compression)
- ✅ Build time initialization for static components
- ✅ Quickbuild for development, full build for production
**Troubleshooting:**
- ✅ Explicit reflection configuration for third-party libraries
- ✅ Declaration of resources to include
- ✅ JDK and CGLIB proxy management
**Deployment:**
- ✅ Multi-stage Docker images with distroless
- ✅ Reduced Kubernetes resources (64 Mi vs 256 Mi)
- ✅ Probes with minimal delays (instant startup)
Native compilation is ideal for microservices, serverless functions, and resource-constrained environments. Instant startup time and low memory consumption far outweigh the longer build time.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/en/blog/spring-boot/graalvm-native-image-spring-boot-3-aot-compilation