How would you extend Spring's AOT engine to contribute custom generated code or runtime hints for a bean the engine can't model?
answer
- BeanRegistrationAotProcessor = per bean
- BeanFactoryInitializationAotProcessor = whole factory
- return AotContribution -> applyTo emits code + hints
- RuntimeHintsRegistrar + @ImportRuntimeHints
- aot.factories, RuntimeHintsPredicates for tests
basics
~10 sImplement Spring's AOT SPI: a BeanRegistrationAotProcessor (per-bean) or BeanFactoryInitializationAotProcessor (factory-level) returns an AotContribution that emits generated code and registers RuntimeHints. For simple reflection needs, a RuntimeHintsRegistrar via @ImportRuntimeHints is enough.
solid answer
~40 sThe AOT engine is extensible through well-defined SPIs invoked by `ApplicationContextAotGenerator`. For **per-bean** customization implement **`BeanRegistrationAotProcessor`**; for **factory-wide** contributions implement **`BeanFactoryInitializationAotProcessor`**. Each is asked, during process-aot, to return an *AotContribution* (`BeanRegistrationAotContribution` / `BeanFactoryInitializationAotContribution`) whose `applyTo` method receives a `GenerationContext` and a code-generator, letting you **emit generated Java** and **register `RuntimeHints`** (reflection, resources, proxies, serialization). These processors are discovered as beans or via `spring.factories`, and a bean can opt out of default handling by having its type implement the processor. When you only need reflection/resource metadata (not custom instantiation code), the lighter path is a **`RuntimeHintsRegistrar`** wired with **`@ImportRuntimeHints`**, or annotations like **`@RegisterReflectionForBinding`**. This is how framework/library authors make otherwise-dynamic beans AOT- and native-friendly.
code
java · 40 lines// Register reflection metadata for types that Spring can't infer,
// so they work inside a GraalVM native image.
import 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(PaymentConfig.PaymentHints.class)
public class PaymentConfig {
static class PaymentHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.reflection().registerType(PaymentGateway.class,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS);
hints.resources().registerPattern("payment/*.json");
}
}
}
// For a bean the engine can't model, contribute a custom instance supplier:
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
import org.springframework.beans.factory.support.RegisteredBean;
class PaymentGatewayAotProcessor implements BeanRegistrationAotProcessor {
@Override
public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean bean) {
if (!PaymentGateway.class.isAssignableFrom(bean.getBeanClass())) {
return null; // let the engine handle other beans normally
}
return (generationContext, code) ->
generationContext.getRuntimeHints().reflection()
.registerType(PaymentGateway.class,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
}go deeper
Awareness that hints exist; @ImportRuntimeHints is the entry point.
Use RuntimeHintsRegistrar / @RegisterReflectionForBinding to fix native reflection gaps.
Choose between hint-only and BeanRegistrationAotProcessor; test with RuntimeHintsPredicates.
Design library-grade AOT integrations: per-bean vs factory-level SPI, aot.factories wiring, surgical hint policy, and CI verification of hint coverage.
## The extension points During `process-aot`, `ApplicationContextAotGenerator` drives two families of processors: ### 1. `BeanRegistrationAotProcessor` (per bean) - Method: `BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean)` (return `null` to skip). - The returned contribution's `applyTo(GenerationContext, BeanRegistrationCode)` can **customize how the bean is instantiated in generated code** (e.g. supply a reflection-free `InstanceSupplier`) and **add RuntimeHints**. - Discovered as an infrastructure bean; a bean **type** implementing this interface signals it wants to influence its own registration. ### 2. `BeanFactoryInitializationAotProcessor` (whole factory) - Method: `BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory)`. - Its `applyTo(GenerationContext, BeanFactoryInitializationCode)` contributes code that runs once at factory initialization and/or registers global hints. - Registered via a bean or `META-INF/spring/aot.factories` (the AOT counterpart of `spring.factories`). Both receive a **`GenerationContext`** exposing `getGeneratedClasses()`/`getGeneratedFiles()` (to emit source via `GeneratedMethod`/`CodeBlock` from Spring's JavaPoet fork) and **`getRuntimeHints()`**. ## The lighter hint-only paths If you don't need custom instantiation code, just metadata: - **`RuntimeHintsRegistrar`** — implement `registerHints(RuntimeHints, ClassLoader)` and attach with **`@ImportRuntimeHints(MyHints.class)`** on a `@Configuration` class. - **`@RegisterReflectionForBinding({Foo.class, Bar.class})`** — register reflection needed to (de)serialize types (Jackson, etc.). - **`@Reflective` / `@ReflectiveScan`** — mark methods/types needing reflective access. - Programmatic hint categories: `hints.reflection()`, `hints.resources()`, `hints.proxies()`, `hints.serialization()`. ## When to reach for which - **Missing reflection/resource only** → `RuntimeHintsRegistrar` / annotations. Simplest, covers most app-level native gaps. - **Bean whose instantiation the engine can't infer** (dynamic factory, proxy, programmatic registration) → `BeanRegistrationAotProcessor` to emit a correct supplier. - **Cross-cutting, factory-level setup** (register many things, framework integration) → `BeanFactoryInitializationAotProcessor`. ## Testing the contribution Spring provides testing support: `ApplicationContextAotGenerator` + `TestGenerationContext`, and `RuntimeHintsPredicates` to assert a hint was registered, e.g.: ``` assertThat(RuntimeHintsPredicates.reflection() .onType(MyDto.class) .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) .accepts(generationContext.getRuntimeHints()); ``` ## Gotchas - Contributions run **at build time**; they must be deterministic and side-effect-free. - Registering *too broad* reflection (e.g. whole packages) bloats the native image and undermines the closed-world benefits — be surgical. - The generated code SPI (JavaPoet fork, `GeneratedMethods`) is lower-level and version-sensitive; prefer hints unless you truly need custom instantiation. - `aot.factories` vs `spring.factories`: AOT processors that must run even when their triggering beans aren't present go in `META-INF/spring/aot.factories`.
- When is a RuntimeHintsRegistrar enough versus needing a BeanRegistrationAotProcessor?Use a RuntimeHintsRegistrar (with @ImportRuntimeHints) when you only need to declare reflection/resource/proxy/serialization metadata. Reach for a BeanRegistrationAotProcessor when the engine can't generate correct instantiation code for a bean and you must emit a custom InstanceSupplier.
- How do you unit-test that your registrar added the right hints?Use RuntimeHintsPredicates (e.g. reflection().onType(...).withMemberCategory(...)) asserted against a RuntimeHints instance you populated, avoiding a full native build. Spring's TestGenerationContext helps drive the generator in a test.
- Where do you register an AOT processor that must run even if no triggering bean exists?In META-INF/spring/aot.factories, the AOT-specific factories file, so the processor is picked up during process-aot independent of bean presence.
saying these in an interview costs you the question
- Registering package-wide reflection hints broadly, defeating native image's closed-world size/security benefits.
- Putting AOT processors in spring.factories instead of aot.factories when they must run without a triggering bean.
- Making AOT contributions with runtime side effects (they run at build time and must be deterministic).
- Assuming you always need code generation when a simple RuntimeHintsRegistrar would suffice.