How would you design a reliable workflow to catch missing resource/serialization hints for a native image before it reaches production, given JVM tests don't exercise them?
answer
- JVM-green ≠ native-green
- RuntimeHintsPredicates (resource/serialization) unit tests
- Tracing agent captures real accesses → config JSON
- CI native build + binary smoke test = ground truth
- Shrink surface: no JDK serialization, few locales
basics
~20 sCombine three layers: unit-test hints with RuntimeHintsPredicates on the JVM, use the GraalVM tracing agent to capture real accesses into config, and run an actual native-image build in CI plus a smoke test of the binary so missing-resource failures surface before prod.
solid answer
~40 sThe trap is that a normal JVM run never exercises the closed-world constraints, so missing hints only fail in the native binary. I'd defend in depth. First, **assert hints in JVM unit tests** using Spring's `RuntimeHintsPredicates` (e.g. `RuntimeHintsPredicates.resource().forResource("db/x.sql")`) against the hints my `RuntimeHintsRegistrar` produces — fast feedback, no native build. Second, **bootstrap and refresh config with the GraalVM tracing agent** (`-agentlib:native-image-agent`) driving representative flows/integration tests, so accessed resources, bundles and serialized types are captured into `resource-config.json`/`serialization-config.json`. Third, **build the native image in CI** and run a **smoke test** hitting the real endpoints/locales/serialization paths, since only the actual binary proves reachability. I'd also minimize the surface — avoid JDK serialization, prefer Boot's auto-registered config — so there are fewer hints to get wrong.
code
java · 17 linesimport org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class HintsTest {
@Test
void registersSqlAndBundleAndSerialization() {
RuntimeHints hints = new RuntimeHints();
new AppResourceHints.MyHints().registerHints(hints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.resource().forResource("db/migration/V1.sql"))
.accepts(hints);
assertThat(RuntimeHintsPredicates.resource().forBundle("messages"))
.accepts(hints);
}
}go deeper
Knows you must test the native binary, not just the JVM.
Can use the tracing agent to generate config and knows RuntimeHintsPredicates exist.
Layers predicate unit tests + agent + native build, understands each layer's limits.
Designs the full defense-in-depth pipeline, balances CI cost vs coverage, and reduces hint surface by architecture choices.
## Why this is a design question, not a config trick The **closed-world assumption** means resources, bundles, and serialization metadata are only present if registered — but a standard JVM test suite loads everything lazily and **never enforces** that constraint. So JVM-green ≠ native-green. A principal-level answer builds a **pipeline** that closes this gap cheaply and early rather than discovering `null` streams or `MissingResourceException` in production. ## Layer 1 — Test the hints on the JVM (fast) Spring provides `RuntimeHintsPredicates` to assert what a `RuntimeHintsRegistrar` registered, without a native build: ```java RuntimeHints hints = new RuntimeHints(); new MyHints().registerHints(hints, getClass().getClassLoader()); assertThat(RuntimeHintsPredicates.resource().forResource("db/migration/V1.sql")) .accepts(hints); ``` There are predicates for `resource()`, `reflection()`, `proxies()`, `serialization()`. This runs in milliseconds and guards against regressions when someone removes a hint. It proves the hint **exists**, not that it's **sufficient** — hence the later layers. ## Layer 2 — Discover reality with the tracing agent Run the app/integration tests on the JVM with `-agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image/...`. The agent records every reflective access, resource load, bundle lookup, proxy, and serialization, emitting/merging the GraalVM JSON. Drive **representative flows** — all locales, all serialization paths, all conditionally-loaded resources — because the agent only captures what your test actually exercises. This bootstraps hints you'd otherwise hand-write and error on. ## Layer 3 — Build and smoke-test the real native image in CI Ultimately only the binary tells the truth. Add a CI job: `native-image` build (or `./mvnw -Pnative native:compile` / Gradle `nativeCompile`), then run the executable and **smoke test** the endpoints that touch resources, i18n, and serialization. Native builds are slow/expensive, so run them on merges to main or nightly rather than every commit, but they must run before release. ## Layer 4 — Reduce the surface area - Prefer Spring Boot's **auto-registered** config over ad-hoc resource loading. - **Avoid JDK serialization** entirely (switch session/cache stores to JSON) to delete a whole hint category. - Restrict **locales** to what you ship. - Keep hints **colocated** with the feature (`@ImportRuntimeHints`) so they're maintained together. ## Gotchas / trade-offs - Tracing agent only sees exercised paths → incomplete coverage if tests are thin. Pair with the smoke test. - `RuntimeHintsPredicates` can pass while the native still fails if the *pattern* is wrong (glob vs regex, wrong path) — the binary smoke test catches that. - Over-registration (broad patterns, all locales) bloats image size and can pull in unwanted resources; scope tightly. - Third-party libraries may ship their own hints or reachability-metadata; check the GraalVM reachability-metadata repository before writing your own. ## The synthesis No single mechanism is sufficient: predicates give fast regression safety, the agent bootstraps correctness from real runs, and the native smoke test provides ground truth. Combined, missing-hint defects can't silently reach production.
- RuntimeHintsPredicates passes but the native binary still can't find the resource. What went wrong and how do you catch it?The hint likely uses the wrong path or pattern syntax (glob vs regex) — the predicate only checks a hint was registered, not that it matches the real file. The CI native-image smoke test that actually loads the resource is what catches this ground-truth mismatch.
- Why not just run the native build on every commit to be safe?Native-image builds are slow and resource-heavy, hurting feedback loops and CI cost. Better: fast RuntimeHintsPredicates unit tests on every commit, with the full native build + smoke test on merges to main or nightly before release.
- How does the tracing agent's coverage limitation affect your strategy?It only records paths your tests actually exercise, so thin tests yield incomplete config. You must drive representative flows (all locales, serialization paths) and still back it with a native smoke test, since unexercised branches won't be captured.