skip to content

When would you reach for a BeanFactoryInitializationAotProcessor versus a RuntimeHintsRegistrar, and how do they differ?

level: seniorimportance: should knowfreq 33%

answer

  1. Registrar = hints only, no factory, no codegen
  2. @ImportRuntimeHints registers a registrar
  3. Processor sees ConfigurableListableBeanFactory + can codegen
  4. Lowest-power tool that works
  5. Both build-time only

basics

~20 s

RuntimeHintsRegistrar only registers RuntimeHints — it can't generate code and needs no view of the bean factory. Use an AOT processor when you must also emit generated code or make decisions based on the actual bean definitions in the factory.

solid answer

~40 s

RuntimeHintsRegistrar is the minimal SPI: its single registerHints(RuntimeHints, ClassLoader) declares reflection/resource/proxy hints and is registered via @ImportRuntimeHints on a config class or via aot.factories. It has no access to the bean factory and cannot generate code. BeanFactoryInitializationAotProcessor is heavier: it receives the whole ConfigurableListableBeanFactory, can inspect every bean definition, and returns a contribution that both registers hints and emits generated Java code through the GenerationContext. So the decision is: pure static hints known at author time → RuntimeHintsRegistrar; hints derived by scanning the live factory, or any code generation → the AOT processor. Frameworks often expose both — a simple hints registrar for consumers, and internal AOT processors for the code-generation heavy lifting.

go deeper

for a junior

Know RuntimeHintsRegistrar is the simple hints-only option and processors are the more powerful one.

for a middle

Know the registrar's signature, @ImportRuntimeHints registration, and that processors can generate code and see the factory.

for a senior

Can choose correctly based on static-vs-dynamic hints and code-gen needs, and cite the framework 'ship both' pattern.

for a principal

Applies the lowest-power principle across a library's public API surface and weighs build-time cost of extra processors.

## Two tools, different weight classes Spring's AOT extensibility spans a spectrum. At the light end is `RuntimeHintsRegistrar`; at the heavy end are the AOT processors. Choosing wrongly means either boilerplate you didn't need or a ceiling you hit later. ### RuntimeHintsRegistrar — hints only ```java public interface RuntimeHintsRegistrar { void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader); } ``` - **Input:** just a `RuntimeHints` and the `ClassLoader`. **No** view of the bean factory, **no** `GenerationContext`, **cannot** emit generated code. - **Registration:** annotate a `@Configuration` (or any bean) with `@ImportRuntimeHints(MyHints.class)`, or list it in `META-INF/spring/aot.factories` under `RuntimeHintsRegistrar`. - **Use when:** the set of types/resources needing reflection is **known statically at authoring time** — e.g. you serialize a fixed set of DTOs via Jackson, or load a fixed resource bundle. ```java public class MyRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader cl) { hints.reflection().registerType(OrderDto.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS); hints.resources().registerPattern("config/*.json"); } } @Configuration @ImportRuntimeHints(MyRuntimeHints.class) class HintsConfig {} ``` ### BeanFactoryInitializationAotProcessor — inspect + generate - **Input:** the full `ConfigurableListableBeanFactory`. You can iterate `getBeanDefinitionNames()`, read merged definitions, discover annotations across beans, etc. - **Output:** a contribution with `applyTo(GenerationContext, BeanFactoryInitializationCode)` — registers hints **and** can emit generated classes. - **Use when:** the hints (or code) depend on **what beans actually exist**, which you can't know at authoring time — e.g. scan all beans for a marker annotation, or generate a dispatcher for every `@EventListener` found. ### Decision table | Need | Pick | |------|------| | Fixed, author-time-known hints, no code | `RuntimeHintsRegistrar` | | Hints derived from scanning the live factory | `BeanFactoryInitializationAotProcessor` | | Hints/code tied to one specific bean's instantiation | `BeanRegistrationAotProcessor` | | Generated Java source of any kind | an AOT processor (not the registrar) | ## Gotchas - A `RuntimeHintsRegistrar` **cannot** generate code — reaching for it and then discovering you need a generated class means a rewrite; if in doubt about future code-gen, the processor is safer. - Both register via `aot.factories`, but the registrar additionally supports the ergonomic `@ImportRuntimeHints` annotation, which the processors do not. - All of them run at **build time**; none can react to runtime state. - Prefer the **lowest-power** tool that solves the problem: unnecessary AOT processors add build-time cost and complexity. ## Framework pattern Libraries frequently ship *both*: internal `BeanRegistrationAotProcessor`s do the code generation, while a small public `RuntimeHintsRegistrar` (or `@ImportRuntimeHints`) is offered to application authors who just need to declare a few extra reflective types.

  • Can a RuntimeHintsRegistrar emit a generated Java class?
    No. It only receives a RuntimeHints and ClassLoader; there is no GenerationContext, so code generation requires an AOT processor instead.
  • You need to register reflection hints for every bean annotated with a custom @Reportable — which SPI and why?
    BeanFactoryInitializationAotProcessor: it receives the whole bean factory so you can scan all definitions for the annotation, something a static RuntimeHintsRegistrar cannot do.

saying these in an interview costs you the question

  • Claiming RuntimeHintsRegistrar can generate code
  • Thinking @ImportRuntimeHints works on an AOT processor
  • Reaching for a full AOT processor when a hints registrar suffices
  • Believing the registrar sees the bean factory

context