Native Image with Spring Boot

Philippe Agra
ekino-france
Published in
7 min readJan 24, 2024

--

Introduction

In the realm of Java development, Spring Boot stands out for its ability to simplify the creation of robust and scalable applications. However, traditional Java applications often grapple with longer startup times and higher memory usage. This is where the concept of native images comes into play, revolutionizing the performance aspects of Spring Boot applications.

Native images, facilitated by technologies like GraalVM, compile Java code ahead-of-time into a standalone executable, optimized for specific operating systems. This approach significantly reduces startup time and memory footprint compared to the traditional Java Virtual Machine (JVM) runtime. The result is a Spring Boot application that is not only faster and lighter but also more efficient, especially in resource-constrained environments like cloud platforms and microservices architectures.

This article aims to explore the integration of native images with Spring Boot, highlighting their benefits and explaining the process of converting a Spring Boot application into a native image.

A repository example illustrating this article can be found here : https://github.com/philippeagra/native-intro

GraalVM and ahead-of-time processing (AOP)

GraalVM is a state-of-the-art, versatile virtual machine designed to enhance the performance of applications written in various programming languages, including Java. What distinguishes GraalVM is its ability to create native images, transforming Java applications into pre-compiled standalone executables.

In traditional Java development, source code is first compiled into bytecode, an intermediate language that is machine-independent. This bytecode is then executed by the Java Virtual Machine (JVM), which employs Just-In-Time (JIT) compilation. JIT translates the bytecode into machine-specific code at runtime, optimizing performance as the application runs. However, Ahead-Of-Time (AOT) processing, as employed in tools like GraalVM, takes a different approach. AOT compiles the source code directly into native executables before the application is run, bypassing the bytecode stage. This pre-compilation significantly reduces the application’s startup times and memory consumption compared to JIT compilation, which happens concurrently with application execution. The efficiency of AOT makes it particularly attractive for performance-sensitive and resource-constrained environments, such as microservices architectures and cloud applications, offering a streamlined alternative to the traditional JVM process.

Create a Spring Boot Project with Native Image Support

To embark on the journey of creating a native image with Spring Boot, start by setting up a new Spring Boot project. You can use Spring Initializr, a convenient web-based tool, to generate your project structure. Select your desired Spring Boot version and include dependencies relevant to your application. Crucially, add the GraalVM Native Support dependency, which is pivotal for native image compilation. Once your project is set up, you can enhance it with specific configurations for native compilation.

Finally, to build your native image, use the command mvn spring-boot:build-image if you're using Maven, or gradle bootBuildImage for Gradle. This process compiles your application into a standalone executable, optimized for your target environment, leveraging the power of ahead-of-time compilation provided by GraalVM.

gradle :

plugins {
java
id("org.springframework.boot") version "3.2.0"
id("io.spring.dependency-management") version "1.1.4"
id("org.graalvm.buildtools.native") version "0.9.28"
}

maven :

<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

Choosing Between Docker Desktop and Colima for Containerization

When it comes to containerizing Spring Boot native applications, professionals have a key decision to make: opting for Docker Desktop or Colima. Docker Desktop, a widely-used platform, offers an intuitive interface and robust feature set for building, sharing, and running containerized applications. However, it’s important to note that in a professional context, Docker Desktop requires a paid license for organizations with more than 250 employees or higher than $10 million in annual revenue. On the other hand, Colima emerges as a compelling alternative, especially for macOS and Linux users. It’s a free, open-source tool that provides similar functionalities to Docker Desktop, including the ability to run Docker containers and Kubernetes clusters locally. Colima’s compatibility with Docker CLI commands makes it an accessible option for those familiar with Docker workflows. For professionals and organizations weighing cost-effectiveness against functionality, the choice between Docker Desktop and Colima hinges on their specific needs, budget constraints, and the scale of their operations.

If you are not on Linux, feel free to increase the memory allocation for the Docker VM.

colima start --memory 8

Since Spring Boot Native directly calls the Docker socket, this command is necessary if using Colima:

colima ssh -- sudo chmod 666 /var/run/docker.sock

Building the application

An important aspect to consider when working with Spring Boot Native images is the build time.The process of compiling a Spring Boot application into a native image can be notably time-consuming, demanding significant amounts of memory and CPU resources.

This is due to the ahead-of-time (AOT) compilation, which involves an extensive analysis and conversion of the application code into a machine-specific executable. Unlike traditional Java compilation, AOT in GraalVM performs numerous optimizations and thorough checks, ensuring that the final native image is highly optimized for performance. While this results in faster startup times and reduced memory usage at runtime, it does require patience during the initial build process. It’s crucial for developers to be aware of this extended build times, particularly when integrating native image builds into continuous integration pipelines or when working under tight development schedules.

./gradlew bootBuildImage

./mvnw -Pnative spring-boot:build-image

Run the application

docker run -p 8080:8080 intro:0.0.1-SNAPSHOT

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.0)

2023-12-18T13:53:55.195Z INFO 1 --- [ main] com.ekino.intro.IntroApplication : Starting AOT-processed IntroApplication using Java 21.0.1 with PID 1 (/workspace/com.ekino.intro.IntroApplication started by cnb in /workspace)
2023-12-18T13:53:55.195Z INFO 1 --- [ main] com.ekino.intro.IntroApplication : No active profile set, falling back to 1 default profile: "default"
2023-12-18T13:53:55.231Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2023-12-18T13:53:55.234Z INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-12-18T13:53:55.234Z INFO 1 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.16]
2023-12-18T13:53:55.281Z INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-12-18T13:53:55.281Z INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 86 ms
2023-12-18T13:53:55.382Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''
2023-12-18T13:53:55.384Z INFO 1 --- [ main] com.ekino.intro.IntroApplication : Started IntroApplication in 0.24 seconds (process running for 0.294)

Effective Reflection Management in Spring Boot Native Images

In the world of Spring Boot native images, mastering reflection management is crucial due to the ahead-of-time (AOT) compilation of GraalVM. Spring Native addresses this by providing custom runtime hints, allowing for explicit configuration of reflection and resource inclusion. This involves registering methods and resources essential for your application’s functionality. For example, methods for reflection are registered to ensure they are accessible in the native compilation, adhering to GraalVM’s requirements. Similarly, vital resources like configuration files are also registered to be included in the native image build. This level of detailed configuration is key in maintaining the dynamic capabilities of Spring Boot applications while optimizing for the efficient, performance-focused environment of native images.

public class IntroRuntimeHints implements RuntimeHintsRegistrar {  

@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// Register method for reflection
Method method = ReflectionUtils.findMethod(IntroController.class, "privateMethod");
hints.reflection().registerMethod(method, ExecutableMode.INVOKE);

// Register resources
hints.resources().registerPattern("my-resource.txt");

}
}

This configuration can then be imported on a Configuration file.

@Configuration(proxyBeanMethods = false)
@ImportRuntimeHints(IntroRuntimeHints.class)
public class NativeConfig {
}

Managing Spring Profiles in Native Images

When working with Spring Boot native images, handling different environments through Spring profiles remains a key aspect. In traditional JVM-based applications, profiles are easily managed at runtime, allowing for the flexible configuration of environment-specific parameters. However, with native images, the approach requires a bit more consideration due to the ahead-of-time (AOT) compilation nature of GraalVM. To effectively manage Spring profiles in a native image context, you need to ensure that all profile-specific configurations are visible to the AOT process. This is typically achieved by explicitly specifying the active profiles during the image build process. You can set the profiles in your application.properties or application.yml file, or pass them as arguments when building the native image. For example, using a Maven command like

./mvnw -Dspring.profiles.active=prod spring-boot:build-image

./gradlew -Dspring.profiles.active=prod bootBuildImage

specifies the 'prod' profile for the build. This approach ensures that the configuration relevant to the specified profile is included and optimized in the final native image, allowing your application to maintain its environment-specific behavior while benefiting from the performance enhancements of native compilation.

Conclusion and Looking Forward to Performance Insights

As we wrap up our exploration of native images in Spring Boot, it’s clear that this technology opens up new avenues for building efficient, high-performance Java applications. We’ve delved into the intricacies of creating native images, managing Spring profiles, and the pivotal role of reflection and custom hints. These insights pave the way for understanding how native images can transform the way we develop and deploy Spring Boot applications. In our next discussion, we will shift our focus to the performance aspect. We’ll examine how native images enhance application performance in terms of startup time, memory usage, and overall efficiency. We’ll also compare these metrics against traditional JVM-based deployments to provide a comprehensive understanding of the benefits and potential trade-offs. Stay tuned as we continue to unravel the capabilities and impact of native images in the Spring Boot ecosystem, guiding you towards making informed decisions for your next high-performance Java project.

--

--