How do you verify a RuntimeHintsRegistrar produces the right hints, and how do @Conditional configs interact with @ImportRuntimeHints at AOT time?
answer
- RuntimeHintsPredicates.accepts(hints)
- new RuntimeHints() + call registerHints directly
- conditions evaluated at BUILD time
- excluded config → no hints → native failure
- bean set is frozen for native
basics
~20 sUnit-test the registrar by calling registerHints on a fresh RuntimeHints and asserting with RuntimeHintsPredicates (e.g. reflection().onType(...)). Remember AOT evaluates @Conditional/@Profile at build time, so a config excluded during the AOT build contributes no hints — align build-time conditions with intended native behavior.
solid answer
~40 sTo verify hints, instantiate the registrar directly, pass a new RuntimeHints and a ClassLoader to registerHints, then assert the expected entries with org.springframework.aot.hint.support.RuntimeHintsPredicates — e.g. RuntimeHintsPredicates.reflection().onType(Foo.class).withMemberCategory(...), .resource().forResource(...), or proxies(). This catches missing MemberCategory or missing resource patterns at unit-test speed instead of at native-image build. On conditions: Spring's AOT engine runs @Conditional/@Profile evaluation at build time using the build-time Environment. A @Configuration carrying @ImportRuntimeHints that is excluded during the AOT build never instantiates its registrar, so its hints are absent from the image. Consequently, if a feature can be toggled on at native runtime, its enabling condition must also hold during the AOT build, or you move the hints to an unconditional channel (aot.factories). This build-time-vs-runtime mismatch is the classic native-image footgun.
code
java · 21 linesimport static org.assertj.core.api.Assertions.assertThat;
import org.springframework.aot.hint.*;
import org.springframework.aot.hint.support.RuntimeHintsPredicates;
import org.junit.jupiter.api.Test;
class AppConfigHintsTest {
@Test
void contributesExpectedHints() {
RuntimeHints hints = new RuntimeHints();
new AppConfig.Hints().registerHints(hints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.reflection()
.onType(OrderDto.class)
.withMemberCategory(MemberCategory.INVOKE_PUBLIC_METHODS))
.accepts(hints);
assertThat(RuntimeHintsPredicates.resource()
.forResource("config/rules/rules.json"))
.accepts(hints);
}
}go deeper
Aware you can test hints and that native is built ahead of time.
Can write a RuntimeHintsPredicates assertion for a registrar.
Understands build-time condition evaluation and its effect on which @ImportRuntimeHints fire.
Designs the hint strategy: predicate tests + CI native build, aligns build-time conditions, and decides unconditional vs conditional channels to avoid frozen-bean-set failures.
## Testing hints without building a native image Building a full native image to discover a missing hint is slow. Spring provides `org.springframework.aot.hint.support.RuntimeHintsPredicates` for fast unit tests: ```java @Test void registersReflectionForOrderDto() { RuntimeHints hints = new RuntimeHints(); new AppConfig.Hints().registerHints(hints, getClass().getClassLoader()); assertThat(RuntimeHintsPredicates.reflection() .onType(OrderDto.class) .withMemberCategory(MemberCategory.INVOKE_PUBLIC_METHODS)) .accepts(hints); assertThat(RuntimeHintsPredicates.resource() .forResource("config/rules/rules.json")) .accepts(hints); } ``` `RuntimeHintsPredicates` offers `reflection()`, `resource()`, `proxies()`, `serialization()` builder predicates you compose and pass to `accepts(hints)`/`rejects(hints)` (AssertJ-friendly). This is the recommended way to lock in a registrar's contract and prevent regressions. For higher-level coverage, `@SpringBootTest` combined with `ApplicationContextAotGenerator` / the Boot Gradle/Maven AOT tasks lets you assert over the aggregate hints, but the predicate unit test is the fast feedback loop. ## How @ImportRuntimeHints participates in AOT During AOT processing Spring builds the bean factory, evaluates configuration **at build time**, and while contributing AOT artifacts it collects every `@ImportRuntimeHints` reference reachable in that (build-time) context, instantiates each registrar via its no-arg constructor, and calls `registerHints`. The outputs merge with hints from `aot.factories`, `@Reflective` processors, and framework contributions, then serialize to GraalVM reachability metadata. ## The condition-evaluation trap This is the highest-value principal-level insight: - `@Conditional`, `@ConditionalOnProperty`, `@ConditionalOnClass`, `@Profile` are all evaluated **at AOT build time**, using whatever `Environment`/classpath exists then. - A `@Configuration` that would be active at native **runtime** but is excluded during the **build** contributes **no beans and no hints**. The bean graph is essentially frozen at build time for native images. - Therefore: if a feature is switched on by a property only at runtime, but the `@Configuration` (and its `@ImportRuntimeHints`) is conditional on that property, the AOT build won't include it and native runtime will fail with reflection/resource errors. **Mitigations:** 1. Ensure the enabling property/profile is present during the AOT build (e.g. build-time properties or a dedicated AOT profile) so the config is included. 2. Move the hints to an **unconditional** channel (`aot.factories`) so they're always emitted regardless of whether the conditional bean is active. 3. Accept that native images have a **fixed bean set** — this is a fundamental GraalVM/AOT constraint, not a Spring quirk. ## Other principal-level considerations - **Over-hinting** inflates image size and can weaken native-image security posture (more reflective surface); hint precisely. - **Deterministic registrars**: registrars must be pure/deterministic — no dependence on runtime-only state, network, or non-reproducible input, since they run at build time. - **Version/library drift**: hand-written hints can rot when a dependency changes its reflective surface; predicate tests + integration native builds in CI guard against this. - **Prefer targeted annotations** (`@RegisterReflectionForBinding`) where they suffice; reserve full registrars for resources, proxies, patterns, and computed hints. - **Layering**: framework/starter hints via `aot.factories`; app-feature hints via `@ImportRuntimeHints`; keep the two concerns cleanly separated for maintainability.
- A feature works when enabled on the JVM but its endpoints break in the native image when the same property is set. What's the AOT cause?The feature's @Configuration (and its @ImportRuntimeHints) was conditional on a property that was NOT set during the AOT build, so it was excluded and contributed no beans/hints. The native image's bean graph is frozen at build time. Fix: set the property at AOT build time, or move hints to aot.factories, or make the config unconditional.
- What's the fastest way to keep a registrar honest as dependencies evolve?A unit test using RuntimeHintsPredicates that asserts the exact types/members/resources are registered, run in CI, plus a periodic real native build. The predicate test gives seconds-level feedback for the common regressions (missing MemberCategory, dropped resource pattern).
saying these in an interview costs you the question
- Believing @Conditional is evaluated at native runtime rather than AOT build time
- Assuming you must build a native image to test that hints exist
- Thinking the native-image bean set can change per runtime property like on the JVM