AOT processing and GraalVM native images
What changes when the context is computed at build time instead of at startup.
Open this lesson in the learning hubKey points
- A native image runs under a closed-world assumption: every class, method and resource reachable at run time must be known when the image is built. Nothing can be loaded later.
- Spring AOT processing runs during the build. It evaluates conditions, resolves bean definitions and generates Java source that recreates the context directly - no classpath scanning, no condition evaluation at startup.
- The consequence people miss: conditions are frozen at build time. A profile or property that would have changed which beans exist has already been decided, so a native image built with one profile cannot switch to another at run time.
- Reflection, resource loading and proxies must be declared. Spring generates hints for what it can see; anything reached by a string name in your own code needs a
RuntimeHintsRegistrar. - The payoff is a startup measured in tens of milliseconds and a much smaller memory footprint - which is what makes short-lived or scale-to-zero workloads viable.
- The cost is real: build times of minutes rather than seconds, no runtime JIT to optimise the hot path, and libraries that do dynamic things needing explicit support.
Example
// AOT also runs on the JVM, not only for native images:
// ./gradlew bootJar then java -Dspring.aot.enabled=true -jar app.jar
// which cuts startup work by skipping scanning and condition evaluation.
// Reflection Spring cannot see must be declared.
class PluginHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// We load this by name at run time, so nothing in the bytecode references it.
hints.reflection().registerType(TypeReference.of("com.example.plugin.CsvExporter"),
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS,
MemberCategory.INVOKE_PUBLIC_METHODS);
hints.resources().registerPattern("plugins/*.properties");
hints.serialization().registerType(TypeReference.of("com.example.PluginKey"));
}
}
@Configuration
@ImportRuntimeHints(PluginHints.class)
class PluginConfig {
@Bean
Exporter exporter(@Value("${export.impl}") String className) throws Exception {
// Without the hint above, this line throws ClassNotFoundException in a
// native image while working perfectly on the JVM.
return (Exporter) Class.forName(className).getDeclaredConstructor().newInstance();
}
}
AOT trades run-time flexibility for startup speed - conditions are decided at build time, so anything resolved by name needs a hint.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Spring Boot course, and every lesson in it is listed on the Spring Boot contents page.