# 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