Expand Authors Top

If you have a few years of experience in the Java ecosystem and you’d like to share that with the community, have a look at our Contribution Guidelines.

November Discount Launch 2022 – Top
We’re finally running a Black Friday launch. All Courses are 30% off until next Friday:

>> GET ACCESS NOW

Expanded Audience – Frontegg – Security (partner)
announcement - icon User management is very complex, when implemented properly. No surprise here.

Not having to roll all of that out manually, but instead integrating a mature, fully-fledged solution - yeah, that makes a lot of sense.
That's basically what Frontegg is - User Management for your application. It's focused on making your app scalable, secure and enjoyable for your users.
From signup to authentication, it supports simple scenarios all the way to complex and custom application logic.

Have a look:

>> Elegant User Management, Tailor-made for B2B SaaS

NPI – Lightrun – Spring (partner)

We rely on other people’s code in our own work. Every day. It might be the language you’re writing in, the framework you’re building on, or some esoteric piece of software that does one thing so well you never found the need to implement it yourself.

The problem is, of course, when things fall apart in production - debugging the implementation of a 3rd party library you have no intimate knowledge of is, to say the least, tricky. It’s difficult to understand what talks to what and, specifically, which part of the underlying library is at fault.

Lightrun is a new kind of debugger.

It's one geared specifically towards real-life production environments. Using Lightrun, you can drill down into running applications, including 3rd party dependencies, with real-time logs, snapshots, and metrics. No hotfixes, redeployments, or restarts required.

Learn more in this quick, 5-minute Lightrun tutorial:

>> The Essential List of Spring Boot Annotations and Their Use Cases

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 (the current version is 3.0.0-RC2), but we'll address discrepancies with Spring Boot 2 where appropriate.

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, but it is easier to use the provided interface of Spring Boot so that both the Maven and Gradle Plugin can use our code during AOT processing to generate the build configuration.

One possibility to specify the additional native configuration is Native HintsIn the currently available documentation, this topic is still missing, so we could only find it in the documentation sources (last paragraph, “Runtime 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) {
                // ...
            }
        }
    }

}

A pull request to solve this issue within Spring Boot has merged since version 3.0.0-RC2.

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)

November Discount Launch 2022 – Bottom
We’re finally running a Black Friday launch. All Courses are 30% off until next Friday:

>> GET ACCESS NOW

Generic footer banner
Comments are closed on this article!