skip to content

Explain how spring-boot process-aot and native:compile are bound in the Maven lifecycle, and why the ordering matters.

level: middleimportance: must knowfreq 45%

answer

  1. process-aot bound in compile stage, native:compile at package
  2. closed-world: all hints known before image build
  3. process-aot generates bean code + reflect/resource-config.json
  4. native:compile forks; compile-no-fork bound to package
  5. always build via the native profile

basics

~20 s

In the native profile, Spring Boot binds its process-aot goal to run during the build's earlier phases; native:compile (bound around the package phase) runs after it. process-aot generates bean code and reachability hints that native-image needs, so it must run first.

solid answer

~40 s

Spring Boot's `native` profile chains two plugins. `spring-boot-maven-plugin:process-aot` is bound to run during compilation (it needs compiled classes, then emits AOT-generated Java sources plus `META-INF/native-image` config — reflect/resource/proxy hints), and the GraalVM `native-maven-plugin` runs its build goal later (`native:compile` forks to `package`; `compile-no-fork` binds to `package`). The ordering is mandatory: GraalVM does a closed-world static analysis, so every reflective access, resource, and proxy must be declared before the image is built. process-aot produces exactly those declarations plus the generated `@Configuration`/bean-registration code, so if it ran after native-image the binary would be missing bean definitions and hints and would fail at startup. Spring Boot's profile guarantees the correct phase binding, which is why you should build through the profile rather than invoking `native:compile` standalone.

code

java · 24 lines
java
// Custom hints are emitted by process-aot because it runs the context at
// build time BEFORE native:compile. Register what static analysis can't see:
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.stereotype.Component;

@Component
@ImportRuntimeHints(MyReflectionHints.Registrar.class)
class MyReflectionHints {
    static class Registrar implements RuntimeHintsRegistrar {
        @Override
        public void registerHints(RuntimeHints hints, ClassLoader cl) {
            // Reflective type native-image can't discover on its own
            hints.reflection().registerType(
                com.example.dto.LegacyPayload.class,
                MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
                MemberCategory.DECLARED_FIELDS);
            hints.resources().registerPattern("templates/*.txt");
        }
    }
}
// process-aot writes these into META-INF/native-image/**; native:compile then consumes them.

go deeper

for a junior

Know that process-aot runs before native:compile and generates needed hints.

for a middle

Explain the phase bindings, closed-world analysis, and why order is mandatory.

for a senior

Diagnose process-aot build-time context failures and missing-hint runtime errors from the generated sources.

for a principal

Set conventions so build-time context startup is side-effect-free and library RuntimeHints coverage is audited before adopting native.

## The Maven lifecycle context Maven's default lifecycle runs phases in order: `validate` → `compile` → `process-classes` → `test` → `package` → ... Plugins bind *goals* to *phases*. A Spring Boot native build layers two plugins onto this sequence: 1. **`spring-boot-maven-plugin` → `process-aot` goal.** This runs Spring's **AOT engine** (`ApplicationContextAotGenerator` / the AOT `SpringApplication` run). It actually *starts* your application context at build time (no web server), inspects the fully-configured bean factory, and then generates: - Java source implementing bean registrations (replacing runtime reflection/`@Configuration` parsing with explicit generated code — these become `*__BeanDefinitions` / an `ApplicationContextInitializer`), - GraalVM reachability metadata under `META-INF/native-image/<group>/<artifact>/`: `reflect-config.json`, `resource-config.json`, `serialization-config.json`, `proxy-config.json`, plus `RuntimeHints` contributed by libraries. It is bound so that it runs after classes compile (Spring Boot binds it into the compile portion of the build in the `native` profile). The generated sources are then compiled too. 2. **`native-maven-plugin` → build goal.** `native:compile` **forks** a parallel lifecycle up to `package` and then runs `native-image`. The complementary `native:compile-no-fork` is **bound to the `package` phase** in the profile (so `mvn -Pnative package` works without a second fork). There is also `native:add-reachability-metadata` (bound earlier) that merges community metadata into the classpath before the image build. ## Why ordering is non-negotiable GraalVM `native-image` performs a **closed-world static reachability analysis**: it must see, at build time, every class that will be loaded, every method reached by reflection, every resource read, and every JDK dynamic proxy created. Anything not declared is simply *absent* from the executable — you get `ClassNotFoundException`, `MissingResourceException`, or reflection failures at runtime. Spring relies heavily on reflection and `@Configuration` proxying, which are invisible to GraalVM's static analysis. `process-aot` is what converts those dynamic behaviors into (a) explicit generated code and (b) explicit JSON hints. If `native:compile` ran **before** `process-aot`, none of that would exist yet and the image would be broken. Hence process-aot must complete first — the profile's phase bindings enforce it. ## Practical implications & gotchas - **Always build through the profile.** Invoking `native:compile` without the profile skips AOT wiring. - **process-aot fails fast** if your context can't start at build time (e.g. a bean that opens a DB connection in its constructor). Keep build-time context startup side-effect-free. - **Test AOT separately:** `spring-boot:process-test-aot` + `-PnativeTest` build/run tests natively; the same ordering logic applies. - **Generated sources live in `target/spring-aot/main/sources`;** they are real Java that you can inspect when debugging missing-bean issues. - **`add-reachability-metadata`** runs before the compile goal so community metadata is on the image classpath alongside the AOT-generated hints. ## When this bites you Custom reflection the AOT engine can't infer (e.g. reflective access to a class known only at runtime) still needs manual `RuntimeHintsRegistrar` / `@RegisterReflectionForBinding`. process-aot picks those up because they run within the same build-time context, but only if you registered them — ordering guarantees they're *emitted*, not that they're *complete*.

  • What does process-aot actually do at build time that ordinary compilation doesn't?
    It starts the Spring application context (build-time run), inspects the configured bean factory, and generates explicit Java bean-registration source plus GraalVM reachability metadata (reflect/resource/proxy/serialization config). This replaces runtime reflection and @Configuration proxying with statically analyzable code the native image can compile.
  • What happens if a bean does I/O in its constructor during a native build?
    process-aot runs the context at build time, so the constructor executes then — it may try to hit a DB or external service that isn't available during the build and fail the build. Keep build-time context startup free of external side effects; defer such work to runtime (e.g. lazy init or lifecycle callbacks).

saying these in an interview costs you the question

  • Claiming native:compile does AOT hint generation itself
  • Saying ordering doesn't matter because native-image scans everything anyway
  • Not knowing process-aot starts the context at build time
  • Believing you can safely run native:compile without the native profile

context