AOT processing and GraalVM native images

Spring Boot · lesson 32 of 39 · 6 min read

What changes when the context is computed at build time instead of at startup.

Open this lesson in the learning hub

Key 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.