skip to content

Explain the 'closed-world assumption' of native image and how it constrains a Spring application at build and run time.

level: seniorimportance: must knowfreq 55%

answer

  1. build-time reachability analysis → only proven-reachable code ships
  2. reflection / proxies / resources / serialization need hints
  3. Spring AOT engine auto-contributes most hints
  4. RuntimeHintsRegistrar + @ImportRuntimeHints; @RegisterReflectionForBinding
  5. conditions/profiles fixed at build → one binary per config; failures at runtime not build

basics

~20 s

Native image assumes a closed world: every class, reflective call, resource, and proxy that will ever run must be known at build time. Anything discovered dynamically at runtime (unregistered reflection, runtime classpath additions, dynamic proxies) simply isn't there and fails.

solid answer

~50 s

GraalVM does **static reachability analysis** at build time and includes only code it can prove is reachable — a **closed-world assumption**. Anything the analysis can't see statically must be declared, or it won't exist in the binary. That mainly bites on **reflection, dynamic proxies, resources, JNI, and serialization**, which frameworks use heavily. Spring mitigates this with its **AOT engine**, which processes the context at build time and contributes GraalVM **reachability metadata** (hints) automatically for most Spring/Boot usage; libraries also ship metadata in the GraalVM reachability-metadata repository. For your own dynamic code you register hints via **RuntimeHintsRegistrar** / `@ImportRuntimeHints`, or annotations like `@RegisterReflectionForBinding`. The rigidity: no arbitrary runtime class loading, conditional beans are largely fixed at build time, and profiles/config that change the bean graph must be decided when you build — so one binary per meaningful configuration. Missing a hint yields a runtime `ClassNotFoundException`/reflection failure, not a build error.

code

java · 24 lines
java
// Register hints for code the static analysis can't see (e.g. a DTO you reflect over).
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;

@Configuration
@ImportRuntimeHints(MyRuntimeHints.class)
class AppConfig { }

class MyRuntimeHints implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        // Make ReportDto reflectively constructable + its members visible at runtime
        hints.reflection().registerType(ReportDto.class,
                MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
                MemberCategory.DECLARED_FIELDS);
        // Embed a resource that is only loaded via getResourceAsStream at runtime
        hints.resources().registerPattern("templates/report.json");
    }
}

record ReportDto(String id, int count) { }

go deeper

for a junior

Grasp the idea: everything must be known at build time; reflection needs registering.

for a middle

Name what breaks (reflection/proxies/resources) and that Spring AOT + hints handle most of it.

for a senior

Explain reachability analysis, RuntimeHintsRegistrar, build-time bean graph, and runtime-not-build failure mode.

for a principal

Judge architectural fit — reject native for plugin/dynamic-reflection designs; plan per-config binaries and native testing in CI.

**Closed-world, defined.** To produce a self-contained executable, GraalVM performs **static points-to / reachability analysis** at build time: starting from entry points it computes every method, field, and class that *could* be reached, and bakes only those into the binary. Whatever the analysis can't prove reachable is **absent** from the image. This is the **closed-world assumption** — the set of code and data is fixed at build time and cannot grow at runtime. **What breaks.** Static analysis can't follow *dynamic* behavior driven by strings/config: - **Reflection** — `Class.forName(name)`, `getDeclaredMethod(...)`, reflective instantiation. If the target isn't registered, it isn't in the image. - **Dynamic proxies** — `java.lang.reflect.Proxy` interface sets must be pre-declared. - **Resources** — files loaded via `getResourceAsStream` must be registered to be embedded. - **Serialization / JNI** — also need explicit registration. All of these are pervasive in Spring (component scanning, `@Configuration` proxying, Jackson binding, JPA entities, etc.). **How Spring copes.** The **Spring AOT engine** (`process-aot`, run automatically in native builds) executes the `ApplicationContext` *at build time*: it resolves bean definitions, evaluates most conditions, and **generates source code** for bean registration plus **GraalVM reachability metadata** (reflection/resource/proxy hints) for the framework's own needs. Libraries contribute their hints via the community **GraalVM reachability metadata repository**, which the build consumes. Together these cover the vast majority of typical Boot apps with zero manual work. **Your own hints.** When you do dynamic things the analysis can't see, register them explicitly: - Implement **`RuntimeHintsRegistrar`** and attach it with **`@ImportRuntimeHints`**, calling `hints.reflection().registerType(...)`, `hints.resources().registerPattern(...)`, `hints.proxies()...`. - Or use **`@RegisterReflectionForBinding(SomeDto.class)`** for reflective (e.g., JSON) binding of specific types. - Testing: run under the GraalVM agent or the Boot `nativeTest` task to surface missing hints early. **The rigidity cost.** 1. **Build-time bean graph.** `@Conditional` / `@Profile` decisions are largely resolved at build time. If profile `prod` vs `dev` changes which beans exist, you effectively bake that in — leading toward **one binary per meaningful configuration**, unlike a JVM jar you re-profile at launch. (Property *values* can still be read at runtime; it's structural, bean-graph-changing conditions that are fixed.) 2. **No arbitrary runtime class loading.** Plugin systems that load unknown jars at runtime, or code that reflects over classes chosen from user input, don't work without registering every possibility up front. 3. **Failures move right, not left.** A missing hint is usually **not** a build error — it surfaces at runtime as `ClassNotFoundException`, `NoSuchMethodException`, missing-resource, or a proxy failure, often only on a rarely-hit code path. This is why native apps need to be exercised (integration/native tests) before trusting them. **When to accept it.** Closed-world is fine — even desirable (smaller attack surface, no surprise class loading) — for well-defined services and functions. It's a poor fit for highly dynamic, plugin-heavy, or reflection-by-arbitrary-string designs.

  • If you forget a reflection hint, when do you find out?
    Usually at runtime, not build time — a ClassNotFoundException / NoSuchMethodException or missing-resource error, often on a code path you didn't exercise. That's why native builds need integration/native tests and can be run under the GraalVM tracing agent to capture hints.
  • Can you still switch Spring profiles at runtime with a native image?
    You can read property values at runtime, but conditions that change the *bean graph* (@Conditional/@Profile adding or removing beans) are largely resolved at build time. If profiles change which beans exist, you effectively build one binary per configuration.
  • Do you have to write hints for Spring and common libraries yourself?
    Mostly no. The Spring AOT engine contributes hints for framework usage automatically, and libraries ship metadata via the GraalVM reachability-metadata repository. You only write hints for your own dynamic/reflective code the analysis can't see.

saying these in an interview costs you the question

  • Believing missing hints cause a build failure (they usually fail at runtime)
  • Thinking you can load arbitrary classes at runtime in a native image
  • Assuming @Profile is fully runtime-selectable like on the JVM
  • Claiming you must hand-write all reflection metadata for Spring itself

context