Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/spring-projects/spring-boot/llms.txt

Use this file to discover all available pages before exploring further.

Ahead-of-Time (AOT) processing is a build-time phase in which Spring analyses your application’s bean definitions, generates optimized Java source code, and produces GraalVM hint files — all before the application ever runs. On the JVM this reduces startup time by replacing runtime reflection with pre-generated initialization code. For GraalVM native images, AOT processing is a prerequisite: without it, the compiler cannot statically understand your Spring context.

AOT vs. JIT: what changes at build time

In a standard JVM application, Spring parses @Configuration classes, discovers @Bean methods, and builds the application context at startup using reflection and dynamic proxies. The JIT compiler then optimizes hot code paths over time. AOT processing shifts the heavy lifting earlier:
PhaseJIT (standard JVM)AOT
Bean definition discoveryRuntimeBuild time
@Configuration parsingRuntime (reflection)Build time (source generation)
Proxy class generationRuntime (cglib)Build time (bytecode)
GraalVM hint filesNot requiredGenerated at build time
Startup speedBaselineFaster (no reflection overhead)
AOT cache and Spring’s AOT processing can be combined to further improve startup time on the JVM.

What Spring AOT generates

When the AOT processing phase runs, Spring produces three categories of output:

Java source code

Bean definitions are rewritten as direct, reflection-free Java code. Generated sources land in target/spring-aot/main/sources (Maven) or build/generated/aotSources (Gradle).

Bytecode

Dynamic proxy classes generated by cglib are pre-compiled to .class files. Find them in target/spring-aot/main/classes or build/generated/aotClasses.

GraalVM hint files

JSON hint files describing reflection, resources, serialization, proxies, and JNI are written to META-INF/native-image/{groupId}/{artifactId}/. They are also available in target/spring-aot/main/resources or build/generated/aotResources.

Running AOT processing during a build

Activate the native profile to include AOT generated code in your JAR:
mvn -Pnative package
This triggers the spring-boot:process-aot goal automatically as part of the build lifecycle.

Running a JAR with AOT initialization on the JVM

Once you have built a JAR that includes AOT generated code, you can instruct the JVM to use the pre-generated initialization path instead of the standard reflection-based startup:
java -Dspring.aot.enabled=true -jar myapplication.jar

........ Starting AOT-processed MyApplication ...
Using AOT initialization on the JVM implies the following restrictions:
  • The classpath is fixed and fully defined at build time.
  • The beans defined in your application cannot change at runtime.
  • @Profile annotations and profile-specific configuration have limitations.
  • Properties that change if a bean is created are not supported (for example, @ConditionalOnProperty and .enabled properties).

Registering custom runtime hints

Spring AOT generates hints automatically for the vast majority of Spring-managed code. When you use reflection, resources, serialization, or dynamic proxies outside of what Spring can detect statically — for example, in a third-party library or custom infrastructure code — you register additional hints using RuntimeHintsRegistrar.

Implementing RuntimeHintsRegistrar

Create a class implementing RuntimeHintsRegistrar and make the appropriate calls on the provided RuntimeHints instance:
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.util.ReflectionUtils;

public class MyRuntimeHints implements RuntimeHintsRegistrar {

    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        // Register a resource pattern
        hints.resources().registerPattern("my-resource.txt");

        // Register a method for reflection
        hints.reflection().registerMethod(
            ReflectionUtils.findMethod(MyClass.class, "someMethod"),
            ExecutableMode.INVOKE
        );
    }

}

Activating the registrar with @ImportRuntimeHints

Apply @ImportRuntimeHints on any @Configuration class — including your @SpringBootApplication class — to activate the registrar:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportRuntimeHints;

@SpringBootApplication
@ImportRuntimeHints(MyRuntimeHints.class)
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

}

Using @RegisterReflectionForBinding

For classes that need reflection binding — most commonly when serializing or deserializing JSON — annotate a bean with @RegisterReflectionForBinding:
@Configuration
@RegisterReflectionForBinding(MyDto.class)
public class MyConfiguration {
    // ...
}
Most reflection hints are inferred automatically when a type is accepted or returned by a @RestController method. Use @RegisterReflectionForBinding explicitly when working directly with WebClient, RestClient, or RestTemplate.

Testing custom hints

Use RuntimeHintsPredicates to write unit tests that verify your RuntimeHints instance includes the entries you registered. With AssertJ:
@Test
void shouldRegisterHints() {
    RuntimeHints hints = new RuntimeHints();
    new MyRuntimeHints().registerHints(hints, getClass().getClassLoader());
    assertThat(RuntimeHintsPredicates.resource().forResource("my-resource.txt"))
        .accepts(hints);
}

AOT cache for faster JVM startup

The AOT cache is a JVM feature (available in Java 24+) that records the output of the JVM’s own AOT compiler during a training run and replays it on subsequent starts. Combined with Spring’s AOT processing, it can reduce startup time substantially without requiring a full native image compilation.
If you are using Java older than 24, use CDS (Class Data Sharing) instead of the AOT cache. CDS is the predecessor and works similarly.
1

Extract the application JAR

The AOT cache must be used with the extracted form of the JAR, not the uber JAR:
java -Djarmode=tools -jar my-app.jar extract --destination application
cd application
2

Run the training pass

java -XX:AOTCacheOutput=app.aot -Dspring.context.exit=onRefresh -jar my-app.jar
This creates app.aot, which can be reused as long as the application and Java version remain unchanged.
3

Start with the cache enabled

java -XX:AOTCache=app.aot -jar my-app.jar
The AOT cache file must be used with the extracted form of the application. Passing it to the uber JAR directly has no effect.

AOT cache in container images

To bake the AOT cache into a Docker image, add a training run step to your Dockerfile after copying the extracted layers:
# Execute the AOT cache training run
RUN java -XX:AOTCacheOutput=app.aot -Dspring.context.exit=onRefresh -jar application.jar
# Start the application jar with AOT cache enabled
ENTRYPOINT ["java", "-XX:AOTCache=app.aot", "-jar", "application.jar"]
For containers running Java below 24, use the CDS equivalent:
# Execute the CDS training run
RUN java -XX:ArchiveClassesAtExit=application.jsa -Dspring.context.exit=onRefresh -jar application.jar
# Start the application jar with CDS enabled
ENTRYPOINT ["java", "-XX:SharedArchiveFile=application.jsa", "-jar", "application.jar"]
See the container images guide for complete multi-stage Dockerfile examples.

Build docs developers (and LLMs) love