A Spring native image builds successfully but throws MissingReflectionRegistrationError at runtime. What happened and how do you fix it?
answer
- no static edge -> metadata eliminated
- build OK, fails when path runs
- MissingReflectionRegistrationError
- fix: RuntimeHints / @RegisterReflectionForBinding
- tracing agent -> META-INF/native-image json
basics
~20 sThe points-to analysis couldn't see a reflective call, so the target's metadata was left out of the closed world. The build still succeeds; the failure appears when that code runs. Fix it by registering a reflection hint.
solid answer
~40 sThe build succeeds because the missing reflection is only discovered when the code path executes — the closed-world analysis had no static edge to that reflective target, so it eliminated the metadata. At runtime, when Spring/Jackson/etc. reflectively touches the type, Substrate VM raises MissingReflectionRegistrationError. The fix is to declare a reachability hint so the analysis includes it: use a RuntimeHintsRegistrar with @ImportRuntimeHints and register the type's constructors/methods/fields via hints.reflection().registerType(...), or @RegisterReflectionForBinding for DTOs that are (de)serialized. For broad or third-party cases, run integration tests under the GraalVM tracing agent (-agentlib:native-image-agent) to auto-generate the META-INF/native-image config, then rebuild. Prevention: keep good AOT-supported starters, and add a native smoke test so these surface in CI rather than production.
code
java · 20 linesimport org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
@Configuration
@ImportRuntimeHints(ReflectionFix.Hints.class)
public class ReflectionFix {
static class Hints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// The class was instantiated reflectively via a runtime string,
// invisible to points-to analysis -> register it explicitly.
hints.reflection().registerType(com.acme.PaymentProcessor.class,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS);
}
}
}go deeper
Recognize the error means a reflection hint is missing.
Explain why build passes but runtime fails, and apply RuntimeHints / agent fixes.
Pick the right hint mechanism per case and prevent recurrence with native smoke tests + minimal MemberCategory.
Institutionalize a metadata pipeline (agent + reachability-metadata repo) and CI native testing across services.
## Why the build passed but runtime failed The closed-world **points-to analysis** only keeps what it can prove reachable through **static edges**. A reflective call like `clazz.getDeclaredConstructor().newInstance()` where `clazz` comes from a string or a generic parameter is **invisible** to that analysis. So the builder **dead-code-eliminates** the target's reflection metadata. Crucially, this is **not a build error** — the builder has no way to know you'll need it. The problem only manifests when the reflective code **actually executes**, at which point Substrate VM throws **`MissingReflectionRegistrationError`** (older/related: `ClassNotFoundException`, `NoSuchMethodException`, or an empty result from `getDeclaredFields`). ## The fix: declare a reachability hint **Option A — programmatic hints (your own code):** ```java @Configuration @ImportRuntimeHints(AppHints.class) class AppConfig {} class AppHints implements RuntimeHintsRegistrar { public void registerHints(RuntimeHints hints, ClassLoader cl) { hints.reflection().registerType(Widget.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS); } } ``` **Option B — DTO (de)serialization:** annotate with `@RegisterReflectionForBinding(OrderDto.class)` so Jackson/validation reflection is kept. **Option C — tracing agent (third-party / broad):** run your integration tests on the JVM with `-agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image`, exercise the failing path, commit the generated JSON (`reflect-config.json`, `resource-config.json`, `proxy-config.json`, `serialization-config.json`), then rebuild. ## Choosing the approach - Your own small surface → programmatic hints (type-safe, refactor-friendly). - Data binding DTOs → `@RegisterReflectionForBinding`. - Library-driven or large/unknown surface → tracing agent, plus check the **GraalVM Reachability Metadata repository** which already ships metadata for many popular libraries (Spring wires this in automatically). ## Prevention - Prefer **AOT-aware Spring starters** so hints come for free. - Add a **native smoke test** (`nativeTest`) hitting the risky endpoints so missing hints fail **CI**, not production. - Capture agent config from **representative** runs — unexercised branches won't be recorded and will still fail. ## Gotchas - **Over-registration bloats** the image and slows the build; register the minimal `MemberCategory` you need. - Reflection hints must match **exactly** what the code calls (constructors vs methods vs fields) — registering the type alone isn't always enough. - Resources and dynamic proxies have their **own** hint categories (`hints.resources()`, `hints.proxies()`); a reflection hint won't cover a missing resource.
- Why didn't the native build fail if the metadata was missing?Because the builder can't know a reflective, string-driven target will be needed — there's no static edge to it. It legitimately eliminates the metadata; the gap only shows up at runtime when that path executes.
- How would you catch these before production?Add a native test/smoke suite (GraalVM nativeTest) that exercises the reflective paths in CI, and generate metadata via the tracing agent from representative integration tests so unexercised branches are surfaced.
saying these in an interview costs you the question
- Expecting the native build to fail when a hint is missing
- Registering an entire package for reflection 'to be safe'
- Thinking a reflection hint also covers missing resources or proxies
- Generating agent config from a trivial run that misses real paths