Skip to content
Spring Boot AOT Processing and Runtime Reflection Registration for GraalVM Native Images

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.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop

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 JSON

Generated 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
__BeanDefinitions

contains 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

MemberCategory

controls 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 methods

Data 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 {
}
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GraalVMNative ImageAOTRuntimeHintsSpring Boot 3Cloud Native JavaReflection Registration
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.