Sitemap
Chalaka

Chalaka

10+ years in Software Engineering, specializing in Programming, System Design, Cloud Solutions, and boosting tech productivity.

Spring boot 3 AOT + GraalVM

12 min readApr 7, 2024

--

Press enter or click to view image in full size

What is an AOT Compilation

Ahead-of-time (AOT) Compilation is a process where a high-level programming language is compiled into native machine code before the application is run. Unlike Just-In-Time (JIT) compilation, which happens dynamically at runtime, AOT compilation transforms the code into an executable or binary form in advance. This method has significant implications for performance, including faster startup times, reduced memory usage, and overall improved runtime efficiency.

In the context of Java and frameworks like Spring Boot, AOT compilation is particularly beneficial. It allows applications to start quicker and consume fewer resources, which is crucial in environments that demand rapid scaling or have limited resources, such as cloud-native applications or IoT devices. AOT compilation achieves these benefits by eliminating the need for a Just-In-Time compiler to process the high-level code at runtime, thereby reducing the computational overhead.

One of the key tools enabling AOT compilation for Java applications is GraalVM. GraalVM extends the capabilities of traditional Java Virtual Machines (JVMs) by offering not just a runtime environment for executing Java applications but also features for compiling Java code directly into native executables. These executables are platform-specific and do not require a JVM to run, which further reduces the resource footprint of the application.

GraalVM’s support for AOT compilation is complemented by its ability to execute code written in multiple languages, facilitating the creation of polyglot applications. This feature is particularly useful in a microservices architecture where different services might be written in different languages but need to be compiled into a unified executable format for deployment.

Moreover, frameworks like Spring Boot have begun to integrate AOT compilation capabilities directly into their toolchains, offering developers streamlined paths to compiling their applications into native images. This integration simplifies the process of developing high-performance, efficient applications by leveraging the strengths of AOT compilation.

Why you might want to compile to native

Compiling applications to native binaries through technologies like GraalVM’s Ahead-of-Time (AOT) compilation offers numerous benefits that can significantly improve both the performance and security of your applications. Here’s a detailed look at why you might want to compile your applications to native code, focusing on the points you’ve listed:

1. Instant Startup
Native compilation results in applications that start almost instantaneously. This is because the application code is already compiled to machine code, which the CPU can execute directly without the overhead of bytecode interpretation or Just-In-Time (JIT) compilation. Instant startup is particularly valuable in environments where rapid scaling is required, such as cloud-native applications or microservices, where services need to be quickly spun up or down based on demand.

2. No Warmup
Applications compiled to native code eliminate the “warmup” time typically associated with JIT compilation. In traditional JVM environments, applications often run slower upon startup and gradually reach their peak performance as the JIT compiler optimizes the bytecode over time. Native binaries run at optimal performance from the moment they are launched, ensuring consistent and predictable behavior, which is crucial for performance-sensitive applications.

3. Low Resource Usage
Native binaries tend to have a lower memory footprint and reduced CPU usage compared to their JVM counterparts. This is because they don’t need to load the entire JVM or the associated class libraries at runtime. Lower resource usage translates into the ability to run more instances of an application on the same hardware or run applications on resource-constrained devices, making it an eco-friendly option as well.

4. Reduced Attack Surface
Compiling to native code can potentially reduce the application’s attack surface. By eliminating the need for a JVM, and controlled use of reflection significantly narrows the attack surface. This method means only needed components are in the build, and there’s tight oversight on how reflection is used, cutting down chances for attackers to find vulnerabilities. It simplifies the application, making it tougher for unwanted access or exploits, directly aligning with the goal of reducing the attack surface.

5. Compact Packaging
Native applications can be packaged with only the necessary runtime components required for execution, leading to smaller application sizes. This compact packaging is beneficial for distributed applications, as it reduces the time and bandwidth needed to deploy or update applications across a network or the internet. It also simplifies containerization by reducing the size of container images.

6. Lower Compute Costs
The combination of instant startup, no warmup, low resource usage, and compact packaging can significantly reduce compute costs, especially in cloud environments where resources are billed based on usage. By optimizing resource consumption, applications compiled to native code can help organizations reduce their operational costs. This is particularly advantageous for applications with fluctuating demand, where the ability to quickly scale up or down without incurring unnecessary costs is valuable.

Usacase for Native Images

Application Use Cases

  1. Low Memory and CPU Environment

In environments constrained by limited memory and CPU resources, such as embedded systems or IoT devices, native images are invaluable. They ensure applications run efficiently by minimizing their resource footprint. For example, a native image of a monitoring tool on an IoT device allows it to perform optimally, using minimal resources while providing real-time data analysis.

2. Function as a Service (FaaS)

Serverless computing models, where functions execute in response to events, benefit greatly from native images. The reduced startup time and lower resource requirements of native images mean functions can be initialized and terminated rapidly, optimizing resource use and cost. A cloud function compiled as a native image, for instance, can process image uploads to a storage bucket much quicker and more cost-effectively than its traditional counterpart.

3. Scale to Zero

The ability for services to scale to zero — not consuming resources until needed — is a hallmark of efficient cloud-native architectures. Native images facilitate this by enabling services to start up instantaneously when demand arises, making them perfect for fluctuating workloads. An e-commerce site’s recommendation service might scale to zero during off-peak hours, springing to life instantly when customer traffic increases.

4. CLI Tools

Command-line interface (CLI) tools benefit significantly from the quick startup times and execution efficiency provided by native images. For instance, a CLI tool designed for managing cloud resources, when compiled into a native image, delivers instant feedback and action capabilities. This not only enhances developer productivity but also operational agility. Such improvements can be achieved using Spring Shell.

Architectural Use Cases

  1. Microservices

Microservices architectures, which decompose applications into smaller, independently deployable services, see significant advantages with native images. These binaries reduce the startup time and operational overhead of each microservice, facilitating rapid scaling and deployment cycles. For example, a native image of a payment processing microservice allows it to handle transaction spikes efficiently, scaling up quickly as needed.

2. Container Image Distribution

Native images align well with containerized deployments, offering smaller image sizes that reduce storage and speed up distribution. This makes deploying and scaling applications in containerized environments more efficient. A containerized analytics application, when built as a native image, can be rapidly deployed across a cluster, enhancing data processing capabilities.

3. Kubernetes

In Kubernetes environments, the orchestration of containerized applications benefits from the characteristics of native images. Their minimal resource consumption and fast startup improve Kubernetes’ ability to manage workloads effectively, enabling more responsive scaling and resource optimization. A native image-based logging service in a Kubernetes cluster, for example, can scale quickly to meet the logging demands of dynamically changing workloads.

4. Back Offices

Back-office applications that support business operations, like inventory management or customer relationship management systems, also stand to gain. Compiling these applications as native images can lead to improved performance and reliability, supporting business operations more effectively. For instance, a native image-based inventory management tool could offer faster data processing, enhancing supply chain efficiency.

Tradeoff when using Native Image

Let’s illustrate the trade-offs of using GraalVM native images in Java Spring Boot applications with practical examples.

1. Compilation Time
Imagine a Spring Boot microservice that is part of a microservices architecture, undergoing frequent updates. The longer compilation times for creating a native image with GraalVM could slow down the continuous deployment process. If the microservice typically takes a few seconds to compile in a JVM setup, moving to GraalVM could extend this to several minutes, impacting the agility of the development cycle.

2. Memory Consumption During Compilation
A startup developing a Spring Boot application on a limited budget may use modest cloud instances or older hardware for development and builds. The high memory requirements for compiling to a native image could exceed the capabilities of their existing setup, forcing a choice between hardware upgrades or facing slower build processes due to swapping and other memory management issues.

3. Dynamic Features Limitations
A Spring Boot application that heavily relies on runtime reflection for loading plugins dynamically based on customer configurations. With GraalVM’s native images, developers would need to anticipate all possible paths and configurations at compile time, significantly altering the application’s architecture or implementing workarounds through explicit configuration, which could introduce maintenance complexity.

4. Compatibility and Library Support
An e-commerce Spring Boot application uses a third-party library for payment processing, which is not fully compatible with GraalVM’s native compilation. The development team must then either find an alternative library that supports native images, contribute to the existing library to add support, or remove the feature, affecting product functionality.

5. Debugging and Profiling
A financial analytics Spring Boot application behaves unexpectedly under high load, requiring profiling to identify performance bottlenecks. The tools and techniques developers are accustomed to using with the JVM may not apply or provide less insight with a native image, requiring a learning curve and potentially new tooling to diagnose and resolve the issue effectively.

6. Runtime Behavior Differences
A Spring Boot application designed for IoT devices collects data and sends it to a central server. When converted to a native image, developers notice changes in garbage collection behavior that weren’t apparent during JVM-based testing, leading to unexpected pauses or memory issues under certain conditions. This scenario would necessitate additional testing and optimization to ensure the application runs smoothly as a native image.

Supercharging Spring Boot Applications with GraalVM

All the sample codes are available in this repository.

Work with Building Native Image build tool

In this segment, we explore the role of Native Build Tools plugins, essential for compiling your Spring Boot application into a native executable, especially during the dynamic phases of active development. Outlined below are the suggested installation procedures for users on Linux and Mac platforms using SDKMAN

  1. Install SDKMAN
    curl -s “https://get.sdkman.io" | bash
    source “/home/eduk8s/.sdkman/bin/sdkman-init.sh”
    sdk list java
  2. List down all the Java distributions. Several options are available, but recommended to choose between ‘GraalVM CE’ (where CE stands for Community Edition) or ‘Liberica NIK’ (with NIK representing Native Image Kit)
  3. Install GraalVM
    sdk install java 23.r17-nik
    java -version
    native-image — version
  4. Compile the application
    cd native-demo-app
    ./gradlew nativeCompile
  5. Run the application
    build/native/nativeCompile/native-demo-app

Work with Cloud Native Buildpack

Let’s address further considerations relevant to cloud deployment. In this section, we’ll adopt a different build approach that’s better aligned with cloud deployment strategies.

What and Why?
Spring Boot makes it easy to create container images with Cloud Native Buildpacks. When you build your app this way, you get a native executable and a Docker container, or an OCI container, to be exact. This makes your app ready to run on most cloud platforms like AWS, Azure, GCP, and Tanzu.

How?
Before diving into a native compilation, let’s briefly look at how a Spring Boot app usually gets deployed. First, the Java source code is turned into Java bytecode. Then, using Buildpacks, this is packaged into a container image with three key parts: the operating system layer, the JVM layer, and the layer for the app itself.

Press enter or click to view image in full size

Spring Boot 3 brings new features that help create a smaller and more efficient container image, achieved through two extra steps.

Press enter or click to view image in full size

Spring Framework 6 introduces the ability to use Ahead-Of-Time (AOT) optimizations. This process determines at build-time which beans to inject, cutting down the runtime reflection required. This makes Spring Boot apps simpler for the GraalVM native image compiler to analyze.

Get Chalaka’s stories in your inbox

Join Medium for free to get updates from this writer.

Following Java bytecode generation, the GraalVM native image compiler conducts a static analysis of the app. It then creates an optimized, standalone native executable. This executable can run on a minimal operating system and be deployed as a compact, efficient container image, eliminating the JVM installation necessity.

Requirement for Cloud Native Buildpack
To build your application with a Cloud Native Buildpack, you don’t need the GraalVM native image compiler on your machine. However, you must have Docker installed and set up so that the current user (who isn’t the root user) can use it.

Build and run with Cloud Native Buildpack

  1. Build
    cd native-demo-app
    ./gradlew bootBuildImage
  2. Run
    docker run — rm -p 8080:8080 docker.io/library/native-demo-app:0.0.1-SNAPSHOT

The command explained above does the following:
docker run tells Docker to start a container using the image you’ve built.
--rm asks Docker to remove the container once it stops, keeping things tidy.
-p 8080:8080 maps port 8080 of your local machine to port 8080 of the container, letting you access the app running inside the container from your machine.
docker.io//library/native-demo-app:0.0.1-SNAPSHOT this is the specific image name that was stored in your local Docker registry in an earlier step.

Coding Walk Through

The native-demo-app, built with Java 21, Spring Boot 3, and Gradle 8.7, is compatible with any GraalVM distribution for Java 21.

Dependencies

build.gradle

plugins {
id 'java'
id 'org.springframework.boot' version '3.2.4'
id 'io.spring.dependency-management' version '1.1.4'
id 'org.graalvm.buildtools.native' version '0.9.28'
}

group = 'dev.native.app'
version = '0.0.1-SNAPSHOT'

java {
sourceCompatibility = '21'
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
useJUnitPlatform()
}

This build.gradle file is a configuration script used for setting up a Java project with Gradle, a popular build automation tool. It specifies how to build and test the Native-demo-app, which uses Spring Boot and is prepared for compiling into a native image with GraalVM. Here's a breakdown of its main components:

  • Plugins: These extend Gradle’s capabilities. The file includes the basic java plugin for Java projects, org.springframework.boot for integrating Spring Boot (version 3.2.4), io.spring.dependency-management for managing dependencies (version 1.1.4), and org.graalvm.buildtools.native (version 0.9.28) for native image support.
  • Group and Version: These settings specify the project’s group ID (dev.native.app) and version (0.0.1-SNAPSHOT). These are important for identifying the project artifacts in a repository.
  • Java Version: It sets sourceCompatibility to Java 21, indicating the version of Java source code the project is compatible with.
  • Repositories: The project will download its dependencies from Maven Central, one of the largest collections of Java libraries.
  • Dependencies: The dependencies block lists the libraries the project needs. org.springframework.boot:spring-boot-starter-web is included for web application development with Spring Boot, and org.springframework.boot:spring-boot-starter-test is for testing the application using Spring's testing support.
  • Testing Configuration: The tasks.named('test') block configures the testing framework to use JUnit Platform, which is a foundation for launching testing frameworks on the JVM.

Summary

AOT Compilation Benefits

  • Transforms code into native machine code before runtime.
  • Enhances performance with faster startup and reduced memory usage.
  • Particularly beneficial for Java and Spring Boot applications.

Advantages of Compiling to Native

  • Instant application startup.
  • No need for warmup periods typical of JIT compilation.
  • Lower memory footprint and CPU usage.
  • Reduced attack surface due to fewer runtime components.
  • Smaller application sizes and lower compute costs.

Use Cases for Native Images

Application Use Cases

  • Optimal for low memory and CPU environments like IoT devices.
  • Ideal for Function as a Service (FaaS) with rapid scalability.
  • Supports scale to zero capabilities for efficient resource use.

Architectural Use Cases

  • Enhances microservices architecture with quick startup.
  • Streamlines container image distribution and Kubernetes orchestration.
  • Suitable for back-office applications requiring performance and reliability.

Trade-offs When Using GraalVM Native Image

  • Longer compilation times.
  • Increased memory requirements during compilation.
  • Restrictions on dynamic features and potential library compatibility issues.
  • Requires careful consideration in the development and deployment phases.

Building with Cloud Native Buildpacks

  • Simplifies container image creation without needing local GraalVM installation.
  • Requires Docker installation for building and running applications.
  • Aligned with cloud deployment strategies, offering ease of deployment on cloud platforms.

Development Workflow

  • Utilizes a `build.gradle` file for project setup including Java, Spring Boot, and GraalVM plugins.
  • Specifies using Java 21 for source compatibility to leverage the latest Java features.
  • Includes dependencies for web development and testing with Spring Boot.

Key Takeaway

  • Integration of Spring Boot 3 with AOT compilation and GraalVM native images presents a formidable approach for developing efficient, high-performance applications, addressing the modern demands of application development and deployment, especially in cloud-native environments.

From Chalaka

Thank you for reading until the end. Before you go:

  • Please consider clapping and following the writer! 👏
  • In the next chapter, I’ll introduce you to the GraalVM Hint Processor with practical use cases.

--

--