Spring Update #2 – AOT and Native Compilation
In this blog, I continue my 4 part blog series which provides an overview of the most important new features introduced with the release of Spring Framework 6 & Spring Boot 3. I also explain what’s involved when migrating existing applications to these new versions.
If you missed the first edition of this series “New Baselines: Java 17 and Jakarta EE” make sure to check it out.
Next up, I want to share with you the introduction of native compilation supported by an Ahead Of Time (AOT) module that enables compilation to native code, providing improved startup time and reduced memory usage. Let’s dive in.
AOT and Native Compilation
Over the last couple of years, a lot of work has been done to support native compilation using GraalVM in a separate project called Spring Native. The fruits of that labor are now available in Spring 6 and Spring Boot 3 directly: with that, the Spring Native project has been end-of-lifed.
Using this, applications can be turned into machine code: instead of running byte code on a JVM, you create a native executable for your target platform.
This has the benefits of starting up way faster than JVM-based applications and using a lot less memory at runtime. The downside is that the build time increases significantly, but more importantly also that because of the lack of a JVM a lot of dynamic aspects that Java provides, like runtime classloading and reflection, do not just continue to work. This poses quite some challenges for a framework like Spring, which relies heavily on those features
An important part of the solution is provided by a new AOT module. AOT stands for “Ahead Of Time”: during the build process a lot of work is being done already based on an analysis of your application, so that at runtime there’s less reliance on functionality that’s not available in a native image by default.
To make this work, Spring Boot has to make some assumptions about things like what the classpath will look like at runtime, so that no scanning needs to happen, and what Spring profiles will be active, so that it’s known up-front what beans will be defined in your ApplicationContext.
With that it’s also predetermined what auto-configuration needs to be active; without that, your native image would need to contain the code of all possible @AutoConfiguration classes that Boot provides.
This information is then used to generate code that executes the setup that would otherwise have been dynamically determined and executed at startup time in a JVM-based, normal Boot application. With this “closed world assumption” you give up on some dynamic behavior, but get back an application that starts way faster and is much more efficient with its memory.
Many parts of the Spring ecosystem have been made to work with native compilation already, but do check whether that applies to all of your dependencies before you try this out!
The result of applying the AOT module is code that’s much more suitable to be compiled to a native image using GraalVM; this includes the generation of GraalVM-specific hints.
However, the AOT functionality can also be used without GraalVM to make applications start faster and, through the reduced usage of reflection, sometimes also use less memory. Those benefits won’t be quite as dramatic as with native images, though.
Having said that, there is a lot of research happening in how JVM-based applications can start up faster as well, and the Spring team is actively contributing to that. It’s expected that later releases in the 6.x line will support project CRaC
When you execute a native image build on Windows or MacOS using the GraalVM Native Build Tools, that results in an executable targeted at those specific platforms.
Usually you’d rather generate a Docker image containing a Linux executable: Spring Boot provides an option for that using Cloud Native Buildpacks. Both approaches are supported using Maven or Gradle.
For more information, make sure to read the native image documentation.
In conclusion, Spring Boot’s AOT module provides the necessary functionality for applications to be turned into machine code. This process has the benefits of faster start-up times and less memory usage at runtime, but it poses challenges due to the lack of a JVM.
The native image build can be done directly on and for the target platform, or via Cloud Native Buildpacks to produce a Linux executable in a Docker image