Introduction

As we introduced in the previous spring boot3 article, one of the important features of spring boot3 is the support for compiling spring boot3 applications into GraalVM Native Images.

Today we use a specific example to show you how to properly compile a spring boot3 application into a native image.

Install GraalVM

If you want to compile spring boot3 app as a native application, you need the support of GraalVM.

What is GraalVM?

As you can see from the name GraalVM is a virtual machine whose main goal is to improve the performance of java applications and consume less resources.

It adds a JIT compiler and AOT to the java HotSpot JVM to achieve compilation of the application into a native executable. In addition to java, GraalVM also supports JavaScript, Ruby, Python and many other programming languages.

So, why GraalVM? One word: fast.

Installing GraalVM is also relatively simple, we just go to its official download page and download the corresponding version: https://www.oracle.com/downloads/graalvm-downloads.html.

There are two versions of GraalVM, like the JDK, the community version and the enterprise version, so you can choose according to your needs.

Note that spring boot3 requires GraalVM 22.3 or higher, so don’t download the wrong version.

After downloading, we can install GraalVM like a normal JDK installation, here is the example of mac, if the directory we install is /Library/Java/JavaVirtualMachines/graalvm-ee-java17-22.3.0, then we need to configure the corresponding JAVA_HOME and PATH environment variables as follows.

1
2
export PATH=/Library/Java/JavaVirtualMachines/graalvm-ee-java17-22.3.0/Contents/Home/bin:$PATH
export JAVA_HOME=/Library/Java/JavaVirtualMachines/graalvm-ee-java17-22.3.0/Contents/Home

There is a very important command in PATH called gu. If it is not added to PATH, then you may encounter the following exception in use.

1
'gu' tool wasn't found. This probably means that JDK at isn't a GraalVM distribution.

After installation, you can verify it with the following command.

1
2
3
4
java -version
java version "17.0.5" 2022-10-18 LTS
Java(TM) SE Runtime Environment GraalVM EE 22.3.0 (build 17.0.5+9-LTS-jvmci-22.3-b07)
Java HotSpot(TM) 64-Bit Server VM GraalVM EE 22.3.0 (build 17.0.5+9-LTS-jvmci-22.3-b07, mixed mode, sharing)

If you are in a mac environment, you also need to execute the following command to un-quarantine graalvm.

1
sudo xattr -r -d com.apple.quarantine /path/to/graalvm

Otherwise, you will encounter the following problems in use.

graalvm-ee-java17-22.3.0 is corrupted and cannot be opened

Add Native Image support

The purpose of installing GraalVM is to use its native image feature. native image is a separate jar package, which we can install by executing the following command.

1
gu install native-image

Where gu is the command in /Library/Java/JavaVirtualMachines/graalvm-ee-java17-22.3.0/Contents/Home/bin.

The download process also requires you to enter a valid email and perform an email verification. Then ENTER all the way and you are done.

Of course, you can also download the Oracle GraalVM Enterprise Edition Native Image locally and use gu install -L to do a local installation.

Okay, so far everything is ready, let’s see how to package the spring boot3 application as a native image next.

build spring boot3 application

Here we are using maven, so we need to add the following spring boot3 dependencies.

1
2
3
4
5
6
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.0.1</version>
    <relativePath/> 
</parent>

Because we want to build native images, we also need to use the following native-maven-plugin plugin.

1
2
3
4
<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
</plugin>

Here we have only created a very simple main method.

1
2
3
4
5
6
7
8
@SpringBootApplication
public class NativeImageApplication {

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

}

Then, we try to run mvn native:build to build the spring boot3 application.

Remember to always compile the project before you build it.

Unfortunately, you will get the following exception.

1
2
3
4
5
[INFO] --- native-maven-plugin:0.9.19:build (default-cli) @ native-image ---
[WARNING] 'native:build' goal is deprecated. Use 'native:compile-no-fork' instead.
[INFO] Found GraalVM installation from JAVA_HOME variable.
...
Error: Please specify class (or <module>/<mainclass>) containing the main entry point method. (see --help)

From the above exception we found two problems, the first one is a warning which recommends us to use native:compile-no-fork.

The second problem is that the mainclass is not found, according to the exception, we add the following configuration information to the pom plugin, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <configuration>
        <!-- imageName is used to set the name of the generated binary file -->
        <imageName>${project.artifactId}</imageName>
        <!-- mainClass is used to specify the path to the main method class -->
        <mainClass>com.flydean.nativeimage.NativeImageApplication</mainClass>
        <buildArgs>
            --no-fallback
        </buildArgs>
    </configuration>
    <executions>
        <execution>
            <id>build-native</id>
            <goals>
                <goal>compile-no-fork</goal>
            </goals>
            <phase>package</phase>
        </execution>
    </executions>
</plugin>

Then re-execute the command: mvn native:compile-no-fork.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
GraalVM Native Image: Generating 'native-image' (executable)...
========================================================================================================================
[1/7] Initializing...                                                                                    (4.3s @ 0.25GB)
 Version info: 'GraalVM 22.3.0 Java 17 EE'
 Java version info: '17.0.5+9-LTS-jvmci-22.3-b07'
 C compiler: cc (apple, arm64, 14.0.0)
 Garbage collector: Serial GC
 1 user-specific feature(s)
 - org.springframework.aot.nativex.feature.PreComputeFieldFeature
Field org.apache.commons.logging.LogAdapter#log4jSpiPresent set to true at build time
Field org.apache.commons.logging.LogAdapter#log4jSlf4jProviderPresent set to true at build time
Field org.apache.commons.logging.LogAdapter#slf4jSpiPresent set to true at build time
Field org.apache.commons.logging.LogAdapter#slf4jApiPresent set to true at build time
Field org.springframework.core.NativeDetector#imageCode set to true at build time
Field org.springframework.core.KotlinDetector#kotlinPresent set to false at build time
Field org.springframework.core.KotlinDetector#kotlinReflectPresent set to false at build time
Field org.springframework.format.support.DefaultFormattingConversionService#jsr354Present set to false at build time
Field org.springframework.cglib.core.AbstractClassGenerator#imageCode set to true at build time
[2/7] Performing analysis...  [**********]                                                              (24.8s @ 4.57GB)
  10,266 (89.50%) of 11,470 classes reachable
  16,675 (63.53%) of 26,248 fields reachable
  53,776 (60.71%) of 88,575 methods reachable
     469 classes,   140 fields, and 2,281 methods registered for reflection
      63 classes,    69 fields, and    55 methods registered for JNI access
       5 native libraries: -framework CoreServices, -framework Foundation, dl, pthread, z
[3/7] Building universe...                                                                               (5.0s @ 2.72GB)
[4/7] Parsing methods...      [**]                                                                       (4.4s @ 2.42GB)
[5/7] Inlining methods...     [***]                                                                      (1.3s @ 3.87GB)
[6/7] Compiling methods...    [********]                                                                (70.0s @ 1.04GB)
[7/7] Creating image...                                                                                  (4.7s @ 3.35GB)
  30.27MB (58.75%) for code area:    30,771 compilation units
  20.50MB (39.79%) for image heap:  305,579 objects and 93 resources
 769.52KB ( 1.46%) for other data
  51.52MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 packages in code area:                               Top 10 object types in image heap:
   2.02MB com.oracle.svm.core.code                             5.79MB byte[] for code metadata
   1.77MB sun.security.ssl                                     2.31MB byte[] for java.lang.String
   1.29MB java.util                                            2.09MB byte[] for general heap data
 929.52KB java.lang.invoke                                     2.07MB java.lang.String
 925.96KB com.sun.crypto.provider                              1.76MB java.lang.Class
 802.99KB java.lang                                          671.09KB byte[] for embedded resources
 633.35KB sun.nio.ch                                         567.26KB byte[] for reflection metadata
 625.89KB java.util.concurrent                               481.22KB com.oracle.svm.core.hub.DynamicHubCompanion
 601.86KB org.apache.tomcat.util.net                         450.06KB java.util.HashMapNode
 594.48KB sun.security.x509                                  401.78KB java.util.concurrent.ConcurrentHashMapNode
  20.02MB for 397 more packages                                3.40MB for 2297 more object types
------------------------------------------------------------------------------------------------------------------------
                        9.5s (7.9% of total time) in 50 GCs | Peak RSS: 3.75GB | CPU load: 4.39
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
 /Users/learn-springboot3/learn-springboot3/native-image/target/native-image (executable)
 /Users/learn-springboot3/learn-springboot3/native-image/target/native-image.build_artifacts.txt (txt)
========================================================================================================================
Finished generating 'native-image' in 2m 0s.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  02:01 min
[INFO] Finished at: 2023-01-05T20:43:39+08:00
[INFO] ------------------------------------------------------------------------

After a long wait, we finally finished the build.

Because our artifactId is called native-image, we end up with an executable called native-image in the target directory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.
├── classes
│   ├── application.properties
│   └── com
│       └── flydean
│           └── nativeimage
│               └── NativeImageApplication.class
├── generated-sources
│   └── annotations
├── generated-test-sources
│   └── test-annotations
├── maven-archiver
│   └── pom.properties
├── maven-status
│   └── maven-compiler-plugin
│       ├── compile
│       │   └── default-compile
│       │       ├── createdFiles.lst
│       │       └── inputFiles.lst
│       └── testCompile
│           └── default-testCompile
│               ├── createdFiles.lst
│               └── inputFiles.lst
├── native-image
├── native-image-0.0.1-SNAPSHOT.jar
├── native-image-0.0.1-SNAPSHOT.jar.original
├── native-image.build_artifacts.txt
├── surefire-reports
│   ├── TEST-com.flydean.nativeimage.NativeImageApplicationTests.xml
│   └── com.flydean.nativeimage.NativeImageApplicationTests.txt
└── test-classes
    └── com
        └── flydean
            └── nativeimage
                └── NativeImageApplicationTests.class

20 directories, 14 files

If you run target/native-image at this point, you will probably get the following exception.

1
2
3
4
[main] DEBUG org.springframework.context.aot.AotApplicationContextInitializer - Initializing ApplicationContext with AOT
[main] ERROR org.springframework.boot.SpringApplication - Application run failed
java.lang.IllegalArgumentException: Could not find class [com.flydean.nativeimage.NativeImageApplication__ApplicationContextInitializer]
        at org.springframework.util.ClassUtils.resolveClassName(ClassUtils.java:333)

This is because we are missing some spring boot AOT metafile information, the correct way to do this is to use the following command.

1
mvn clean package -Pnative

It actually executes the following commands.

1
2
3
mvn spring-boot:process-aot
mvn spring-boot:process-test-aot
mvn spring-boot:build-image

We end up with a compiled native-image, which we run to get the following result.

1
2
3
4
5
6
7
8
9
2023-01-05T17:07:11.692+08:00  INFO 69299 --- [           main] c.f.nativeimage.NativeImageApplication   : Starting AOT-processed NativeImageApplication using Java 17.0.5 with PID 69299 (/Users/wayne/data/git/ddean2009/learn-springboot3/learn-springboot3/native-image/target/native-image started by wayne in /Users/wayne/data/git/ddean2009/learn-springboot3/learn-springboot3/native-image)
2023-01-05T17:07:11.693+08:00  INFO 69299 --- [           main] c.f.nativeimage.NativeImageApplication   : No active profile set, falling back to 1 default profile: "default"
2023-01-05T17:07:11.709+08:00  INFO 69299 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2023-01-05T17:07:11.710+08:00  INFO 69299 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-01-05T17:07:11.710+08:00  INFO 69299 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.4]
2023-01-05T17:07:11.717+08:00  INFO 69299 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-01-05T17:07:11.717+08:00  INFO 69299 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 24 ms
2023-01-05T17:07:11.729+08:00  INFO 69299 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-01-05T17:07:11.729+08:00  INFO 69299 --- [           main] c.f.nativeimage.NativeImageApplication   : Started NativeImageApplication in 0.053 seconds (process running for 0.072)

Summary

From the runtime, native-image is very fast to start and should improve performance quite a bit.

Reference: http://www.flydean.com/0002-use-native-image-in-springboot3/