Spring Boot Native lets you compile your Spring Boot applications into a native executable using GraalVM, ditching the JVM entirely for faster startup and lower memory usage.
Here’s a simple Spring Boot web app:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/")
public String hello() {
return "Hello, Native Spring Boot!";
}
}
To make this native, you’ll add the spring-native Maven plugin to your pom.xml:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder-jammy-base:latest</builder>
</image>
<classifier>native</classifier>
<jvmArguments>-Dspring.native.remove-unused-autoconfiguration=true</jvmArguments>
</configuration>
<executions>
<execution>
<id>build-native</id>
<phase>package</phase>
<goals>
<goal>build-image</goal>
</goals>
</execution>
</executions>
</plugin>
And you’ll need to add the spring-native-maven-plugin dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.1.5</version> <!-- Use the latest compatible version -->
<type>pom</type>
</dependency>
Now, when you run mvn -Pnative clean package, the spring-boot-maven-plugin with the build-image goal kicks off the native compilation process. It uses GraalVM’s native-image tool behind the scenes. This tool analyzes your application’s code at build time, determines all the necessary classes, methods, and resources, and then compiles them directly into a standalone executable. It doesn’t involve a Just-In-Time (JIT) compiler or a traditional JVM at runtime.
The core problem Spring Boot Native solves is the startup time and memory footprint associated with the JVM. For microservices, especially those that scale up and down rapidly (like in serverless environments), the JVM’s warm-up time and resource consumption can be a significant bottleneck. A native executable starts almost instantaneously and uses a fraction of the memory.
Here’s how it works internally: GraalVM’s native-image tool performs static analysis. It needs to know everything about your application ahead of time. This includes all the classes that will be instantiated, all the methods that will be called, and all the reflection or dynamic proxies that will be used. Spring Native provides the necessary configuration and hints to native-image so it can accurately analyze a Spring Boot application, which relies heavily on reflection and proxies for its auto-configuration and dependency injection.
The spring-native-maven-plugin orchestrates this. It integrates with GraalVM’s build process. When you run mvn -Pnative package, it triggers the build-image goal. This goal does two main things:
- Configuration: It applies Spring Boot’s native-image configuration, which includes hints for GraalVM about how Spring components are initialized and accessed.
- Compilation: It invokes the
native-imagetool from GraalVM to produce the native executable.
The result is a single, self-contained binary. For our DemoApplication, running java -jar target/demo-0.0.1-SNAPSHOT-native.jar would be replaced by ./target/demo-0.0.1-SNAPSHOT-native.
The jvmArguments -Dspring.native.remove-unused-autoconfiguration=true is a critical optimization. Spring Boot’s auto-configuration can discover and enable many features. In a native image, all this code must be included. This flag tells Spring to perform an aggressive analysis and exclude any auto-configuration classes that are not actually used by your application. This significantly reduces the size of the final native executable and can also improve startup performance by reducing the amount of code that needs to be processed.
When you build a native executable, the native-image tool needs to know about all the classes that might be instantiated, even if they aren’t directly referenced in your code. This is because frameworks like Spring use reflection to discover and instantiate beans. The native-image tool performs "reachability analysis" to determine which code is reachable from the application’s entry point. If a class is only ever instantiated via reflection, and the native-image tool doesn’t have a hint that this reflection will occur, it might exclude that class, leading to a ClassNotFoundException or similar errors at runtime. Spring Native provides these hints automatically for most common Spring Boot features.
The builder: paketobuildpacks/builder-jammy-base:latest configuration is for creating container images. When you use build-image, Spring Boot uses Cloud Native Buildpacks to create a container image containing your native executable. This is a convenient way to package your application for deployment. The builder you specify is a pre-configured set of buildpack layers that knows how to assemble your application into a runnable image, including setting up the necessary environment for the native executable.
You’ll next encounter issues with reflection and proxy configurations if you use less common libraries or advanced Spring features that native-image can’t automatically detect.