When and over what scope does @Reflective processing run, and what are the consequences of that design?
answer
- build-time AOT, never at runtime
- ReflectiveProcessorBeanFactoryInitializationAotProcessor drives it
- scans bean types + declared members via ReflectiveRuntimeHintsRegistrar
- bean-scoped -> non-beans ignored
- fail only in native -> use RuntimeHintsPredicates / native tests
basics
~20 sIt runs at AOT build time, not at runtime. A BeanFactoryInitializationAotProcessor scans registered bean classes and their members for @Reflective-meta-annotated annotations. So only beans (and their reachable members) are covered; arbitrary classpath types are not.
solid answer
~40 sThe @Reflective engine executes during Spring's AOT processing phase, driven by ReflectiveProcessorBeanFactoryInitializationAotProcessor — a BeanFactoryInitializationAotProcessor. It iterates the bean definitions' resolved types and, via ReflectiveRuntimeHintsRegistrar, inspects each type plus its declared constructors, methods and fields for annotations meta-annotated with @Reflective, invoking the bound processor for each hit. Consequences: (1) coverage is bean-scoped — non-bean utility/model classes are not scanned, so their annotations are ignored; (2) everything is build-time, so incorrect or missing hints only fail when the native binary runs, demanding native or AOT-hint tests; (3) processors must be cheap, stateless, no-arg-constructible; (4) for non-bean types you fall back to imperative RuntimeHintsRegistrar via @ImportRuntimeHints. Understanding this scope prevents the classic 'I annotated it but no hint appeared' bug.
code
java · 20 lines// Verify @Reflective-contributed hints at build time (no native compile needed)
RuntimeHints hints = new RuntimeHints();
// ... run AOT contribution that processes your @Reflective-annotated beans ...
assertThat(RuntimeHintsPredicates.reflection()
.onType(OrderEntity.class)
.withMemberCategory(MemberCategory.DECLARED_FIELDS))
.accepts(hints);
// For a NON-bean type, @Reflective scanning won't cover it; register imperatively:
@ImportRuntimeHints(MyHints.class)
@Configuration
class HintsConfig {}
class MyHints implements RuntimeHintsRegistrar {
public void registerHints(RuntimeHints hints, ClassLoader cl) {
hints.reflection().registerType(SomeNonBean.class,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
}go deeper
Know it happens at build time and only for beans.
Name that a BeanFactoryInitializationAotProcessor scans bean types and members.
Explain bean-scoped consequences and the JVM-vs-native failure gap, plus the RuntimeHintsRegistrar fallback.
Reason about verification strategy, hint-source merging/idempotency, determinism, and when to escape the annotation model entirely.
## Timing: build time, not runtime Spring AOT has two phases. **Build-time AOT processing** (during `process-aot` / native compilation, or in AOT-mode tests) generates code and `RuntimeHints`. **Runtime** just consumes what was generated. `@Reflective` belongs entirely to the *build-time* phase — by the time the app (or native binary) starts, the hints are already baked in. Nothing scans annotations for hints at runtime. ## Who drives it The entry point is **`ReflectiveProcessorBeanFactoryInitializationAotProcessor`** (`org.springframework.context.aot`), which implements **`BeanFactoryInitializationAotProcessor`**. During AOT, Spring calls it with the bean factory. It resolves the **types of registered bean definitions** and hands them to **`ReflectiveRuntimeHintsRegistrar`** (`org.springframework.aot.hint.annotation`). That registrar: - checks the type itself, and its **declared constructors, methods, and fields**, - looks for any annotation **meta-annotated with `@Reflective`** (using `MergedAnnotations`, so meta-meta layering works), - for each match, instantiates the referenced `ReflectiveProcessor`(s) and calls `registerReflectionHints`, accumulating into `ReflectionHints`. ## Consequences of the design ### 1. Bean-scoped coverage Only **beans and their reachable members** are scanned. If you slap a `@Reflective`-based annotation on a class Spring never registers as a bean, **nothing happens**. This is the single most common surprise. Remedies: make it a bean, reference it from a scanned bean, list it explicitly in a processor's target set (like `@RegisterReflectionForBinding`'s class list), or bypass the mechanism with a `RuntimeHintsRegistrar` registered via **`@ImportRuntimeHints`**. ### 2. Build-time failure locality A wrong hint (missing member category, wrong `ExecutableMode`) is invisible on the JVM and in JVM tests. It only manifests as `NoSuchMethodException`, `ClassNotFoundException`, or broken binding **when the native image runs**. Mitigate by: - writing **AOT hint tests** that assert with `RuntimeHintsPredicates` against the generated `RuntimeHints`, - running a **GraalVM native test** job in CI. ### 3. Processor lifecycle constraints Processors are instantiated by the registrar (no-arg constructor) at build time. Keep them **stateless and cheap**; they must not depend on a running application context. ### 4. Interaction with other hint sources `@Reflective` is one of several hint contributors. Others: framework-inferred hints (controllers, `@Configuration`), `RuntimeHintsRegistrar` + `@ImportRuntimeHints`, and GraalVM's own `reachability-metadata` from libraries. All merge into the final `RuntimeHints`. Duplicate/overlapping registrations are harmless (idempotent merge), but redundant broad categories still enlarge the binary. ### 5. Ordering / determinism Because it's build-time and driven by the resolved bean set, output is deterministic for a given bean configuration — good for reproducible native builds. ## Architectural takeaway Treat `@Reflective` as an **annotation-driven, bean-scoped** convenience layer on top of the general `RuntimeHints` API. When your reflective needs escape bean scope or need conditional logic, drop to `RuntimeHintsRegistrar`. Always pair native-targeted code with native/AOT-hint verification, since the plain JVM will happily hide the gaps.
- Why can a JVM integration test pass while the same code fails in the native image?The JVM allows unrestricted reflection, so missing hints don't matter there. The native image only permits declared reflection, so a gap in @Reflective/RuntimeHints coverage surfaces only when the native binary runs — hence you need native or RuntimeHintsPredicates-based tests.
- When should you prefer a RuntimeHintsRegistrar over the @Reflective annotation route?When the target isn't a bean (bean-scoped scanning won't reach it), when you need conditional/programmatic logic, or when you must register resource/proxy/serialization hints beyond simple annotated elements. Register it with @ImportRuntimeHints.
saying these in an interview costs you the question
- Claiming @Reflective is processed at application startup/runtime.
- Assuming every annotated class on the classpath is scanned.
- Trusting a green JVM test suite to prove native reflection correctness.
- Putting runtime state or context dependencies in a ReflectiveProcessor.