In the ever-evolving landscape of cloud-native development and serverless architectures, the demand for applications with lightning-fast startup times and minimal memory footprints is greater than ever. Java, traditionally known for its “write once, run anywhere” philosophy, has sometimes faced criticism regarding these very aspects. However, with the advent of GraalVM and Spring Boot’s dedicated support, Java is now a formidable contender in this space.
This post will guide you through the process of building GraalVM native images of your Spring Boot native applications (specifically Spring Boot 3.x), demonstrating how to unlock significant optimizations in startup time and memory consumption, perfect for serverless Java and general cloud native Java deployments.
Why Native Images? The GraalVM Advantage
Traditional Java applications run on the Java Virtual Machine (JVM). While the JVM offers incredible runtime optimizations through its Just-In-Time (JIT) compiler, there’s an inherent overhead: the JVM itself needs to start, classes need to be loaded, and code needs to be JIT-compiled at runtime.
GraalVM Native Image technology compiles your Java application ahead-of-time (AOT) into a standalone executable. This executable includes the application code, required libraries, and a minimal runtime environment (the Substrate VM) – all compiled into a single binary.
The benefits are substantial:
- Blazing Fast Startup: Native images typically start in milliseconds, making them ideal for serverless functions, microservices, and environments where rapid scaling is crucial.
- Reduced Memory Footprint: By eliminating the JVM overhead and only including the necessary code paths, native images use significantly less memory.
- Smaller Deployment Size: The resulting binary is often much smaller than a traditional JAR file bundled with a JRE.
- Lower Resource Consumption: Less CPU and memory usage translates to lower operational costs in cloud environments.
Spring Boot 3.x has embraced GraalVM native image compilation with first-class support, making the process more seamless than ever.
Step 1: Prerequisites
Before we dive in, ensure you have the following installed:
- Java Development Kit (JDK): A recent version (JDK 17 or higher is recommended for Spring Boot 3).
- GraalVM: You’ll need GraalVM itself, specifically the
native-imagetool. The easiest way to get started is with SDKMAN! or by downloading directly from Oracle.- Using SDKMAN!:
sdk install java 22.3.r17-grl # or the latest GraalVM version
sdk use java 22.3.r17-grl
gu install native-image
- Manual Installation: Download GraalVM Community Edition, extract it, set your
JAVA_HOME, and then rungu install native-image. - Maven or Gradle: Your preferred build tool.
Step 2: Create a Basic Spring Boot 3.x Application
Let’s start with a simple Spring Boot web application. You can use Spring Initializr (start.spring.io) for this.
- Project: Maven Project or Gradle Project
- Language: Java
- Spring Boot: 3.x (e.g., 3.2.5)
- Dependencies: Spring Web
Generate the project and open it in your IDE.
Here’s a basic DemoApplication.java and a simple REST controller:
src/main/java/com/example/demo/DemoApplication.java
package com.example.demo;
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);
}
}
src/main/java/com/example/demo/HelloController.java
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello from Spring Boot Native Image!";
}
}
Step 3: Configure for Native Image Building
Spring Boot 3.x leverages the spring-boot-starter-parent and its Maven/Gradle plugins to simplify native image compilation.
For Maven:
Add the spring-boot-maven-plugin configuration within your pom.xml. If you generated the project with Spring Initializr, this should largely be in place. Ensure you have the native profile activated.
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder-jammy-tiny:latest</builder>
</image>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native</id>
<dependencies>
<dependency>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native-buildtools.version}</version>
<extensions>true</extensions>
<type>pom</type>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-native</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<mainClass>${start-class}</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
Important: You might need to add <native-buildtools.version> to your <properties> section. Check the latest version on Maven Central for native-maven-plugin (e.g., 0.10.2).
For Gradle:
In your build.gradle file, ensure you have the org.graalvm.buildtools.native plugin applied.
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.5' // Use your Spring Boot version
id 'io.spring.dependency-management' version '1.1.4'
id 'org.graalvm.buildtools.native' version '0.10.2' // Check for the latest version
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17' // Or your desired Java version
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
// Optional: For building a Docker image with Cloud Native Buildpacks
// bootBuildImage {
// builder = "paketobuildpacks/builder-jammy-tiny:latest"
// }
Step 4: Build the Native Image
Now comes the exciting part!
With Maven:
./mvnw clean package -Pnative
This command will compile your application, run the AOT processing, and then use GraalVM to compile the native executable. This process can take a few minutes, as it’s performing a full static analysis of your application and its dependencies.
With Gradle:
./gradlew nativeCompile
Similar to Maven, Gradle will orchestrate the native compilation process.
Step 5: Run and Observe!
Once the build is complete, you’ll find the executable in your target/ directory (Maven) or build/native/nativeCompile/ directory (Gradle). The executable will typically have the name of your project (e.g., demo).
Execute it:
./target/demo # For Maven
# or
./build/native/nativeCompile/demo # For Gradle
You’ll immediately notice the difference! The application should start up almost instantaneously.
Now, open your browser or use curl:
curl http://localhost:8080/hello
You should see: Hello from Spring Boot Native Image!
Optimizing Startup and Memory
While Spring Boot 3.x and GraalVM do a lot of the heavy lifting, here are some general tips for further optimizing your native images:
- Reduce Dependencies: Every dependency adds to the complexity and size of your native image. Be judicious about what you include. If a feature isn’t used, consider removing its dependency.
- Avoid Reflection (where possible): GraalVM’s native image compilation performs a closed-world analysis. It tries to identify all reachable code paths. Reflection makes this difficult, requiring explicit configuration. Spring Boot handles most common reflection scenarios for you, but excessive custom reflection can lead to larger images or runtime issues if not configured.
- Favor AOT-Friendly Libraries: As the ecosystem matures, more libraries are becoming “GraalVM-aware” and providing native hints. Check if your frequently used libraries have dedicated native support.
- Profile for Reachability Metadata: For complex applications or libraries that heavily rely on reflection, dynamic proxies, or resource loading, you might need to provide “reachability metadata” (configuration files that tell GraalVM what to include). Spring Boot’s AOT plugin usually generates much of this, but for specific edge cases, you might need to add custom
reflect-config.json,resource-config.json, etc. - Use Tiny Base Images for Docker: If you’re containerizing your native image, use extremely lightweight base images like
scratchordistrolessfor the smallest possible container size. Cloud Native Buildpacks (which Spring Boot uses withbootBuildImage) are excellent for this. - Monitor with GraalVM Insight: For advanced debugging and understanding why certain components are included, GraalVM Insight can be a powerful tool.
Conclusion
Building native images of Spring Boot applications with GraalVM is a game-changer for serverless Java and cloud native Java development. With Spring Boot 3.x’s robust support, the process is streamlined, allowing developers to easily leverage the benefits of ultra-fast startup times and significantly reduced memory consumption.
By following this step-by-step guide, you can transform your Spring Boot applications into lean, mean, cloud-ready machines. Experiment with your existing projects, observe the improvements, and embrace the future of high-performance Java!