Spring Boot AOT Processing and Runtime Reflection Registration for GraalVM Native Images
This article explains how Spring Boot 3's Ahead-of-Time (AOT) processing resolves reflection registration issues when compiling Spring applications to GraalVM native images, covering build-time artifact generation, RuntimeHints API, and the @RegisterReflectionForBinding annotation.
Why AOT Is Needed
Traditional Spring startup relies heavily on runtime reflection: scanning classpaths for annotations, invoking constructors, injecting fields, and calling methods via reflection. On a standard JVM this works freely because classes can be loaded and inspected at runtime.
GraalVM Native Image performs static analysis at compile time. It only retains classes and methods deemed reachable, and by default blocks reflective access unless explicitly registered via reachability metadata. This creates a fundamental conflict with Spring's dynamic assembly model.
What AOT Does at Build Time
Processing Entry Points
Spring Framework 6 provides core AOT processing in org.springframework.context.aot package, centered on ContextAotProcessor which prepares an ApplicationContext, collects BeanDefinitions, generates Bean definition source code ( __BeanDefinitions), registration classes ( __Registrations), and outputs reflection/resource/proxy configuration. Spring Boot's entry point is ApplicationAotProcessor, invoked during spring-boot:process-aot or native:compile.
Complete Processing Flow
① Start a "production-mode" ApplicationContext at build time
↓
② Resolve auto-configuration, @ComponentScan, @Bean definitions
↓
③ BeanFactoryAotProcessor converts Bean definitions to Java source
↓
④ Pre-generate proxy classes for Beans requiring AOP
↓
⑤ Collect RuntimeHints: reflection types, resource patterns, JDK proxies, JNI
↓
⑥ Output to build directory and generate GraalVM configuration JSONGenerated Artifacts
In a Maven project, artifacts land under target/spring-aot/main/:
target/spring-aot/main/
├── sources/ # Generated Java source
│ └── com/demo/
│ ├── __BeanDefinitions.java
│ └── __Registrations.java
├── classes/ # Compiled AOT classes
└── resources/META-INF/native-image/
├── reflect-config.json # Reflection registration
├── resource-config.json # Resource registration
├── proxy-config.json # JDK dynamic proxy registration
└── jni-config.json # JNI registration__BeanDefinitionscontains explicit registerBean calls for beans originally discovered by scanning; __Registrations registers listeners, serializers, and other runtime components.
Two Usage Modes
Pure Native : AOT artifacts participate directly in GraalVM compilation to produce a native executable.
JVM AOT Verification : Set spring.aot.enabled=true to load AOT-generated bean definitions on a regular JVM, exposing wiring issues without GraalVM.
Why Runtime Reflection Must Be Registered
Registration Essence
GraalVM's static analyzer sees direct calls but cannot see class names inside strings:
// Analyzer sees MyDto used directly
OrderDTO dto = new OrderDTO();
// Analyzer cannot see the class name in the string, must rely on registration
Class<?> clazz = Class.forName("com.demo.dto.OrderDTO");The registration file reflect-config.json explicitly lists these invisible entry points:
[
{
"name": "com.demo.dto.OrderDTO",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredFields": true,
"allPublicFields": true,
"allDeclaredMethods": true,
"allPublicMethods": true
}
]Registration Granularity
Registration can target specific members rather than whole classes. Categories:
Class itself : Allows Class.forName to obtain the Class object
Constructors : Allows reflective instance creation
Fields : Allows reflective field read/write
Methods : Allows reflective method invocation
JNI : Allows native code access
Consequences of Missing Registration
Accessing unregistered reflective elements at native runtime throws MissingReflectionRegistrationError:
com.oracle.svm.core.reflect.MissingReflectionRegistrationError:
Invoking constructor com.demo.dto.OrderDTO.<init>() is not allowed
because the class has not been registered for reflection.This is why "works on JVM, crashes on native" almost always points to missing reflection, proxy, or resource registration.
Spring's Registration System: RuntimeHints and RuntimeHintsRegistrar
Manually writing JSON is error-prone. Spring Framework 6 provides a developer-facing API — RuntimeHints — collected during AOT and transformed into GraalVM configuration.
Five Dimensions of RuntimeHints
public interface RuntimeHints {
ReflectionHints reflection(); // Reflection: classes, constructors, methods, fields
ResourceHints resources(); // Classpath resource files
ProxyHints proxies(); // JDK dynamic proxy interfaces
JniHints jni(); // JNI access
SerializationHints serialization(); // Java serialization
}Custom Registrar
Implement RuntimeHintsRegistrar and declare needed elements in registerHints:
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.MemberCategory;
public class OrderRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// ① Register DTO constructors, fields, methods for Jackson/reflection binding
hints.reflection()
.registerType(OrderDTO.class,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.DECLARED_FIELDS,
MemberCategory.INVOKE_DECLARED_METHODS);
// ② Register classpath resources (configs, templates, dictionary files)
hints.resources()
.registerPattern("dto-mappings/*.json")
.registerPattern("META-INF/services/*");
// ③ Register interfaces needing JDK proxy generation
hints.proxies()
.registerJdkProxy(OrderRemoteService.class, FallbackService.class);
}
}MemberCategory Selection
MemberCategorycontrols which members are opened:
PUBLIC_FIELDS - All public fields
DECLARED_FIELDS - All declared fields (including private)
PUBLIC_METHODS - All public methods (accessible only, not invocable)
DECLARED_METHODS - All declared methods
PUBLIC_CONSTRUCTORS - All public constructors
DECLARED_CONSTRUCTORS - All declared constructors
INVOKE_PUBLIC_METHODS - Allow reflective invocation of public methods
INVOKE_DECLARED_METHODS - Allow reflective invocation of declared methodsData binding scenarios typically need: accessible constructors + accessible fields + invocable getters/setters.
Importing the Registrar
Use @ImportRuntimeHints on a configuration class:
import org.springframework.context.annotation.Configuration;
import org.springframework.aot.hint.annotation.ImportRuntimeHints;
@Configuration
@ImportRuntimeHints(OrderRuntimeHints.class)
public class HintsConfiguration {
}During AOT processing, this Registrar's declarations are collected into RuntimeHints and exported as reflect-config.json etc.
Conditional Registration
Registrars support conditional registration to avoid over-registration:
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)) {
hints.reflection().registerType(OrderDTO.class, MemberCategory.values());
}
}Automatic Registration: Framework-Provided Hints and Binding Annotation
What Spring Boot Already Covers
Spring Boot automatically generates registration for:
Framework-internal reflection types
Hints bundled with Spring Data, Jackson, Validation modules
Bean types involved in auto-configuration
Known required resource patterns
@RegisterReflectionForBinding: For Data Binding
For JSON serialization/deserialization and form binding, Spring provides the convenience annotation @RegisterReflectionForBinding:
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
@RegisterReflectionForBinding({
OrderDTO.class,
OrderItemDTO.class,
PageResult.class
})
@RestController
@RequestMapping("/order")
public class OrderController {
// ...
}It registers reflection members required for data binding (constructors, fields, getters/setters) for the annotated types and their nested types, equivalent to configuring MemberCategory for binding scenarios in a Registrar. Suitable for API DTOs, nested objects, and generic collection element types.
Third-Party Library Hints
Mature libraries often ship META-INF/native-image configuration or Spring Hints modules. If a library lacks native support and missing registration surfaces only at runtime, you must supply RuntimeHints yourself.
Complete Example Project Configuration
Dependencies (Spring Boot 3.x):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</dependency>Handling custom DTO reflection:
// 1. DTO
public class OrderDTO {
private Long id;
private String title;
private List<OrderItemDTO> items;
// getter / setter
}
// 2. Registrar
public class OrderDtoHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader cl) {
hints.reflection().registerTypes(
List.of(OrderDTO.class, OrderItemDTO.class),
type -> type.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.DECLARED_FIELDS,
MemberCategory.INVOKE_DECLARED_METHODS));
}
}
// 3. Import
@Configuration
@ImportRuntimeHints(OrderDtoHints.class)
public class AppConfig {
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
