skip to content

How does the native Gradle plugin integrate with the Spring Boot Gradle plugin's AOT processing to build a native image?

level: seniorimportance: must knowfreq 40%

answer

  1. closed-world -> Spring AOT bridges runtime dynamism
  2. processAot / processTestAot tasks
  3. generates bean defs + reachability-metadata + RuntimeHints
  4. nativeCompile consumes AOT output on classpath
  5. metadata repository for 3rd-party libs; bootBuildImage sets BP_NATIVE_IMAGE

basics

~20 s

When both plugins are applied, Spring Boot registers processAot, which runs Spring's AOT engine to generate bean-registration code and reachability hints. The native plugin's nativeCompile then depends on that output and feeds it to native-image.

solid answer

~40 s

Spring Boot's Gradle plugin (3.0+) detects the `org.graalvm.buildtools.native` plugin and wires the AOT pipeline. It adds `processAot` (and `processTestAot`) tasks that execute Spring's `SpringApplicationAotProcessor`: this runs your application context at build time, emitting functional bean-definition source, generated proxy/reflection code, and `reachability-metadata` files (reflect-config, resource-config, proxy-config, serialization) under `META-INF/native-image`. The native plugin arranges `nativeCompile`'s classpath and `buildArgs` to include those generated sources and hint files, so `native-image` performs closed-world analysis with the reflection/resource/proxy information Spring needs. The Spring Boot plugin also enables the GraalVM Reachability Metadata Repository for third-party libraries. Net effect: `./gradlew nativeCompile` transparently chains `processAot -> compile generated sources -> native-image`, and `bootBuildImage` gets `BP_NATIVE_IMAGE=true` for the buildpack path.

code

java · 19 lines
java
// Contribute reachability hints AOT can't infer (reflection on a DTO):
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.context.annotation.ImportRuntimeHints;

@ImportRuntimeHints(MyApp.Hints.class)
public class MyApp {
    static class Hints implements RuntimeHintsRegistrar {
        @Override
        public void registerHints(RuntimeHints hints, ClassLoader cl) {
            hints.reflection().registerType(OrderDto.class,
                MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
                MemberCategory.INVOKE_PUBLIC_METHODS);
            hints.resources().registerPattern("data/*.json");
        }
    }
}
// processAot bakes these into META-INF/native-image; nativeCompile passes them to native-image.

go deeper

for a junior

Know that Spring needs an extra AOT step before a native build and the native plugin uses it.

for a middle

Name processAot, what it generates (bean defs + hints), and that nativeCompile consumes it.

for a senior

Explain closed-world analysis, RuntimeHints/metadata repository, and how missing hints fail only at native runtime.

for a principal

Reason about AOT freezing profiles/conditions at build time, testing via nativeTest, and governing hint contributions across modules.

## Why AOT is needed GraalVM native image uses **closed-world analysis**: everything reachable must be known at build time. Spring's traditional model — component scanning, `@Configuration` proxying (CGLIB), reflection-driven bean instantiation, dynamic proxies — is fundamentally runtime-dynamic and can't be resolved by static analysis alone. **Spring AOT** (introduced in Spring Framework 6 / Spring Boot 3) shifts that work to build time. ## What AOT produces Spring's `SpringApplicationAotProcessor` actually *starts* your `ApplicationContext` at build time (without running the app) and emits: 1. **Functional bean definitions** — generated Java source that registers beans programmatically instead of via reflection/scanning (no CGLIB `@Configuration` proxies at runtime). 2. **Generated classes** — e.g. proxy classes made concrete. 3. **Reachability metadata** — `reflect-config.json`, `resource-config.json`, `proxy-config.json`, `serialization-config.json` (and the newer unified `reachability-metadata.json`) under `META-INF/native-image/…`, telling `native-image` what reflection/resources/proxies to keep. 4. **`RuntimeHints`** — programmatic hints contributed by `RuntimeHintsRegistrar` implementations and `@ImportRuntimeHints`. ## The Gradle wiring When the Spring Boot plugin sees the GraalVM plugin applied, it: - registers **`processAot`** (main) and **`processTestAot`** tasks (`org.springframework.boot.gradle.tasks.aot.ProcessAot`); - adds the AOT-generated sources to a dedicated source set so they are compiled; - ensures **`nativeCompile`** consumes the AOT output (generated classes + metadata) and that its classpath and `configurationFileDirectories` point at the generated `META-INF/native-image`. So the effective chain is: `processAot` → compile generated sources → `nativeCompile` (invokes `native-image`). ## The metadata repository Many third-party libraries can't be fully analyzed even with AOT. The **GraalVM Reachability Metadata Repository** is a community-maintained catalog of hints keyed by library coordinates. The Spring Boot / native plugin enables it (`graalvmNative { metadataRepository { enabled = true } }`) so those hints are merged in automatically. ## Two build paths, same AOT - **`nativeCompile`** (native-build-tools) — local, needs GraalVM. - **`bootBuildImage`** (Spring Boot + buildpacks) — Spring Boot sets `BP_NATIVE_IMAGE=true` automatically when the GraalVM plugin is present, so the Paketo buildpack builds a native image in-container. Both rely on the same `processAot` output. ## Testing `processTestAot` + `nativeTest` let you run the test suite in native mode, catching missing hints (e.g. a reflection call not covered) before production. ## Common gotchas - **Missing hints** cause runtime `ClassNotFoundException`/`NoSuchMethodException` in the native binary even though the JVM run worked. Fix by adding a `RuntimeHintsRegistrar` via `@ImportRuntimeHints`, or the `@RegisterReflectionForBinding`/`@Reflective` annotations, or relying on the metadata repo. - **AOT changes behavior slightly**: profiles and conditional beans are largely evaluated at build time. `@Profile`/conditions resolved during AOT are frozen; you must set build-time-relevant properties when running `processAot` (via `springBoot { … }` or the `-Dspring.profiles.active` at AOT time). - Beans that fail to instantiate at AOT time will break the build, since the context is actually refreshed. - You still need GraalVM for `nativeCompile`; AOT alone just produces JVM-friendly generated code (you can even run AOT on the JVM with `java -Dspring.aot.enabled=true`).

  • Your app runs fine on the JVM but the native binary throws ClassNotFoundException for a reflectively loaded class. Why, and how do you fix it?
    Closed-world analysis dropped the class because no static reference reached it and no hint covered it. Add a RuntimeHintsRegistrar via @ImportRuntimeHints (or @RegisterReflectionForBinding/@Reflective), or rely on the reachability metadata repository — then processAot emits the reflect-config so native-image keeps it.
  • Does running `processAot` require GraalVM?
    No. AOT processing runs on an ordinary JVM and just generates code plus metadata. GraalVM's native-image is only needed for the `nativeCompile` step that turns that into a native binary; you can even run AOT-optimized on the JVM with `spring.aot.enabled=true`.

saying these in an interview costs you the question

  • Saying native image works with Spring 'out of the box' without AOT
  • Thinking `processAot` invokes GraalVM native-image itself
  • Believing profiles/conditions are still fully evaluated at runtime in an AOT/native app
  • Not knowing missing reachability hints surface only at native runtime

context