skip to content

Walk through what a BeanRegistrationAotContribution's applyTo does with the GenerationContext — how does a processor emit generated code and hints?

level: seniorimportance: should knowfreq 38%

answer

  1. Contribution = deferred applyTo unit
  2. GenerationContext = hints + generatedClasses + generatedFiles
  3. BeanRegistrationCode = customise instantiation
  4. Hints-only → use RuntimeHintsRegistrar instead
  5. Build-time, deterministic, no runtime re-run

basics

~20 s

processAheadOfTime returns a contribution; its applyTo(GenerationContext, BeanRegistrationCode) is called to do the work. Through the GenerationContext it registers RuntimeHints (getRuntimeHints()) and emits generated Java classes (getGeneratedClasses()); the BeanRegistrationCode lets it customise the bean's instantiation code.

solid answer

~40 s

The processor's processAheadOfTime(RegisteredBean) returns a BeanRegistrationAotContribution — a functional interface whose applyTo(GenerationContext, BeanRegistrationCode) Spring later calls. The GenerationContext is the shared sink: getRuntimeHints() registers reflection/resource/proxy hints the native compiler needs, and getGeneratedClasses()/getGeneratedFiles() let you emit new generated Java source (built with the JavaPoet-based ClassGenerator). The BeanRegistrationCode (for the factory-wide variant, BeanFactoryInitializationCode) gives access to the code being generated for that bean so you can contribute custom instantiation via setCustomInstanceSupplier-style code fragments. A common lightweight pattern is to return a lambda that only calls generationContext.getRuntimeHints().reflection().registerType(...). Everything runs at build time and must be deterministic, because the emitted code and hints are frozen into the native image.

code

java · 26 lines
java
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.lang.Nullable;

public class ReflectiveMapperAotProcessor implements BeanRegistrationAotProcessor {

    @Nullable
    @Override
    public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
        Class<?> beanClass = registeredBean.getBeanClass();
        if (!ReflectiveMapper.class.isAssignableFrom(beanClass)) {
            return null;
        }
        // The returned contribution's applyTo runs during the generation pass.
        return (generationContext, beanRegistrationCode) -> {
            generationContext.getRuntimeHints()
                .reflection()
                .registerType(beanClass,
                    MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
                    MemberCategory.INVOKE_DECLARED_METHODS);
            // Could also emit a generated class via generationContext.getGeneratedClasses().
        };
    }
}

go deeper

for a junior

Know the contribution has an applyTo that receives a GenerationContext used to add hints/code.

for a middle

Know GenerationContext exposes getRuntimeHints and getGeneratedClasses and that work happens in applyTo, not processAheadOfTime.

for a senior

Can explain the two-phase collect/generate flow, BeanRegistrationCode for instantiation, and the determinism/build-time constraints.

for a principal

Can design generated-code strategies (custom instance suppliers, JavaPoet classes) and reason about hint completeness to avoid native-only failures.

## The contribution is the deferred work unit `processAheadOfTime(...)` is called during a first pass while Spring walks beans; it should be cheap and just **decide** whether there is work. The actual code/metadata emission happens in the returned contribution's `applyTo`, which Spring invokes during the generation pass: ```java @FunctionalInterface public interface BeanRegistrationAotContribution { void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode); } ``` (The factory-wide sibling is `BeanFactoryInitializationAotContribution.applyTo(GenerationContext, BeanFactoryInitializationCode)`.) ## The GenerationContext — the shared output sink `GenerationContext` is passed to every contribution and centralises three outputs: 1. **`getRuntimeHints()`** → a `RuntimeHints` instance. You call `.reflection()`, `.resources()`, `.proxies()`, `.serialization()` etc. to declare what the native image must retain. (The hint *categories* are a sibling topic; here the point is simply that the contribution is where they get registered.) 2. **`getGeneratedClasses()`** → a `GeneratedClasses` you use to add generated Java classes (Spring builds them with a JavaPoet-based `ClassGenerator`, producing e.g. `MyBean__BeanDefinitions`). 3. **`getGeneratedFiles()`** → lower-level access to write arbitrary generated files (source, resource, class). Because every contribution shares the same `GenerationContext`, hints and generated classes from all modules are merged into one coherent output tree under `build/generated`. ## The BeanRegistrationCode — customising instantiation The second parameter, `BeanRegistrationCode`, models the code being generated to register/instantiate this particular bean. Advanced processors use `BeanRegistrationAotContribution.withCustomCodeFragments(...)` or the code object to inject a custom instance supplier — e.g. Spring Data emits code that returns a repository proxy instead of the raw interface. Most application-level processors never touch this and only register hints. ## Minimal vs advanced - **Minimal** (very common): return a lambda that only registers hints. If that is *all* you need, prefer a `RuntimeHintsRegistrar` instead — it is the purpose-built lighter SPI. - **Advanced**: emit generated classes and rewrite the bean's instantiation code (framework-level). ## Determinism and build-time execution — the big gotcha `applyTo` runs at **build time**. The generated code and hints are baked into the artifact; there is no second chance at runtime. Therefore: - Do not read runtime-only state (current time, env that differs at runtime, network). - Output must be reproducible across builds (important for caching and native-image reproducibility). - Any reflection the *generated* code will perform at runtime must have a matching hint registered here, or it fails only in the native image (not on the JVM), which is a classic late-surfacing bug. ## Order of operations recap 1. AOT engine refreshes context, collects bean definitions. 2. For each bean, every `BeanRegistrationAotProcessor.processAheadOfTime` runs → returns contribution or null. 3. Contributions' `applyTo` run, writing into the shared `GenerationContext`. 4. Spring writes generated sources + a consolidated `reflect-config.json`/`resource-config.json` (from the RuntimeHints) under `build/generated`. 5. GraalVM `native-image` consumes them.

  • If your contribution only registers RuntimeHints and never emits code, what should you use instead?
    A RuntimeHintsRegistrar (registered via @ImportRuntimeHints or aot.factories) — it is the lighter SPI meant purely for hints, avoiding the full processor/contribution ceremony.
  • Why can a bug from a missing hint pass on the JVM but fail only in the native image?
    On the JVM reflection works regardless of hints; the generated code runs fine. In a native image, only reflection declared via registered hints is retained, so an un-hinted reflective call throws at runtime — surfacing the defect only after native compilation.

saying these in an interview costs you the question

  • Thinking applyTo runs at runtime for each request
  • Registering hints from processAheadOfTime directly instead of inside the returned contribution
  • Believing GenerationContext only carries hints and cannot emit code
  • Doing non-deterministic work (time, network) in applyTo

context