Spring Native Application Example

Published
Updated

Please note, Spring Native is an experimental release and implementation may be changed prior to the final release. The Spring Native Framework allows developers to create natively compiled images of their applications. These native images encapsulate all the different functions of your code, libraries, resources, and JDK into a singular package which has been optimized to run on a specific platform. This results in an application that starts more quickly, requires less memory, and uses less CPU due to minimized system overhead and fewer garbage collection cycles.

In addition to native image building, as of version 0.9, Spring Native can act as an AOT (Ahead of Time) framework which runs before the usual project Java build and can be used in a few different ways.

  • Compiling portions of Java code into native code to improve application start times
  • Creating native image configuration files
  • Perform the expected native image building of an application

Intro to Native Images with Spring

Natively built images are by no means a new concept. They are largely used in enterprise settings to improve cloud and container level efficiencies so that applications can start quicker and containers can scale more efficiently. Additionally, there is huge value in short start times when running your Spring application within an AWS Lambda, a Google / Azure Cloud function, or any other serverless settings.

Developers already rely on GraalVM to produce self-contained native Java applications. However, due to how Spring performs dependency injection, and with how the different Spring components rely on reflection, it is difficult to convert a Spring Boot application into a native application using GraalVM. GraalVM has difficulty understanding Spring Framework’s dependency injection and autowiring models as GraalVM relies on static analysis to interpret application function and intent.

The goal of the Spring Native project is to fix this issue, by providing a framework for easy and efficient native image building for Spring Boot applications for the GraalVM runtime.

Spring Native Image Build Process Explained

As with any compiler, Spring Native attempts to create the most optimal, natively-built executable of your application. Spring Native will granularly analyze all aspects of your application and attempt to discard unused portions of code, resources that are not loaded, unnecessary configuration files, metadata not required to perform reflection calls, and JDK components which are not required to run your application.

Similarly, any included libraries will be slimmed down as to remove code which is not necessary to run your application. This simultaneously reduces the size of your application and reduces the CPU and memory impact, thereby improving application performance when running in a native context.

Configuring your application for Spring Native

The following example shows you how to configure Maven to:

  • Build a cloud Docker image of your Spring Application compiled natively using Spring Native.
  • Create a natively compiled application which can be ran and invoked via the command line.

The core app is a simple web function driven by Netty that takes in a string and responds back.

Creating an example Spring Native application

The following code is a simple example of a web function that we will use to build into a native application with Spring Native.

package com.codetinkering.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ExampleApp {

    public static void main(String[] args) {
        SpringApplication.run(ExampleApp.class, args);
    }
}

The following code creates a sample web function that will take in a String and return another String appended to it.

package com.codetinkering.example;

import org.springframework.stereotype.Component;
import java.util.function.Function;

@Component
public class SimpleFunction implements Function<String, String> {

    @Override
    public String apply(String s) {
        return "Test Function + " + s;
    }
}

Maven Setup for Spring Native

You will need to implement the spring-boot-starter-parent and make sure it is at least version 2.4.3 as prior versions do not have the needed hints to perform the build properly.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.codetinkering</groupId>
    <artifactId>spring-native-example</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/>
    </parent>

Next, we define a number of properties which will be used within our build to instruct GraalVM and Spring Native about details of our application. Currently only Java 8 and 11 are supported with GraalVM.

<properties>
        <!-- Only JDK versions 8 and 11 are currently supported -->
    <java.version>1.8</java.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>

The following properties are needed further on in the build plugin process, ignore them for now.

    <!-- This property is needed to prevent a conflict between various build plugins -->
    <classifier/>

    <!-- You can add additional native build arguments with this property -->
    <native.build.args/>

The following property specifies our main class used by our application and is required by the native-image-maven plugin. Point it to your Spring Boot entry point so that GraalVM can perform static analysis on your application.

    <!-- Specify main class to allow your native-image-maven-plugin to find it -->
    <main.class>com.codetinkering.example.ExampleApp</main.class>

The builder property defines which Paketo build pack to use for your cloud image. Valid options are:

  • paketobuildpacks/builder:tiny
  • paketobuildpacks/builder:base
  • paketobuildpacks/builder:full

Finally, we need to override Spring-Cloud to the the snapshot version as the Spring Native framework is currently in the beta stage.

    <!-- This specifies the build pack to be used in your generated cloud image. -->
    <!-- Valid options are tiny, base, full -->
    <builder>paketobuildpacks/builder:base</builder>

    <spring-cloud.version>2020.0.2-SNAPSHOT</spring-cloud.version>
</properties>

The following are the dependencies used in my application. For the purposes of building with Spring Native, only the spring-native is needed in your Maven pom.xml:

<dependencies>
    <dependency>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-native</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-function-web</artifactId>
    </dependency>
</dependencies>

The following are all of the build plugins needed to perform native image creation of your application. I have identified the different plugins and their functions below.

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.experimental</groupId>
            <artifactId>spring-aot-maven-plugin</artifactId>
            <configuration>
                <removeYamlSupport>true</removeYamlSupport>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <classifier>${classifier}</classifier>
                    <image>
                        <builder>${builder}</builder>
                        <env>
                            <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                            <BP_NATIVE_IMAGE_BUILD_ARGUMENTS>${native.build.args}</BP_NATIVE_IMAGE_BUILD_ARGUMENTS>
                        </env>
                    </image>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.experimental</groupId>
                <artifactId>spring-aot-maven-plugin</artifactId>
                <version>0.9.2</version>
                <executions>
                    <execution>
                        <id>test-generate</id>
                        <goals>
                            <goal>test-generate</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>generate</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.graalvm.nativeimage</groupId>
                <artifactId>native-image-maven-plugin</artifactId>
                <version>21.0.0</version>
                <configuration>
                    <mainClass>${main.class}</mainClass>
                    <imageName>${project.artifactId}</imageName>
                    <buildArgs>${native.build.args}</buildArgs>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>native-image</goal>
                        </goals>
                        <phase>package</phase>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

Spring AOT Maven Plugin

Spring Native was designed to be an extensible service that can accommodate many different application designs. As part of this framework, the spring-aot-maven-plugin is an ahead of time service which analyzes your application structure and the included Spring libraries for a number of different metadata objects known as hints. These can be hint annotations like @TypeHint and @NativeHint or could be a number of JSON metadata files which may be more familiar to if you have used GraalVM before.

These hints contain metadata about the code’s usage about reflection and other data relationships. Without these instructions, GraalVM would discard much of the needed Spring library code as its static code analyzer would misjudge the library’s function due to the extensive reflection usage.

For most Spring Boot applications, you likely will not need to implement these hints as much of the core starters and Spring Libraries already include these hints.

Native Image Maven Plugin

The native image plugin is what actually performs the static analysis and compilation of your Spring Boot application so that it is converted into a singular executable file. This plugin is used by Spring Native in conjunction with the Spring AOT plugin and the Spring Boot Maven plugin, it allows executables to be built, or entire docker images to be packaged.

Repositories for Spring experimental releases

The following Maven repositories will need to be included in your Maven build while Spring Native is in the beta stage as it has not been finalized yet.

<pluginRepositories>
    <pluginRepository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </pluginRepository>
    <pluginRepository>
        <id>spring-snapshot</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </pluginRepository>
</pluginRepositories>
<repositories>
    <repository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-snapshot</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
</repositories>

Dependency Management Tweaks

The following entries need to be made to your Pom.xml’s <dependencyManagement> block of your pom.xml file as some of the components we are working with are experimental and snapshots. This will likely change on the general release of Spring Native.

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.experimental</groupId>
            <artifactId>spring-native</artifactId>
            <version>0.9.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Configuring Maven Profiles for Spring Native

The following profile is used to scope the usage of the native-image-maven-plugin . Note the classifier property: this is needed otherwise it will cause conflicts with the build and produce a class not found error.

<profiles>
    <!-- Enable building a native image using a local installation of native-image with GraalVM native-image-maven-plugin -->
    <profile>
        <id>native-image</id>
        <properties>
            <!-- Avoid a clash between Spring Boot repackaging and native-image-maven-plugin -->
            <classifier>exec</classifier>
        </properties>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.nativeimage</groupId>
                    <artifactId>native-image-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Using Homebrew to install GraalVM

If you are trying to install GraalVM using Homebrew, follow my steps here.

Alternatively, use SDKMan to install GraalVM

If you are trying to install GraalVM on a corporate device that is behind a proxy, please refer to my guide here.

SDKMan is a Java SDK management tool that allows you to easily install and configure the dependencies needed for GraalVM. You can install it using the following commands:

curl -s "https://get.sdkman.io" | bash

Close and restart your terminal, then run the following:

sdk install java 21.0.0.r8-grl
sdk use java 21.0.0.r8-grl
gu install native-image

Build a Spring Native application into a Docker Image

Run the following command to build a Docker image file with your compiled code wrapped inside of a build pack.

mvn spring-boot:build-image

From here you can run the image with Docker:

docker run -p 8080:8080 docker.io/library/spring-native-example:1.0-SNAPSHOT

If you include the application code sample, you can also perform a curl call to the endpoint and see a response:

curl localhost:8080 -d TESTMESSAGE -H "Content-Type: text/plain" -w '\n'

Build a Spring Native application into an executable file

Run the following command to simply compile a native binary executable of your Spring Boot application:

mvn clean -Pnative-image package

You can then execute the native application by running:

target/spring-native-example
  • Take note on the application start up time. For me it started in under ~40 milliseconds. The non-native application took around ~5 seconds, that is a 100x difference!

Checkout this project from Github

git clone https://github.com/code-tinkering/spring-native-example
Download Zip

Common errors and issues

Could not find executable native-image

This happens when the native-image plugin in your JDK is not installed. Please see above to install the native-image plugin for your jdk.

The ApplicationContext could not start as ‘org.springframework.aot.StaticSpringFactories’ that is generated by the Spring AOT plugin could not be found.

This can happen when the generate step is missing from your Maven pom file or if spring-aot:generate is not getting executed as part of the Maven build, see below for reference:

...
<plugin>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-aot-maven-plugin</artifactId>
    <version>0.9.2</version>
    <executions>
        <execution>
            <id>test-generate</id>
            <goals>
                <goal>test-generate</goal>
            </goals>
        </execution>
        <execution>
            <id>generate</id>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
</plugin>
...

This happens because core parts of Spring are being stripped away as the Ahead of Time (AOT) analysis is not generating the necessary code for your application.

If you are still having this error, it may be related to your project build configuration. If you are using Eclipse or Intellij and building your project with the IDE, make sure the project build is entirely delegated to Maven. To ensure this, you can run the mvn build command in the terminal instead.

Finally, if you must rely on your IDE to build your project, see see this guide on how to configure IntelliJ to add a trigger for your maven goals..

Could not find option ‘InlineBeforeAnalysis’.

You need to install version 21.0.x of GraalVM. This is a known issue with 20.3.x versions of GraalVM.

Error code 137 when building an application image

This happens when your computer doesn’t have sufficient memory to proceed with the build. Try using a smaller Paketo build pack or closing out of other programs. I have 16GB on my laptop and encountered this error a few times when I had too many things open. My assumption is the process can use upwards of 10GB to build the image.

Error: Main entry point class not found.

This happens when GraalVM is unable to find the entry point to your application.

  • First, make sure you are providing the correct main class to the native-image-maven-plugin for your application.

  • Alternatively, this may also be caused by a conflict between Spring Boot repackaging and the native-image-maven-plugin. To solve this, add <classifier>exec</classifier> to your properties of your native-image profile. See below:

<profiles>
    <profile>
        <id>native-image</id>
        <properties>
            <classifier>exec</classifier>
        </properties>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.nativeimage</groupId>
                    <artifactId>native-image-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

References

https://docs.spring.io/spring-native/docs/current/reference/htmlsingle/index.html#samples

https://github.com/spring-projects-experimental/spring-native#play-with-the-samples

https://spring.io/blog/2021/01/28/spring-cloud-2020-0-1-aka-ilford-is-available