skip to content

How do Spring Boot AOT and GraalVM decide class-initialization defaults, and how do you override them for your own classes?

level: seniorimportance: should knowfreq 40%

answer

  1. GraalVM default = run-time init for app classes
  2. reachability metadata: native-image.properties + JSON
  3. Spring AOT feeds hints, doesn't set init timing
  4. override via Args line or plugin buildArgs
  5. build-time cannot depend on run-time-only -> build fails

basics

~20 s

GraalVM defaults application classes to run-time init and relies on reachability metadata for framework classes. Spring's AOT processing plus that metadata set safe defaults. You override per class with --initialize-at-build-time / --initialize-at-run-time, usually via a native-image.properties Args line.

solid answer

~40 s

GraalVM's baseline is: initialize application classes **at run time** (JVM-like), and only move specific classes to build-time when it's proven safe. It learns those decisions from **reachability metadata** — `native-image.properties` and JSON metadata shipped in jars (or under `META-INF/native-image/...`). Spring Boot's AOT engine (the `org.springframework.aot` Gradle/Maven plugin invoked by the native build) generates bean-registration code and contributes hints, while GraalVM's own metadata handles JDK/framework classes like `SecureRandom` (forced to run-time). To override for your code you pass `--initialize-at-build-time=<class>` or `--initialize-at-run-time=<class>`, typically via `META-INF/native-image/<group>/<artifact>/native-image.properties` with an `Args =` line, or through the build plugin's `buildArgs`. Prefer run-time init for anything stateful or environment-dependent; reserve build-time for pure constants. Build-time init that conflicts with a run-time-only dependency fails the build, which is the intended forcing function.

code

java · 18 lines
java
// src/main/resources/META-INF/native-image/com.example/app/native-image.properties
//
//   Args = --initialize-at-run-time=com.example.crypto \
//          --initialize-at-build-time=com.example.PureConstants
//
// Spring runtime hints are separate — they cover reflection/resources,
// not class-init timing:
class AppHints implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader cl) {
        hints.reflection().registerType(com.example.dto.Token.class,
            MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
    }
}

@Configuration
@ImportRuntimeHints(AppHints.class)
class NativeConfig { }

go deeper

for a junior

Know that you can override init timing with the two flags and that GraalVM/Spring pick defaults for you.

for a middle

Explain native-image.properties Args lines and that run-time is the safe default for app classes.

for a senior

Distinguish Spring AOT hints (reflection/resources) from GraalVM class-init flags, and know where metadata lives and how conflicts fail the build.

for a principal

Set org policy (run-time default, audited build-time exceptions), verify via behavioral tests on the binary and class-init reports, and manage metadata provenance across libraries.

## The default policy Modern GraalVM initializes classes **at run time by default** (matching JVM semantics) and selectively opts specific classes into build-time init where it is safe and beneficial. This 'run-time by default' stance is deliberately conservative: it avoids accidentally freezing environment-dependent state. ## Reachability metadata — how defaults are supplied GraalVM's closed-world compiler needs to know about reflection, resources, proxies, serialization, and **class initialization** decisions that it can't infer statically. This is provided as **reachability metadata**: - **`native-image.properties`** — build-argument file, conventionally placed at `META-INF/native-image/<groupId>/<artifactId>/native-image.properties`, with lines like: ``` Args = --initialize-at-run-time=com.example.CryptoHelper \ --initialize-at-build-time=com.example.PureConstants ``` - **JSON metadata** (`reachability-metadata.json`, historically `reflect-config.json` etc.) shipped by libraries or by the community **GraalVM Reachability Metadata Repository**, which the Spring/GraalVM build plugins consume automatically. GraalVM itself ships metadata forcing security-sensitive JDK classes (e.g. `java.security.SecureRandom`) to **run-time** init. ## Spring Boot's AOT layer Spring Boot doesn't replace GraalVM's mechanism; it **feeds** it. During a native build, Spring's **AOT processing** (`processAot` task via the `org.springframework.aot` plugin, and the GraalVM `native-maven-plugin` / `org.graalvm.buildtools.native` Gradle plugin) runs your `ApplicationContext` ahead of time, emitting: - generated bean-definition/registration Java source (replacing reflective bean setup), - **runtime hints** via `RuntimeHintsRegistrar` / `@RegisterReflectionForBinding` / `@ImportRuntimeHints` for reflection, resources, proxies, and serialization. Class-initialization flags themselves are still expressed as **native-image build args**, not Spring annotations — so overriding init timing is a GraalVM concern that you wire through the build. ## How to override, concretely 1. **Preferred — `native-image.properties`** in your own jar: ``` Args = --initialize-at-run-time=com.example.Tokens ``` 2. **Build plugin config** — Gradle `graalvmNative { binaries.all { buildArgs.add('--initialize-at-run-time=com.example.Tokens') } }`, or Maven `native-maven-plugin` `<buildArgs>`. 3. Package prefixes work too: `--initialize-at-run-time=com.example.crypto` covers a whole package. ## Class-initialization conflicts Build-time and run-time init form a dependency constraint: a build-time-initialized class may **not** transitively depend on a run-time-only class in its initializer, because you can't snapshot a not-yet-initialized dependency. GraalVM detects this and **fails the build** with a class-initialization trace, telling you which class forced run-time init. You resolve it by reclassifying — usually pushing the offending class (and often its holder) to run-time init. Treat these errors as guidance, not obstacles. ## Practical guidance / when-to-use - **Default to run-time init** for all your application and domain classes. It's safe and rarely a measurable startup cost for app code. - **Build-time init** only for pure, deterministic, side-effect-free constants and enum-like tables — and even then, only if you have a reason (constant folding, a library requiring it). - **Never** build-time-init classes whose static initializers touch env vars, files, sockets, threads, clocks, or randomness. - Trust GraalVM + Spring defaults for framework/JDK classes; override only **your** classes, sparingly, and test the resulting binary (behavioral tests, not just a successful build — a frozen secret compiles fine but misbehaves at runtime). ## Verifying Run the native binary and assert runtime behavior (fresh timestamps, distinct tokens per instance, real env values). GraalVM can also emit class-initialization reports (diagnostic build flags) to audit which classes ended up build-time vs run-time.

  • Do Spring's RuntimeHints control class-initialization timing?
    No. RuntimeHints cover reflection, resources, proxies, and serialization. Class-init timing is a GraalVM concern set via --initialize-at-build-time / --initialize-at-run-time build args (e.g. in native-image.properties), not via Spring annotations.
  • Where do you put a --initialize-at-run-time flag so it ships with a library jar?
    In META-INF/native-image/<groupId>/<artifactId>/native-image.properties as an Args = line; GraalVM auto-discovers it on the classpath during the build.
  • A successful native build still misbehaves at runtime with a null secret. What likely happened?
    A class reading the secret was initialized at build time, freezing the build machine's (empty) env value. Move it to run-time init and re-test the running binary, not just the build.

saying these in an interview costs you the question

  • Thinking a @Bean annotation or RuntimeHints controls build-time vs run-time init
  • Assuming GraalVM defaults all classes to build-time init
  • Believing a green native build proves init timing is correct

context