Partner – Microsoft – NPI (cat= Spring)
announcement - icon

Azure Spring Apps is a fully managed service from Microsoft (built in collaboration with VMware), focused on building and deploying Spring Boot applications on Azure Cloud without worrying about Kubernetes.

And, the Enterprise plan comes with some interesting features, such as commercial Spring runtime support, a 99.95% SLA and some deep discounts (up to 47%) when you are ready for production.

>> Learn more and deploy your first Spring Boot app to Azure.

You can also ask questions and leave feedback on the Azure Spring Apps GitHub page.

1. Overview

In this article, we’ll learn about native images and how to create native images from Spring Boot applications and GraalVM’s Native Image builder. We refer to Spring Boot 3, but we’ll address discrepancies with Spring Boot 2 at the end of the article.

2. Native Images

A native image is a technology to build Java code to a standalone executable. This executable includes the application classes, classes from its dependencies, runtime library classes, and statically linked native code from JDK. The JVM is packaged into the native image, so there’s no need for any Java Runtime Environment at the target system, but the build artifact is platform-dependent. So we’ll need one build per supported target system, which will be easier when we use container technologies like Docker, where we can build a container as a target system that can be deployed to any Docker runtime.

2.1. GraalVM and Native Image Builder

The General Recursive Applicative and Algorithmic Language Virtual Machine (Graal VM) is a high-performance JDK distribution written for Java and other JVM languages, along with support for JavaScript, Ruby, Python, and several other languages. It provides a Native Image builder –  a tool to build native code from Java applications and package it together with the VM into a standalone executable. It is officially supported by the Spring Boot Maven and Gradle Plugin with a few exceptions (the worst is that Mockito does not support native tests at the moment).

2.2. Special Features

There are two typical features that we meet when building native images.

Ahead-Of-Time (AOT) Compilation is the process of compiling high-level Java code into native executable code. Usually, this is made by the JVM’s Just-in-time compiler (JIT) at runtime, which allows for observation and optimization while executing the application. This advantage is lost in the case of AOT compilation.

Typically, before AOT compilation, there can optionally be a separate step called AOT processing, i.e. collecting metadata from the code and providing them to the AOT compiler. The division into these 2 steps makes sense because AOT processing can be framework specific, while the AOT compiler is more generic. The following picture gives an overview:

Overview: Native Build Steps

Another specialty of the Java platform is its extensibility on the target system by just putting JARs into the classpath. Because of reflection and annotation scanning on startup, we then get extended behavior in the application.

Unfortunately, this slows down startup time and does not bring any benefit, especially for cloud-native applications, where even the server runtime and the Java base classes are packaged into the JAR. So, we dispense with this feature and can then build the application using Closed World Optimization.

Both features reduce the amount of work needed to be performed at runtime.

2.3. Advantages

Native images provide various advantages, like an instant startup and reduced memory consumption. They can be packaged into a lightweight container image for faster and more efficient deployment, and they present a reduced attack surface.

2.4. Limitations

Because of the Closed World Optimization, there are some limitations, that we have to be aware of when writing application code and using frameworks. Shortly:

  • Class initializers can be executed at build time for faster startup and better peak performance. But we have to be aware that this may break some assumptions in the code, e.g., when loading a file that then has to be available at build time.
  • Reflection and Dynamic Proxies are expensive at runtime and therefore optimized at build time under the Closed World assumption. We can use it without restriction in class initializers when executed at build time. Any other usage must be announced to the AOT compiler, which the Native Image builder tries to reach by performing static code analysis. If this fails, we have to provide this information, e.g., by a configuration file.
  • The same applies to all technologies based on reflection, like JNI and Serialization.
  • In addition, the Native Image builder provides its own native interface that is much simpler than JNI and with lower overhead.
  • For native image builds, bytecode is not available at runtime anymore, so Debugging and Monitoring with tools targeted to the JVMTI is not possible. We then have to use native debuggers and monitoring tools.

Concerning Spring Boot, we have to be aware that features like profiles, conditional beans, and .enable properties are not fully supported at runtime anymore. If we use profiles, they have to be specified at build time.

3. Basic Setup

Before we can build native images, we have to install the tools.

3.1. GraalVM and Native Image

First, we install the current version of GraalVM and the native-image builder following the installation instructions. (Version 22.3 is required by Spring Boot) We should make sure that the installation directory is available via the GRAALVM_HOME environment variable and that “<GRAALVM_HOME>/bin” is added to the PATH variable.

3.2. Native Compiler

During the build, the Native Image builder calls the platform-specific native compiler. So, we need this native compiler, following the “Prerequisite” instructions for our platform. This will make the build platform-dependent. We have to be aware that running the build is only possible within the platform-specific command line. For example, running the build on Windows using Git Bash won’t work. We need to use the Windows command line instead.

3.3. Docker

As a prerequisite, we’ll make sure to install Docker, required later to run native images. The Spring Boot Maven and Gradle Plugins use Paketo Tiny Builder to build a container.

4. Configure and Build Project with Spring Boot

Using the Native Build Feature with Spring Boot is quite simple. We create our project, e.g., by using Spring Initializr and adding the application code. Then, to build a native image with GraalVM’s Native Image builder, we need to extend our build with the Maven or Gradle plugin provided by GraalVM itself.

4.1. Maven

The Spring Boot Maven Plugin has goals for AOT processing (i.e., not AOT compiling itself, but collecting metadata for the AOT compiler, e.g., registering the usage of reflection in the code) and for building an OCI image that can be run with Docker. We could invoke these goals directly:

mvn spring-boot:process-aot
mvn spring-boot:process-test-aot
mvn spring-boot:build-image

We don’t need to do so because the Spring Boot parent POM defines a native profile that binds these goals to the build. We need to build with this activated profile:

mvn clean package -Pnative

If we also want to execute native tests, there’s a second profile that we could activate:

mvn clean package -Pnative,nativeTest

If we want to build a native image, we have to add the corresponding goal of the native-maven-plugin. We could therefore define a native profile too. Because this plugin is managed by the parent POM, we can leave the version number:

<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>build-native</id>
                            <goals>
                                <goal>compile-no-fork</goal>
                            </goals>
                            <phase>package</phase>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Currently, Mockito is not supported in native test execution. So we could exclude Mocking tests or simply skip native testing by adding this to our POM:

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <configuration>
                    <skipNativeTests>true</skipNativeTests>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

4.2. Using Spring Boot Without Parent POM

If we cannot inherit from Spring Boot Parent POM but use it as an import-scoped dependency, we have to configure plugins and profiles by ourselves. Then, we have to add this to our POM:

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <version>${native-build-tools-plugin.version}</version>
                <extensions>true</extensions>
            </plugin>
        </plugins>
    </pluginManagement>
</build>
<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <image>
                            <builder>paketobuildpacks/builder:tiny</builder>
                            <env>
                                <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                            </env>
                        </image>
                    </configuration>
                    <executions>
                        <execution>
                            <id>process-aot</id>
                            <goals>
                                <goal>process-aot</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <configuration>
                        <classesDirectory>${project.build.outputDirectory}</classesDirectory>
                        <metadataRepository>
                            <enabled>true</enabled>
                        </metadataRepository>
                        <requiredVersion>22.3</requiredVersion>
                    </configuration>
                    <executions>
                        <execution>
                            <id>add-reachability-metadata</id>
                            <goals>
                                <goal>add-reachability-metadata</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
    <profile>
        <id>nativeTest</id>
        <dependencies>
            <dependency>
                <groupId>org.junit.platform</groupId>
                <artifactId>junit-platform-launcher</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>process-test-aot</id>
                            <goals>
                                <goal>process-test-aot</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <configuration>
                        <classesDirectory>${project.build.outputDirectory}</classesDirectory>
                        <metadataRepository>
                            <enabled>true</enabled>
                        </metadataRepository>
                        <requiredVersion>22.3</requiredVersion>
                    </configuration>
                    <executions>
                        <execution>
                            <id>native-test</id>
                            <goals>
                                <goal>test</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>
<properties>
    <native-build-tools-plugin.version>0.9.17</native-build-tools-plugin.version>
</properties>

4.3. Gradle

The Spring Boot Gradle Plugin provides tasks for AOT processing (i.e., not AOT compiling itself, but collecting metadata for the AOT compiler, e.g., registering the usage of reflection in the code) and for building an OCI image that can be run with Docker:

gradle processAot
gradle processTestAot
gradle bootBuildImage

If we want to build a native image, we have to add the Gradle plugin for GraalVM Native Image building:

plugins {
    // ...
    id 'org.graalvm.buildtools.native' version '0.9.17'
}

Then, we can run the tests and build the project by invoking

gradle nativeTest
gradle nativeCompile

Currently, Mockito is not supported in native test execution. So we could exclude Mocking tests or skip native testing by configuring the graalvmNative extension as follows:

graalvmNative {
    testSupport = false
}

5. Extend the Native Image Build Configuration

As already mentioned, we have to register each usage of reflection, classpath scanning, dynamic proxies, etc., for the AOT compiler. Because the built-in native support of Spring is a very young feature, not all Spring modules currently have built-in support, so we currently need to add this by ourselves. This could be done by creating the build configuration manually. Still, it is easier to use the provided interface of Spring Boot so that both the Maven and Gradle Plugins can use our code during AOT processing to generate the build configuration.

One possibility to specify the additional native configuration is Native Hints. So, let’s see two examples of built-in support currently missing and how to add it to our application to make it work.

5.1. Sample: Jackson’s PropertyNamingStrategy

In an MVC web application, each return value of a REST controller method is serialized by Jackson, naming each property automatically to a JSON element. We can globally influence name mapping by configuring Jackson’s PropertyNamingStrategy in the application properties file:

spring.jacksonproperty-naming-strategy=SNAKE_CASE

SNAKE_CASE is the name of a static member of the PropertyNamingStrategies type. Unfortunately, this member is resolved by reflection. So the AOT compiler needs to know about that, otherwise, we’ll get an error message:

Caused by: java.lang.IllegalArgumentException: Constant named 'SNAKE_CASE' not found
  at org.springframework.util.Assert.notNull(Assert.java:219) ~[na:na]
  at org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
        $Jackson2ObjectMapperBuilderCustomizerConfiguration
        $StandardJackson2ObjectMapperBuilderCustomizer.configurePropertyNamingStrategyField(JacksonAutoConfiguration.java:287) ~[spring-features.exe:na]

To reach this, we then can implement and register the RuntimeHintsRegistrar in a simple way like this:

@Configuration
@ImportRuntimeHints(JacksonRuntimeHints.PropertyNamingStrategyRegistrar.class)
public class JacksonRuntimeHints {

    static class PropertyNamingStrategyRegistrar implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            try {
                hints
                  .reflection()
                  .registerField(PropertyNamingStrategies.class.getDeclaredField("SNAKE_CASE"));
            } catch (NoSuchFieldException e) {
                // ...
            }
        }
    }

}

Note: A pull request to solve this issue within Spring Boot was already merged since version 3.0.0-RC2, so it works with Spring Boot 3 out of the box.

5.2. Sample: GraphQL Schema Files

If we want to implement a GraphQL API, we need to create a schema file and locate it under “classpath:/graphql/*.graphqls”, where it is automatically detected by Springs GraphQL autoconfiguration. This is done via Classpath Scanning, as well as the welcome page of the integrated GraphiQL test client. So to work correctly within the native executable, the AOT compiler needs to know about this. We can register this the same way:

@ImportRuntimeHints(GraphQlRuntimeHints.GraphQlResourcesRegistrar.class)
@Configuration
public class GraphQlRuntimeHints {

    static class GraphQlResourcesRegistrar implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            hints.resources()
              .registerPattern("graphql/**/")
              .registerPattern("graphiql/index.html");
        }
    }

}

The Spring GraphQL team is already working on this, so we might get this built-in in a future version.

6. Writing Tests

To test the RuntimeHintsRegistrar implementation, we don’t even need to run a Spring Boot test, we can create a simple JUnit test like this:

@Test
void shouldRegisterSnakeCasePropertyNamingStrategy() {
    // arrange
    final var hints = new RuntimeHints();
    final var expectSnakeCaseHint = RuntimeHintsPredicates
      .reflection()
      .onField(PropertyNamingStrategies.class, "SNAKE_CASE");
    // act
    new JacksonRuntimeHints.PropertyNamingStrategyRegistrar()
      .registerHints(hints, getClass().getClassLoader());
    // assert
    assertThat(expectSnakeCaseHint).accepts(hints);
}

If we want to test it with an integration test, we can check the Jackson ObjectMapper to have the correct configuration:

@SpringBootTest
class JacksonAutoConfigurationIntegrationTest {

    @Autowired
    ObjectMapper mapper;

    @Test
    void shouldUseSnakeCasePropertyNamingStrategy() {
        assertThat(mapper.getPropertyNamingStrategy())
          .isSameAs(PropertyNamingStrategies.SNAKE_CASE);
    }

}

To test it with the native mode, we have to run a native test:

# Maven
mvn clean package -Pnative,nativeTest
# Gradle
gradle nativeTest

If we need to provide test-specific AOT support for Spring Boot Tests, we could implement a TestRuntimeHintsRegistrar or a  TestExecutionListener using the AotTestExecutionListener interface. We can find details in the official documentation.

7. Spring Boot 2

Spring 6 and Spring Boot 3 have made a big step concerning native image builds. But with the previous major version, this is also possible. We just need to know that there is no built-in support yet, i.e., there was a supplementary Spring Native initiative that dealt with this topic. So, we have to include and configure this in our project manually. For AOT processing, there was a separate Maven and Gradle plugin, which isn’t merged into the Spring Boot plugin. And, of course, integrated libraries did not provide native support to the same extent as they do now (and will do even more so in the future).

7.1. Spring Native Dependency

First, we have to add the Maven Dependency for Spring Native:

<dependency>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-native</artifactId>
    <version>0.12.1</version>
</dependency>

However, for a Gradle project, Spring Native is automatically added by the Spring AOT plugin.

We should note that each Spring Native version only supports a specific Spring Boot version – for example, Spring Native 0.12.1 only supports Spring Boot 2.7.1. So, we should make sure to use the compatible Spring Boot Maven dependencies in our pom.xml.

7.2. Buildpacks

To build an OCI image, we need to explicitly configure a build pack.

With Maven, we’ll require the spring-boot-maven-plugin with native image configuration using the Paketo Java buildpacks:

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <image>
                        <builder>paketobuildpacks/builder:tiny</builder>
                        <env>
                            <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                        </env>
                    </image>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

Here, we’ll use the tiny builder out of the various available builders, like base and full to build a native image. Also, we enabled the buildpack by providing the true value to the BP_NATIVE_IMAGE environment variable.

Similarly, when using Gradle, we can add the tiny builder along with the BP_NATIVE_IMAGE environment variable to the build.gradle file:

bootBuildImage {
    builder = "paketobuildpacks/builder:tiny"
    environment = [
        "BP_NATIVE_IMAGE" : "true"
    ]
}

7.3. Spring AOT Plugin

Next, we’ll need to add the Spring AOT plugin that performs ahead-of-time transformations helpful in improving the footprint and compatibility of the native image.

So, let’s add the latest spring-aot-maven-plugin Maven dependency to our pom.xml:

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

Similarly, for a Gradle project, we can add the latest org.springframework.experimental.aot dependency in the build.gradle file:

plugins {
    id 'org.springframework.experimental.aot' version '0.10.0'
}

Also, as we noted earlier, this will add the Spring Native dependency to the Gradle project automatically.

The Spring AOT plugin provides several options to determine the source generation. For example, options like removeYamlSupport and removeJmxSupport remove the Spring Boot Yaml and Spring Boot JMX support, respectively.

7.4. Build and Run Image

That’s it! we’re ready to build a native image of our Spring Boot project by using the Maven command:

$ mvn spring-boot:build-image

7.5. Native Image Builds

Next, we’ll add a profile named native with build support of a few plugins like native-maven-plugin and spring-boot-maven-plugin:

<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <version>0.9.17</version>
                    <executions>
                        <execution>
                            <id>build-native</id>
                            <goals>
                                <goal>build</goal>
                            </goals>
                            <phase>package</phase>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <classifier>exec</classifier>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

This profile will invoke the native-image compiler from the build during the package phase.

However, when using Gradle, we’ll add the latest org.graalvm.buildtools.native plugin to the build.gradle file:

plugins {
    id 'org.graalvm.buildtools.native' version '0.9.17'
}

That’s it! We’re ready to build our native image by providing the native profile in the Maven package command:

mvn clean package -Pnative

8. Conclusion

In this tutorial, we explored Native Image builds with Spring Boot and GraalVM’s native build tools. We learned about Spring’s built-in native support.

As usual, all the code implementations are available over on GitHub (Spring Boot 2 sample)

Course – LS (cat=Spring)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.