With the Spring Boot 3 release, the Spring Native project has been rolled into Spring Boot. Now you can build GraalVM Native Images of your Spring Boot application easily, and know that the Spring Boot framework will work. There are some caveats you should be aware of, and we recommend you read the GraalVM Native Image Support section of the Spring Boot docs.
What is a GraalVM Native Image, and why do you care? At a very high level: normal Java applications run in the JVM, and the JIT (Just In Time) compiler runs the Java byte code in the .class files in the JVM, using whatever platform the JVM is installed on. While the JIT can perform runtime optimizations, improving application performance as the application runs, the downside is a relatively slow launch time as the JVM loads everything up, and often a relatively high memory usage. In contrast, a GraalVM Native Image, uses an AOT (Ahead Of Time) complier, to generate a standalone native binary file, similar to how a C/C++ is complied to a native binary. The advantage is that this binary will launch in a fraction of the time that the JVM normally takes to launch a Java application. Often the native application will also run faster and use fewer system resources (CPU and RAM), although in some instances the JIT can provide faster performance for some applications. There are many articles and resources that go much more deeply into the topic than we have room for here, so please check out the GraalVM docs, Oracle’s Quick Start Guide, and others.
The nuances of migrating your Spring Boot application to Spring Boot 3, and ensuring it will work well as a Native Image, is out of scope of this post (although may be covered in an upcoming post). So we will assume you have a happy Spring Boot 3 application, and have run it as a GraalVM Native Image locally. We use Gradle instead of Maven, so we are assuming a Gradle build using the org.graalvm.buildtools.native plugin, and the nativeCompile task for generating a Native Image binary.
GitHub Actions are a great Automation and CI/CD tool from GitHub. You may want to build GraalVM Native Images using GitHub Actions to have it as part of your CI/CD plan, or you may want an easy way to build it on a x86 platform. Because the resulting application is a binary file, it is compiled for the CPU architecture you are on. So if you are using an Apple M1/M2 computer, the binary you build can only be run on arm64 systems, not on Intel/x86 systems. However, you can get an x86 compatible build from GitHub actions, which can be deployed to most standard servers easily.
GitHub actions are configured using YAML files under a .github/workflows path in your GitHub project repository. A workflow’s YAML file contains steps which will be executed to perform the automation actions, which could be building, testing, deploying, or just about anything. It’s a very powerful and flexible system.
Let’s take a look at our GitHub Action workflow file (you can get the file here: gist )
name: Java CI with Gradle
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v3
- name: Set up JDK 19
uses: graalvm/setup-graalvm@v1
with:
version: "22.3.0"
java-version: "19"
components: "native-image"
github-token: ${{ secrets.GITHUB_TOKEN }}
native-image-job-reports: "true"
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: nativeCompile
- uses: actions/upload-artifact@v3
with:
name: Package
path: build/native/nativeCompile
The “on” block tells GitHub when to run the Action, in this case whenever code is pushed or PRed to the main branch. You can add other branches to this as needed.
The “runs-on” attribute declares the OS that the build will happen on. You can use ubuntu-latest for a newer version of things, however if you are deploying the built binary on a system running Amazon Linux 2, then you need to leave it at ubuntu-18.04. Newer versions of Ubuntu use a newer version of GLIBC, and the binaries built on that will not run on AL2.
After the build environment is provisioned, we get into the Action steps. In this case we’ll checkout the repository code, and then setup the GraalVM JDK. You can see we are using JDK 19 in this build, to take advantage of the Project Loom threading model, but you can change this to version 17 just by changing the “java-version” attribute.
Next, we’ll run the Gradle build, using the nativeCompile to generate a GraalVM Native Image binary.
Then we’ll upload the generated binary artifact in a zip file called Package.zip which can be downloaded after the Action runs, or used in a deployment step, etc… This is just a simple builder, and in normal operation we would likely have testing and deployment steps as well.
Keep in mind that Native builds take time and the GitHub Action environment is very memory limited, leading to a lot of time spent in garbage collections while working on the build process. It may take 10-15 minutes or more depending on your application. Be patient!