skip to content

How are RuntimeHints contributed to a Spring AOT build, and how do the sub-APIs map to GraalVM config files?

level: principalimportance: should knowfreq 38%

answer

  1. RuntimeHintsRegistrar.registerHints(hints, cl)
  2. @ImportRuntimeHints or aot.factories or AOT processor
  3. process-aot / processAot build phase
  4. Writer → META-INF/native-image/<g>/<a>/*.json
  5. Verify with RuntimeHintsPredicates + tracing agent

basics

~10 s

Implement RuntimeHintsRegistrar and register it via @ImportRuntimeHints or META-INF/spring/aot.factories. During AOT processing Spring collects all hints and a config writer emits reflect-/resource-/proxy-/serialization-/jni-config.json for GraalVM.

solid answer

~40 s

You contribute hints by implementing `RuntimeHintsRegistrar` (`registerHints(RuntimeHints, ClassLoader)`) and wiring it in one of three ways: `@ImportRuntimeHints(MyHints.class)` on a `@Configuration`/component, listing it under `RuntimeHintsRegistrar` in `META-INF/spring/aot.factories`, or letting a `BeanFactoryInitializationAotProcessor`/`BeanRegistrationAotProcessor` write hints during bean processing. All contributions accumulate in one shared `RuntimeHints`. At build time — the `process-aot` phase (Spring Boot AOT plugin / `processAot` task) — Spring runs these, then a `NativeConfigurationWriter` (e.g. `FileNativeConfigurationWriter`) serializes each sub-registry to its GraalVM file under `META-INF/native-image/<group>/<artifact>/`: `reflection()`→reflect-config.json, `resources()`→resource-config.json, `proxies()`→proxy-config.json, `serialization()`→serialization-config.json, `jni()`→jni-config.json. GraalVM's `native-image` then consumes them. You verify with `RuntimeHintsPredicates` in tests. Newer GraalVM consolidates these into `reachability-metadata.json`, but the mapping is conceptually the same.

code

java · 28 lines
java
// 1. The registrar
public class MyHints implements RuntimeHintsRegistrar {
    @Override public void registerHints(RuntimeHints hints, ClassLoader cl) {
        hints.reflection().registerType(OrderDto.class,
                MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
        hints.resources().registerResourceBundle("messages");
    }
}

// 2a. Wire via annotation on a @Configuration bean
@Configuration
@ImportRuntimeHints(MyHints.class)
class MyConfig {}

// 2b. OR globally via META-INF/spring/aot.factories:
// org.springframework.aot.hint.RuntimeHintsRegistrar=com.acme.MyHints

// 3. Verify in a plain JVM unit test (no native build needed)
class MyHintsTest {
    @org.junit.jupiter.api.Test void registersDto() {
        RuntimeHints hints = new RuntimeHints();
        new MyHints().registerHints(hints, getClass().getClassLoader());
        org.assertj.core.api.Assertions.assertThat(
            RuntimeHintsPredicates.reflection().onType(OrderDto.class)
                .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))
            .accepts(hints);
    }
}

go deeper

for a junior

Know you implement RuntimeHintsRegistrar and register it; the framework turns hints into GraalVM config.

for a middle

Name the registration mechanisms and the file each sub-API maps to.

for a senior

Explain the process-aot phase, the config writer, META-INF/native-image location, and predicate-based testing.

for a principal

Own strategy: third-party metadata, precision-vs-bloat, where to place registrars, CI verification discipline, and the reachability-metadata consolidation.

## The three ways to register hints 1. **`@ImportRuntimeHints`** — annotate a `@Configuration` class (or any component) with `@ImportRuntimeHints(MyHints.class)`, where `MyHints implements RuntimeHintsRegistrar`. Spring picks it up during AOT for that bean's context. Best for hints tied to a specific feature/config. 2. **`META-INF/spring/aot.factories`** — declare the registrar globally: ``` org.springframework.aot.hint.RuntimeHintsRegistrar=com.acme.MyHints ``` Used by libraries/starters to contribute hints without a bean, always applied. 3. **AOT processors** — implement `BeanFactoryInitializationAotProcessor` or `BeanRegistrationAotProcessor`; their generated `...Contribution` can call `generationContext.getRuntimeHints()` to add hints programmatically while also generating code. This is how Spring itself contributes most framework hints (reflection for beans, proxies for AOP, etc.). There's also `@Reflective` / `@RegisterReflectionForBinding` convenience annotations that trigger reflection hints declaratively for DTOs. ## The AOT build phase Spring AOT runs at **build time**, not runtime. Concretely, `process-aot` (Maven `spring-boot:process-aot`, Gradle `processAot` task from the Spring Boot plugin) executes an in-memory refresh of the `ApplicationContext` without starting it, runs every AOT processor and `RuntimeHintsRegistrar`, and produces: generated source code (bean definitions, proxies), and the accumulated `RuntimeHints`. ## From `RuntimeHints` to GraalVM files A `NativeConfigurationWriter` — typically `FileNativeConfigurationWriter` — serializes the single accumulated `RuntimeHints` object. Each sub-registry maps to one classic GraalVM config file, all placed under `META-INF/native-image/<groupId>/<artifactId>/`: | Sub-API | GraalVM file | |---|---| | `reflection()` | `reflect-config.json` | | `resources()` (patterns + bundles) | `resource-config.json` | | `proxies()` | `proxy-config.json` | | `serialization()` | `serialization-config.json` | | `jni()` | `jni-config.json` | GraalVM's `native-image` tool then reads these to decide what to retain. The Buildpacks/`bootBuildImage` or `native:compile` step invokes it. > Evolution: recent GraalVM versions merge these into a single `reachability-metadata.json`; Spring's writer emits the format the toolchain expects. Conceptually the categories are unchanged. ## Verifying hints Because missing hints fail at *runtime*, Spring provides `RuntimeHintsPredicates` for unit tests: ```java RuntimeHints hints = new RuntimeHints(); new MyHints().registerHints(hints, getClass().getClassLoader()); assertThat(RuntimeHintsPredicates.reflection().onType(OrderDto.class) .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) .accepts(hints); ``` Complement with GraalVM's **tracing agent** (`-agentlib:native-image-agent`) to discover hints from a real run, and with native integration tests. ## Strategic considerations (principal-level) - **Third-party libs without metadata**: prefer libraries that ship their own `reachability-metadata` (GraalVM reachability-metadata repository) or Spring hints; otherwise you own their hints — a maintenance burden. - **Precision vs bloat**: broad reflection categories inflate image size and erode the security posture of closed-world analysis; register narrowly. - **Where to put hints**: feature-local `@ImportRuntimeHints` for app code; `aot.factories` for reusable library modules. - **Testing discipline**: treat hints as code — unit-test with predicates and run at least one native (or agent-assisted) smoke test in CI, since JVM tests won't catch missing hints. ## Gotchas - Editing the generated JSON by hand is an anti-pattern — it's overwritten each build; change the registrar instead. - `@ImportRuntimeHints` only fires if the annotated bean is actually part of the AOT-processed context. - Hints registered but never reached still bloat the image; prune with agent output.

  • When exactly do RuntimeHintsRegistrars run — at app startup or at build time?
    At build time, during Spring AOT processing (the process-aot / processAot phase). Spring performs a non-started context refresh, runs all registrars and AOT processors, and a NativeConfigurationWriter emits the GraalVM JSON. At native runtime the hints are already baked into the image; registrars don't execute then.
  • How do you catch a missing hint without doing a full native build every time?
    Unit-test with RuntimeHintsPredicates against a RuntimeHints you populate from your registrar — fast, JVM-only. Additionally use GraalVM's tracing agent on a JVM run to discover needed hints, and keep at least one real native smoke test in CI as the final backstop.
  • Why is hand-editing the generated reflect-config.json a bad idea?
    It's regenerated on every AOT build and your edits are overwritten. The source of truth is the RuntimeHintsRegistrar / AOT processor. Change the registrar so the hint is reproducible and reviewable as code.

saying these in an interview costs you the question

  • Believing registrars run at application startup rather than build time
  • Hand-editing generated *-config.json files
  • Skipping any hint verification because JVM tests pass (they won't catch missing hints)
  • Thinking @ImportRuntimeHints works even if the bean isn't in the AOT context

context