What does the @ImportRuntimeHints annotation do in a Spring application?
answer
- Registrar wired to a @Configuration/bean
- runs during AOT, not runtime
- reflection / resources / proxies / serialization hints
- no-arg ctor, not a bean
- alternative to spring/aot.factories
basics
~20 s@ImportRuntimeHints is placed on a @Configuration class or bean to register one or more RuntimeHintsRegistrar classes. During AOT/native-image build, Spring calls those registrars so they can declare reflection, resource, and proxy hints the GraalVM compiler needs.
solid answer
~40 s@ImportRuntimeHints links a RuntimeHintsRegistrar (a class implementing registerHints(RuntimeHints, ClassLoader)) to a Spring component — usually a @Configuration class or any bean. When Spring runs ahead-of-time (AOT) processing, typically for a GraalVM native image, it invokes each referenced registrar so it can programmatically declare hints: which classes need reflection, which resources must be bundled, which JDK proxies are used, which types are serialized. These hints tell the native-image compiler to keep metadata that closed-world static analysis would otherwise strip. Without them, code that works on the JVM fails at native runtime with ClassNotFoundException, missing-method, or missing-resource errors. It is the annotation-driven, ergonomic way to contribute hints, as an alternative to registering a RuntimeHintsRegistrar in a spring/aot.factories file.
code
java · 21 linesimport 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(MyConfig.MyHints.class)
class MyConfig {
static class MyHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.reflection().registerType(
com.example.SomeReflectivelyUsedType.class,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS);
hints.resources().registerPattern("templates/*.html");
}
}
}go deeper
Know it registers a RuntimeHintsRegistrar for native-image hints and runs at build time, not runtime.
Should be able to write a registrar, call reflection()/resources(), and place @ImportRuntimeHints on a config.
Contrast @ImportRuntimeHints (bean-scoped) with spring/aot.factories (global) and the @Reflective family; know the no-arg-ctor constraint.
Reason about hint strategy across a modular codebase, conditional configs at AOT time, and testing hints with RuntimeHintsPredicates.
## The problem it solves GraalVM **native image** compiles a Java application ahead-of-time under a **closed-world assumption**: everything reachable must be known at build time. Dynamic features — **reflection**, **JDK dynamic proxies**, loading **resources** from the classpath, **serialization** — are invisible to static analysis, so the compiler strips the metadata for them. Code that relied on `Class.forName(...)`, `clazz.getDeclaredMethod(...)`, or `getResourceAsStream(...)` then fails at native **runtime**. Spring solves this with **runtime hints**: a build-time description of the dynamic behavior your app needs, emitted into GraalVM's `reachability-metadata`/config files during Spring's **AOT processing** phase. ## RuntimeHintsRegistrar `org.springframework.aot.hint.RuntimeHintsRegistrar` is the interface you implement: ```java public interface RuntimeHintsRegistrar { void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader); } ``` `RuntimeHints` exposes sub-registries: `hints.reflection()`, `hints.resources()`, `hints.proxies()`, `hints.serialization()`, and `hints.reflection().registerType(...)` etc. You call these to declare what the native compiler must preserve. ## @ImportRuntimeHints — the wiring `org.springframework.context.annotation.ImportRuntimeHints` takes an array of `RuntimeHintsRegistrar` classes and attaches them to the annotated component: ```java @Configuration @ImportRuntimeHints(MyRuntimeHints.class) class MyConfig { } ``` During AOT processing (e.g. `./gradlew nativeCompile`, `mvn -Pnative`, or the `process-aot` phase), Spring's **AOT engine** discovers every bean/config carrying `@ImportRuntimeHints`, **instantiates each registrar via its default (no-arg) constructor**, and calls `registerHints`. The resulting hints are merged with hints from starters, `@Reflective`-annotated code, and `spring/aot.factories`, then written to GraalVM's config so `native-image` keeps the metadata. ## Where it can go - On a `@Configuration` class (most common). - On any `@Component`/`@Bean` class — it is a meta-annotation-friendly, `@Import`-adjacent mechanism processed as configuration metadata. - It is **repeatable-by-array**: `@ImportRuntimeHints({A.class, B.class})`. ## Alternative registration paths (know the difference) 1. **`@ImportRuntimeHints`** — declarative, tied to a bean; the registrar only runs if that bean/config is part of the context. Best when hints are specific to a feature/config. 2. **`spring/aot.factories`** — list `RuntimeHintsRegistrar` under the `org.springframework.aot.hint.RuntimeHintsRegistrar` key. This runs **globally/unconditionally** during AOT of any app on the classpath, regardless of which beans are active. Best for library/starter authors. 3. **`@Reflective` / `@RegisterReflectionForBinding` / `@RegisterReflection`** — targeted convenience annotations for the common reflection cases without writing a registrar. ## Key gotchas - **Runs at build time, not runtime.** The registrar executes during AOT processing, never when the app serves traffic. Don't put runtime logic there. - **No-arg constructor required.** Spring instantiates the registrar reflectively with no dependency injection — it is not a bean and cannot @Autowired anything. - **Hints are additive, not automatic.** Declaring the registrar does nothing unless you actually call the `RuntimeHints` methods for the right types/resources. - **Conditional beans.** If the @Configuration carrying the annotation is excluded (e.g. by `@ConditionalOnProperty`) it still participates in AOT the same way it would at runtime — AOT evaluates conditions at build time, so an excluded config contributes no hints. - **Only matters for AOT/native.** On a plain JVM the annotation is effectively inert; nothing breaks, no hints are needed. - **Test with `RuntimeHintsPredicates`.** Assert your registrar produced the expected hints in a unit test rather than discovering gaps only at native-image build.
- When would the registrar's registerHints method actually execute?Only during Spring's ahead-of-time (AOT) processing phase — the build-time step that runs before/for a GraalVM native image (or when AOT mode is explicitly enabled). Never during normal request-serving runtime.
- Does anything happen if you run the app on a normal JVM without native image?Effectively nothing — the hints aren't needed because the JVM keeps all reflective/resource metadata by default. The annotation is inert unless AOT/native processing runs, so it's safe to leave in place.