After more than four years with the second release of Spring boot, it’s finally time to make way for the third release, which comes with quite a few interesting new features.
Today we are going to focus on what in my view is the most interesting, which is the support for creating native images using GraalVM. Spring Boot 3 provides a plugin that allows us to create native images as easy as:
mvn package -Pnative
I could finish this post here, but there are a few concepts that we need to understand before we start creating native images and which are explained below.
What is the native mode about?
GraalVM is a high-performance runtime built on Oracle Java SE. It adds new compiler
optimizations that deliver the best performance for running Java applications. It enhances the Java ecosystem by offering a compatible and better-performing Java Development Kit (JDK).
Its most important feature is the Native Image utility, which consists on using AOT compilation to produce native executables.
Understanding the compilation modes
Compilation means transforming source code (Java, Python…) into machine code, which is a set of
low-level instructions to be executed in a microprocessor.
- JIT (Just In Time) compilation is the default mode for Java apps → JVM
- Generates machine code during execution of the program
- Traditional computers cannot execute JVM bytecode directly.
- The JVM interprets bytecode at runtime and identifies the architecture where the program is running
- This makes Java apps slower in comparison with other programming languages such as Rust or C which produce native code directly
- AOT (Ahead Of Time) runs over the GraalVM compiler and compiles bytecode directly into machine code at build time
- Generates machine code before the execution of the program
- The resulting code is tailored to a specific OS and hardware architecture → very fast execution
- GraalVM compiler performs a very high AOT compilation of the JVM bytecode. As output, a native executable is produced. It doesn’t require a JVM installed in the machine.
- Native apps consume much less memory and start much faster
What happens during the compilation?
When we build a native image, the compiler performs a static analysis and only elements that are reached from our application entry-point are included. These elements basically consist in the code that our application needs to work, such as JDK classes, dependencies or our own code.
Some elements such as classes or methods may not be discovered due to some aspects such as reflection, dynamic proxies, serialisation… Although the executable will be created correctly, if an element is not included it could lead to runtime failures, so we need to manually tell the compiler that they should also be taken into account and included in the final artefact.
Reachability metadata
In Spring Boot 3, the way we tell the compiler to take into account certain elements during the process of creating a native image is by using reachability metadata. There are different ways to add a class or component to this reachability metadata:
- Adding hints manually in a programmatic way, as described in the official documentation.
- Using configuration files to register the classes we want to include in the native image. These files must be stored under
META-INF.native-image
and have specific naming conventions:reflect-config.json
-> Includes all relative to source code that need to be includedresource-config.json
-> Includes all relative to resources that need to be included
Registering all those elements necessary for the native image to work properly can be a tedious and time-consuming process, especially if your application is large and uses a lot of external dependencies. To assist developers during the process, there is a Github repository which includes a base configuration for reachability metadata. This repostory is consulted during the creation of the native image and for example, if the compiler detects that our app uses PostgreSQL, the reachability metadata related to this will be downloaded from the repository
Summary
We have seen at a high level the fundamental concepts of the processes that take place during the creation of native images, which bring many improvements, but are not always the solution to our needs.
To conclude, I will summarise what I see as the pros and cons of native mode.
👍Dead code elimination → Reduces attack
surface
👍Very fast start time, faster horizontal scale
👍Very low memory consumption
👍No need of Java installation on the machine
👍Small size on disk → smaller containers
→ cheaper cloud infrastructure!
👎More complicated initial configuration
(specially on Windows), learning curve
👎Much slower builds
👎As native mode is based on reflection, even
though we manage to create the image successfully, the app might not work due to
some missing configuration
👎Executable tied to be run on a specific
hardware architecture
I hope this has helped you to understand a little better how native images work. Leave me a comment if you want to know more or if you are interested in creating a small application to see it at work!