Contents

GraalVM Native Image performs Ahead-of-Time (AOT) compilation at build time using a closed-world assumption: it traces all code paths reachable from the entry point and compiles only those paths into a self-contained native binary. Everything not reachable at build time — including dynamically loaded classes, reflection, JNI, serialization, and dynamic proxies — must be declared explicitly.

Spring Boot 3 requires GraalVM 22.3+ or the community Liberica NIK / Mandrel distributions. GraalVM JDK 21 LTS is recommended for production.

Add the following to your pom.xml (Maven) or build.gradle (Gradle). Spring Boot manages compatible versions automatically when you use the Spring Boot BOM.

<!-- pom.xml — spring-boot-maven-plugin with native profile --> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <image> <!-- Buildpacks native image (no local GraalVM needed) --> <builder>paketobuildpacks/builder-jammy-tiny:latest</builder> <env> <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE> </env> </image> </configuration> </plugin> <!-- Native Maven Plugin — for local native-image builds --> <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> </plugin> </plugins> </build> # Install GraalVM via SDKMAN (recommended) sdk install java 21.0.3-graalce sdk use java 21.0.3-graalce # Verify java -version # should show GraalVM native-image --version

When you build a native image, Spring runs its AOT engine before native-image compilation. It pre-computes bean definitions, generates source code for bean factories, and registers hints for reflection and proxies — eliminating most manual configuration.

# Trigger AOT source generation (runs automatically during native build) ./mvnw spring-boot:process-aot # Generated files appear in: # target/spring-aot/main/sources/ — generated Java source files # target/spring-aot/main/resources/ — generated hint JSON files // AOT generates classes like this for each bean: // target/spring-aot/main/sources/com/example/MyApplication__BeanDefinitions.java public class MyApplication__BeanDefinitions { @Bean public static BeanDefinition getOrderServiceBeanDefinition() { RootBeanDefinition beanDefinition = new RootBeanDefinition(OrderService.class); beanDefinition.setInstanceSupplier(OrderService::new); return beanDefinition; } } // This replaces runtime reflection — Spring no longer needs to introspect // bean classes at startup, which is what makes native startup so fast. The AOT engine handles the vast majority of Spring's own infrastructure automatically — @Component, @Bean, @Autowired, JPA entities, Spring Data repositories, and Spring MVC controllers all work without manual hints.

When AOT cannot infer hints automatically (e.g., classes loaded reflectively by a third-party library, dynamic proxies, or serialized types), you provide them via annotations or a RuntimeHintsRegistrar.

// 1. Annotation-based hints (simplest) @RegisterReflectionForBinding(OrderDto.class) // Jackson deserialization @RegisterReflectionForBinding({ProductDto.class, UserDto.class}) @SpringBootApplication public class MyApplication { } // 2. RuntimeHintsRegistrar — programmatic hints import org.springframework.aot.hint.*; public class MyHintsRegistrar implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader classLoader) { // Reflection — make a class and its methods accessible hints.reflection() .registerType(LegacyXmlParser.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); // Resources — include files in the native image hints.resources() .registerPattern("templates/*.html") .registerPattern("META-INF/services/*"); // Proxies — JDK dynamic proxy for an interface hints.proxies() .registerJdkProxy(MyService.class); // Serialization hints.serialization() .registerType(MySerializableClass.class); } } // Register the hints registrar @ImportRuntimeHints(MyHintsRegistrar.class) @SpringBootApplication public class MyApplication { } // 3. @Reflective — annotate a class to register it for reflection @Reflective // registers all public constructors, methods, and fields public class PluginConfig { }

For third-party libraries where you can't add annotations, create JSON hint files in src/main/resources/META-INF/native-image/<groupId>/<artifactId>/:

// reflect-config.json [ { "name": "com.thirdparty.SomeClass", "allDeclaredConstructors": true, "allPublicMethods": true, "allDeclaredFields": true }, { "name": "com.thirdparty.AnotherClass", "methods": [ { "name": "parse", "parameterTypes": ["java.lang.String"] } ] } ] // resource-config.json { "resources": { "includes": [ { "pattern": "\\Qdb/migration/V1__init.sql\\E" }, { "pattern": ".*\\.xml$" } ] } } // proxy-config.json [ ["com.example.MyRepository", "org.springframework.data.repository.Repository"] ]

The commands below build and run the application. Make sure Docker is running locally before executing the image build steps.

# Option 1: Local native-image binary (fastest iteration) ./mvnw -Pnative native:compile # The executable is at: target/<artifactId> ./target/myapp # starts in ~100ms! # Option 2: OCI image via Buildpacks (no local GraalVM needed — uses Docker) ./mvnw spring-boot:build-image -Pnative # Option 3: Dockerfile with multi-stage build # Stage 1: AOT + native-image compilation # Stage 2: minimal distroless/scratch image with the binary # Dockerfile — multi-stage native build FROM ghcr.io/graalvm/native-image-community:21 AS builder WORKDIR /app COPY . . RUN ./mvnw -Pnative -DskipTests native:compile FROM debian:bookworm-slim WORKDIR /app COPY --from=builder /app/target/myapp ./myapp EXPOSE 8080 ENTRYPOINT ["./myapp"] Native image compilation is memory-intensive. Allow at least 4–8 GB RAM for the build step in CI. Use -J-Xmx6g in <buildArgs> if the build runs out of memory.

Run your integration tests as a native binary to catch hint gaps before deploying.

# Compile tests to native and run them ./mvnw -PnativeTest test # This: # 1. Runs AOT on test sources # 2. Compiles a test-native binary # 3. Executes the binary — all @SpringBootTest tests run natively // Tests look identical — the difference is they run in native image @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class NativeCompatibilityIT { @Autowired TestRestTemplate restTemplate; @Test void healthEndpoint_returns200() { ResponseEntity<String> response = restTemplate.getForEntity("/actuator/health", String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @Test void createOrder_persistsToDatabase() { // Tests that JPA, Jackson, and your service layer all work natively ResponseEntity<OrderDto> response = restTemplate.postForEntity( "/orders", new CreateOrderRequest("item-1"), OrderDto.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getBody().id()).isNotNull(); } }

The output or file structure below illustrates the expected result.

// ❌ ClassNotFoundException at runtime in native image // → A class is loaded reflectively but not registered // Fix: add @RegisterReflectionForBinding or a RuntimeHintsRegistrar // ❌ java.lang.reflect.InaccessibleObjectException // → Missing reflection hint for a field or method // Fix: add MemberCategory.INVOKE_DECLARED_METHODS / DECLARED_FIELDS to hints // ❌ Missing resource: could not find ...properties / ...xml // Fix: add resource pattern to RuntimeHints or resource-config.json // ❌ JDK proxy cannot be created for interface X // Fix: hints.proxies().registerJdkProxy(X.class) // ❌ Build fails: "com.example.Foo is not accessible" // → Third-party library uses internal JDK APIs blocked by modules // Fix: add --add-opens JVM args in native-maven-plugin <buildArgs> # Use the native-image agent to auto-generate hints by running the app normally java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image \ -jar target/myapp.jar # Exercise all code paths while the agent is running, then Ctrl+C # The agent writes reflect-config.json, resource-config.json, proxy-config.json # Review and commit these — they are your starting point for hints The native-image-agent is the fastest way to discover missing hints for third-party libraries. Run your full test suite with the agent attached, then review the generated JSON files for anything not covered by Spring's AOT engine.