How to Integrate Spring Native Into Spring Boot Microservices | by Wenqi Glantz | Apr, 2022

Spring Native: the wings that make Spring Boot fly

Image background by finix8 from Pixabay

Despite its massive popularity in web and microservices development, Spring Boot has its limitations. Most noticeably, long startup time, and high memory consumption. The root cause lies in how Spring relies on reflection to inspect classes, interfaces, fields, and methods at runtime. Is it possible to have the same productivity without the use of reflection at runtime?

Yes! Spring Native has the answer!

We are going to explore in this story how to integrate Spring Native into a Spring Boot microservice named customer-service, a simple CRUD application to manage customers. We are going to have two customer-service apps, one vanilla Spring Boot app named customer-service, the other, customer-service-native, a Spring Boot app with Spring Native baked in. We are going to take a close look at the performance boost Spring Native brings to Spring Boot.

A sneak peak below on the startup time comparison between customer-service and customer-service-native.

Blazing fast startup for the native Spring Boot app! To find out how this can be done, read on.

Spring Native compiles Spring applications to native executables using the GraalVM native image compiler at build time. Compared to JVM, GraalVM native images can enable cheaper and more sustainable hosting for many types of workloads. Using GraalVM native image provides key advantages, such as:

  • Instant startup
  • Instant peak performance
  • Reduced memory consumption

GraalVM is a high-performance, cloud-native, polyglot runtime that provides significant improvements in application performance and efficiency which is ideal for microservices. Developed by Oracle Labs, GraalVM aims to improve the performance of JVM-based languages ​​to match the performance of native languages. It also aims to reduce the startup time of JVM-based applications by compiling them ahead of time (AOT) with GraalVM Native Image technology.

Native Image is an innovative technology that compiles Java code ahead of time (AOT) into a standalone binary executable, called a native image. The AOT compiler executes several tasks during build time that reduces startup time such as static analysis, removal of unused code, creating fixed classpath, etc.. The Native Image builder, native-image, is a utility that processes all classes of an application and their dependencies, including those from the JDK. It statically analyzes these data to determine which classes and methods are reachable during the application execution. Then it ahead-of-time compiles that reachable code and data to a native executable for a specific operating system and architecture.

The main drawback of the AOT compiler is its long build time. The AOT engine evaluates conditions at build time in order to generate an optimized application context and Spring factories (the plugin system behind Spring Boot) specifically crafted for our application. In practice, that means:

  • Less Spring infrastructure to execute at runtime.
  • Fewer conditions to evaluate at runtime.
  • Less reflection, since programmatic bean registration is used.

To demonstrate how we can integrate Spring Native into a Spring Boot microservice, we will convert a demo Spring Boot service named customer-service into a native Spring Boot service, customer-service-native, by following the steps below to experience the performance boost brought by Spring Native.

Step 1: Install GraalVM and the native-image Tool

Refer to instructions on getting started with GraalVM to install both GraalVM and Native Image. For Windows users, there are a few extra steps to install Visual Studio with Windows 10 SDK, be sure not to miss those steps, details can be found on story using GraalVM and Native Image on Windows 10. On Windows, the native-image Tool only works when it is executed from the Visual Studio’s x64 Native Tools Command Prompt.

Step 2: Changes in pom.xml

The goal of Spring Native is to support the compilation of existing or new Spring Boot applications to native excutables. Unchanged. This is a key point! We do not expect any code change when introducing Spring Native into Spring Boot microservices. Only build file changes such as pom.xml, or any configuration change related to Native Hints (see the section below on Native Hints).

We need to make the following changes in pom.xml:

  • add spring-native dependency
  • add spring-aot-maven-plugin
  • add spring repositories for native build
  • Option to build native image or docker image using Buildpacks
  • Ensure any dependency with runtime scope has its scope removed, such as H2, see below. Otherwise, native image build will fail, in H2’s case, it complains package org.h2.server.web does not existwhich makes sense as the native image is built at build time, dependency marked as runtime scope will not be visible during build time. The simple fix is ​​to just remove the scope line for that dependency.
<dependency>
<
groupId>com.h2database</groupId>
<
artifactId>h2</artifactId>
<
scope>runtime</scope>
</
dependency>

One key point to keep in mind is that AOT compiler does take much longer during build time (> 8 minutes in building our customer-service-native app), which is not desirable during the development phase. To work around it, we can introduce multiple maven profiles:

  • The default profile allows our app to run as a normal Spring Boot app without spring-native dependency. This is ideal for builds during the development phase. Developers won’t even notice that the Spring Boot app they are working on has Spring Native support.
  • The spring-native profile adds spring-native dependency and AOT maven plugin. This bakes Spring Native into our Spring Boot app. Notice we are adding spring-native dependency in this profile only, so that the default profile is not affected by spring-native.
  • The build-docker-image profile builds the app into a Docker image via paketobuildpacks/builder:tiny builder, which requires Docker installation.
  • The build-native-image profile uses GraalVM Native Image to build the app as a native executable.

Per spring.io, the latest Spring Native version 0.11.4, as of this writing, has been tested against Spring Boot 2.6.6. Let’s take a look at the complete pom.xml with the changes, especially the different profiles, mentioned above:

Step 3: Build Spring Boot native app

As mentioned above, there are two ways to build the Spring Boot native app:

  • Using Spring Boot Buildpacks support to generate a lightweight container containing a native executable, assuming Docker is already installed.
mvn clean package spring-boot:build-image -Pspring-native,build-docker-image
  • Using the Native Build Tools to generate a native executable.
mvn clean package spring-boot:build-image -Pspring-native,build-native-image

Step 4: Run the Spring Boot native app

After a successful Buildpacks build with build-docker-image, to launch the Docker container, run the following to launch our demo native app. Notice the --rm flag was added to instruct Docker to automatically clean up the container and remove the file system when the container exits.

docker run --rm wenqi/customerservice:latest

If you build your native app using the Native Build Tools build-native-imagean executable file gets generated under the target directory. In our case, the file name is com.github.wenqiglantz.service.customerservice.customerserviceapplication.exe. Just launch the app by running the executable:

./target/com.github.wenqiglantz.service.customerservice.customerserviceapplication

0.696 seconds! Amazing!

Let’s put those two demo apps side by side and compare some of the key data points. A load test was also performed by using JMeter, monitoring via VisualVM, with 50 users creating customers and another 50 users retrieving customer data. The load test was carried on for a duration of 5 minutes for each app.

  • customer-service: built and ran on amazon-corretto-17.0.1.12.1-windows-x64-jdk
  • customer-service-native: built and ran on graalvm-ce-java17-22.0.0.2.
  • Tests are performed on my Dell Latitude 7400, Intel Core i7–8665U CPU @ 1.90GHz, 2112 Mhz, 4 Cores, and 8 Logical Processors. 16GB RAM. OS Microsoft Windows 10 Enterprise, x64-based.
Build time comparison between Spring Boot app and Spring Boot app with Spring Native support
Memory footprint comparison between Spring Boot app and Spring Boot app with Spring Native support
  • Native Spring Boot app has a much longer build time, as expected due to its AOT compiler.
  • Native Spring Boot app has lightning-fast startup time, as designed due to its GraalVM and AOT compiler. This is one of the reasons why we are integrating Spring Native into our Spring Boot apps. To achieve such amazing performance boost, I can happily live with the long build time. 🙂 Better yet, we can use the default maven profile for local development builds and hand over the native image builds to a CI pipeline.
  • Native Spring Boot app has a much lower memory footprint at both startup and under load. Heap size and used heap for the native app are much lower than those for the non-native app, especially under load, which is quite impressive! Proof that native apps are the way to go!

Spring Native Beta was rolled out in March of 2021. As of the most current version 0.11.4, it supports the following common Spring Boot starter libraries, notice some libraries do require configuration modification, so it’s not a complete seamless experience, yet:

  • spring-boot-starter-data-jdbc
  • spring-boot-starter-data-jpa (with configuration modification)
  • spring-boot-starter-aop
  • spring-boot-starter-batch
  • spring-boot-starter-web (with configuration modification)
  • spring-boot-starter-actuator (with configuration modification)
  • spring-boot-starter-test (mockito is not supported yet)
  • etc.

For a detailed list of which libraries Spring Native supports and which libraries need configuration modification, please refer to Spring’s website on Spring Boot Support.

While working on the demo native app, I ran into multiple issues related to dependencies including JPA, OpenAPI, and Liquibase. As of this writing, Spring Native still does not support Liquibase, there is an open JIRA ticket for this issue.

There are quite a few libraries that are not supported or fully supported by Spring Native yet, so I had to look to Native Hints for resolution.

As specified on Spring’s Native Hints page, to use a feature or library not yet supported by Spring Native, we can manually add static files under META-INF/native-image that are automatically discovered by the GraalVM native image. This is called “Native Hints”, an interesting term the Spring Native team coined.

I ran into missing PostgreSQLDialect error during my native app launch. After explicitly adding the following snippet in reflect-config.json under directory: META-INFnative-imageorgspringframeworkaotspring-aotreflect-config.json, the error went away. Yes, this is a new directory/file that has to be added to manually give hint to Spring Native to discover the classes in this file.

[
{
"name": "org.hibernate.dialect.PostgreSQLDialect",
"condition": {
"typeReachable": "org.postgresql.Driver"
},
"allDeclaredConstructors": true,
"allDeclaredMethods": true
}
]

I also ran into the following Liquibase error during my native app launch:

Native reflection configuration for liquibase.configuration.LiquibaseConfiguration.<init>() is missing

After adding LiquibaseConfiguration class into reflect-config.json, I ran into a series of more related to other Liquibase classes errors being missing. After some research, I discovered Josh Long’s solution to work around Spring Native’s lack of support for Liquibase. A rather verbose solution, it works nevertheless. Until Spring Native can come up with a more elegant solution, we will have to live with this workaround for now. Check out my GitHub repo for the final version of my reflect-config.json.

As noted in its package naming convention, Spring Native is still in an “experimental” module, which means it’s not yet fully production ready. In addition, there are still some limitations to native images:

  • It doesn’t support all Java features
  • Reflection requires a special configuration
  • Lazy class loading is unavailable

Also worth mentioning, support for Spring Native currently is not as abundant as some other popular Spring libraries. The community is more on the lookout, not quite ready to actively embrace it, yet.

The future of Spring Native is first-class native support in Spring Boot 3.x! Spring Boot 3 AOT and native support aim for:

  • Seamless integration in Spring Boot
  • Increased native support
  • Runtime efficiency via AOT transformations for native and JVM
  • Java 17 baseline

Spring Boot 3 general availability release date is currently scheduled for late November 2022. With a commitment to building a strong Java native ecosystem, the Spring team promises Spring Boot 3 is going to be a framework for the next decade! Exciting time indeed!

Spring Boot 3 AOT optimizations and native support will benefit millions of Spring Boot applications. While we eagerly await this momentous release, let’s start using Spring Native to compile our Spring Boot applications into native executables. In the course of experimenting with Spring Native, I was really hoping to see a more growing developer base around Spring Native to allow better collaboration and support for one another, so we all get to experience flying high on Spring Boot with Spring Native as its wings !

We explored Spring Native in this story by looking into GraalVM, Native Image, and AOT compiler. We then walked through the steps in integrating Spring Native into an existing Spring Boot app. We also looked at some of the limitations of Spring Native. The future of Spring Native and Spring Boot 3 is super exciting! I hope this story gives you a better understanding of what Spring Native is, its pros and cons, and its super promising future.

Check out my GitHub repo for the demo apps.

Happy coding!

Leave a Comment