Backend Development 12 min read

How to Build Fast Spring Boot 3 Apps with GraalVM Native Images and AOT

This guide walks through the prerequisites, GraalVM installation, Maven setup, and step‑by‑step packaging of a Spring Boot 3 application into a native executable using AOT compilation, runtime hints, and Docker, demonstrating dramatically faster startup times.

macrozheng
macrozheng
macrozheng
How to Build Fast Spring Boot 3 Apps with GraalVM Native Images and AOT

Prerequisite Knowledge

Familiarize yourself with Spring Framework 6 new features ( link ) and Spring Boot 3 documentation ( link ).

Install GraalVM

Download the appropriate GraalVM CE build for your JDK version (Spring Boot 3 requires JDK 17+):

https://github.com/graalvm/graalvm-ce-builds/releases

. Installation is the same as a regular JDK. Verify with

java -version

to see the GraalVM VM.

GraalVM Limitations

When compiling to a native binary, GraalVM must know which classes, methods, and fields are used. Dynamically generated code requires configuration, e.g., via

reflect-config.json

, to declare reflective accesses.

Install Maven

Standard Maven installation, preferably using an Alibaba Cloud mirror for faster dependency resolution.

Background

Native compilation removes the JVM startup overhead by compiling Java bytecode directly to machine code, greatly improving startup speed. Spring Boot 3 AOT moves bean scanning to compile time, further reducing launch time.

Packaging Spring Boot 3

Project Preparation

<code>&lt;parent&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;
    &lt;version&gt;3.2.5&lt;/version&gt;
&lt;/parent&gt;

&lt;dependencies&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
        &lt;scope&gt;test&lt;/scope&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;

&lt;build&gt;
    &lt;plugins&gt;
        &lt;plugin&gt;
            &lt;groupId&gt;org.graalvm.buildtools&lt;/groupId&gt;
            &lt;artifactId&gt;native-maven-plugin&lt;/artifactId&gt;
        &lt;/plugin&gt;
        &lt;plugin&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
        &lt;/plugin&gt;
    &lt;/plugins&gt;
&lt;/build&gt;</code>
<code>import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Controller {
    @GetMapping("/demo1")
    public String demo1() {
        return "hello world";
    }
}</code>

Build Native Image

<code># Ensure GraalVM environment and a C compiler (Linux recommended)
mvn -Pnative native:compile</code>

The build produces both an executable binary and a regular JAR. The native binary starts in just a few dozen milliseconds, whereas the JAR takes noticeably longer.

Docker Image

<code># Build Docker image from native binary
mvn -Pnative spring-boot:build-image

# Run container
docker run --rm -p 8080:8080 demo

# Pass environment variable to the container
docker run --rm -p 8080:8080 -e methodName=test demo
</code>

Running the native container also starts within tens of milliseconds.

Understanding AOT and RuntimeHints

RuntimeHints Example

<code>@Component
@ImportRuntimeHints(UserService.MyServiceRuntimeHints.class)
public class UserService {
    public String test() {
        String result = "";
        try {
            Method m = MyService.class.getMethod("test");
            result = (String) m.invoke(MyService.class.newInstance());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return result;
    }

    static class MyServiceRuntimeHints implements RuntimeHintsRegistrar {
        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            try {
                hints.reflection().registerConstructor(MyService.class.getConstructor(), ExecutableMode.INVOKE);
                hints.reflection().registerMethod(MyService.class.getMethod("test"), ExecutableMode.INVOKE);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
</code>

Other Annotations

<code>@RegisterReflectionForBinding(MyService.class)
public String test() { /* ... */ }
</code>
<code>@Component
@ImportRuntimeHints(MyService.MyServiceRuntimeHints.class)
public class UserService {
    // method that reads method name from system property
    public String test() {
        String methodName = System.getProperty("methodName");
        // reflection logic ...
    }
}
</code>

JDK Proxy Configuration

<code>public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
    hints.proxies().registerJdkProxy(UserInterface.class);
}
</code>

@Reflective Example

<code>@Component
public class MyService {
    @Reflective
    public MyService() {}

    @Reflective
    public String test() { return "hello"; }
}
</code>

AOT Plugin Execution Logic

Running

mvn -Pnative native:compile

invokes the

native-maven-plugin

, which calls

ProcessAotMojo.executeAot()

. This method triggers

SpringApplicationAotProcessor

, which processes the application context, generates Java source files under

spring-aot/main/sources

, compiles them, and copies the resulting classes and GraalVM configuration files into

target/classes

.

Generated AOT Classes

The AOT process pre‑creates

BeanDefinition

classes, allowing Spring to skip runtime bean scanning. Diagrams illustrate the flow from

SpringApplication.run

to the generated classes.

Principle Diagram

DockerSpring BootGraalVMnative-imageAOTRuntimeHints
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

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