In this blog, you will learn how to build a GraalVM image for your Spring Boot application. Following these practical steps, you will be able to apply them to your own Spring Boot application. Enjoy!

1. Introduction

Java is a great programming language and is platform independent. Write once, run anywhere! But this comes at a cost. Java is portable because Java compiles your code to bytecode. Bytecode is computer object code which an interpreter (read: Virtual Machine) can interpret and convert to machine code. When you start your Java application, the Virtual Machine will convert the bytecode into bytecode specific for the platform, called native machine code. This is done by the just-in-time compiler (JIT). As you will understand, this conversion takes some time during startup.

Assume you have a use case where fast startup time is very important. An example is an AWS Lambda written in Java. AWS Lambda’s are not running when there is no application activity. When a request needs the AWS Lambda to run, the Lambda needs to start up very fast, execute and then shutdown again. Every time the Lambda starts, the JIT compiler needs to do its work. In this use case, the JIT compilation takes up unnecessary time because you already know which platform you are running. This is where ahead-of-time compilation (AOT) can help. With AOT, you can create an executable or “native image” for your target platform. You do not need a JVM anymore and no JIT compilation. This results in a faster startup time, lower memory footprint and a lower CPU usage.

GraalVM can compile your Java applicaton into a native image. Spring Boot had an experimental project called Spring Native which helps Spring Boot developers to create native images. As from Spring Boot 3, Spring Native is part of Spring Boot and out of the experimentation phase.

In the remainder of this blog, you will create a basic Spring Boot application and create a GraalVM image for it.

If you want to learn more about GraalVM in an interactive way, the GraalVM workshop is strongly recommended.

The sources used in this blog are available at GitHub.

2. Prerequisites

Prerequisites for this blog are:

  • Ubuntu 22.04 is being used;
  • Basic Linux knowledge;
  • Basic Java and Spring Boot knowledge;
  • SDKMAN is used for switching between JDKs.

3. Sample Application

First thing to do, is to create a sample application. Browse to the Spring Initializr and add dependencies Spring Web and GraalVM Native Support. Do ensure that you use Spring Boot 3! Generate the project and open it in your favourite IDE.

Add a HelloController with one endpoint returning a hello message.

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        return "Hello GraalVM!";
    }
}

Build the application:

$ mvn clean verify
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  6.971 s
[INFO] Finished at: 2023-02-18T10:26:33+01:00
[INFO] ------------------------------------------------------------------------

As you can see in the output, it takes about 7 seconds to build the Spring Boot application.

The target directory contains the jar-file mygraalvmplanet-0.0.1-SNAPSHOT.jar which is about 17.6MB in size.

Start the application from the root of the repository:

$ java -jar target/mygraalvmplanet-0.0.1-SNAPSHOT.jar
2023-02-18T10:30:15.013+01:00  INFO 17233 --- [           main] c.m.m.MyGraalVmPlanetApplication         : Starting MyGraalVmPlanetApplication v0.0.1-SNAPSHOT using Java 17.0.6 with PID 17233 (/home/<user directory>/mygraalvmplanet/target/mygraalvmplanet-0.0.1-SNAPSHOT.jar started by <user> in /home/<user directory>/mygraalvmplanet)
...
2023-02-18T10:30:16.486+01:00  INFO 17233 --- [           main] c.m.m.MyGraalVmPlanetApplication         : Started MyGraalVmPlanetApplication in 1.848 seconds (process running for 2.212)

As you can see in the output, it takes 1.848 seconds to start the Spring Boot application.

With the help of top and the PID which is logged in the first line of the output, you can check the CPU and memory consumption.

$ top -p 17233

The output shows that 0.3% CPU is consumed and 0.6% memory.

4. Create Native Image

In the previous paragraph, you created and ran a Spring Boot application as you normally would do. In this paragraph, you will create a native image of the Spring Boot application and run it as an executable.

Because you added the GraalVM Native Support dependency when creating the Spring Boot application, the following snippet is added to the pom file.

<build>
  <plugins>
    <plugin>
	  <groupId>org.graalvm.buildtools</groupId>
	  <artifactId>native-maven-plugin</artifactId>
	</plugin>
	...
  </plugins>
</build>

With the help of the native-maven-plugin, you can compile the native image by using the native Maven profile.

$ mvn -Pnative native:compile
...
[INFO] --- native-maven-plugin:0.9.19:compile (default-cli) @ mygraalvmplanet ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  8.531 s
[INFO] Finished at: 2023-02-05T16:50:20+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.graalvm.buildtools:native-maven-plugin:0.9.19:compile (default-cli) on project mygraalvmplanet: 'gu' tool wasn't found. This probably means that JDK at isn't a GraalVM distribution. -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException

The compilation fails. Reason is that GraalVM is not used for the compilation. Let’s install GraalVM first.

Installing and switching between JDKs is fairly simple when you use SDKMAN. If you do not have any knowledge of SDKMAN, do check out a previous post.

Install GraalVM.

$ sdk install java 22.3.r17-nik

Use GraalVM in the terminal where you are going to compile.

$ sdk use java 22.3.r17-nik

Run the native build again.

$ mvn -Pnative native:compile
...
Produced artifacts:
 /home/<user directory>/mygraalvmplanet/target/mygraalvmplanet (executable)
 /home/<user directory>/mygraalvmplanet/target/mygraalvmplanet.build_artifacts.txt (txt)
========================================================================================================================
Finished generating 'mygraalvmplanet' in 2m 15s.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  02:27 min
[INFO] Finished at: 2023-02-18T10:48:40+01:00
[INFO] ------------------------------------------------------------------------

The build now takes about 2,5 minutes. Remember that the build without native compilation took about 7 seconds. This is a huge increase in build time. This is due to the AOT compilation.

The target directory contains a mygraalvmplanet executable which has a size of about 66.2MB. This is also an increase in size compared the jar-file which was 17.6MB in size. But remember, the executable does not need a JVM to run, the jar-file does.

Start the Spring Boot application from the root of the repository.

$ target/mygraalvmplanet
2023-02-18T10:52:29.865+01:00  INFO 18085 --- [           main] c.m.m.MyGraalVmPlanetApplication         : Starting AOT-processed MyGraalVmPlanetApplication using Java 17.0.5 with PID 18085 (/home/<user directory>/mygraalvmplanet/target/mygraalvmplanet started by <user> in /home/<user directory>/mygraalvmplanet)
...
2023-02-18T10:52:29.920+01:00  INFO 18085 --- [           main] c.m.m.MyGraalVmPlanetApplication         : Started MyGraalVmPlanetApplication in 0.069 seconds (process running for 0.085)

If you blinked your eyes, you probably did not see it starting at all, because the startup time is now 0.069 seconds. Compared to the 1.848 seconds without native compilation, this is almost 27 times faster.

When you take a look at the CPU and memory consumption with top, you notice that the CPU consumption is negligable and the memory consumption is now 0.2% of the available memory, thus 3 times lower memory consumption.

Also note, that it is an executable now for a specific target platform.

5. Something About Reflection

GraalVM uses static analysis during compiling the classes. Only the classes which are being used in the application are analyzed. This means that problems can arise when Reflection is being used. Spring makes extensively use of Reflection in their code and that was one of the reasons for the Spring Native project. A lot of Reflection has been removed from Spring. Besides that, it is possible to instruct GraalVM to add classes by means of a metadata file when GraalVM cannot find them during the static analysis. You can do so for your own application, but you do not have any influence on dependencies you are using. You can ask the maintainers to add the GraalVM metadata file, but they are not obliged to do so. In order to circumvent this issue and to make the life of Spring developers more easy, Spring contributes to the GraalVM Reachability Metadata Repository and this repository is being consulted during native compilation of your Spring Boot application.

Let’s see what happens when you add Reflection to your application.

Create a basic POJO which you will use from within the HelloController by means of Reflection.

public class Hello {

    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

In the HelloController, you try to load the POJO by means of Reflection, set a hello message in the object and return the hello message.

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        String helloMessage = "Default message";
        try {
            Class<?> helloClass = Class.forName("com.mydeveloperplanet.mygraalvmplanet.Hello");
            Method helloSetMessageMethod = helloClass.getMethod("setMessage", String.class);
            Method helloGetMessageMethod = helloClass.getMethod("getMessage");
            Object helloInstance = helloClass.getConstructor().newInstance();
            helloSetMessageMethod.invoke(helloInstance, "Hello GraalVM!");
            helloMessage = (String) helloGetMessageMethod.invoke(helloInstance);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
        return helloMessage;
    }
}

Compile the application again in order to create a native image.

$ mvn -Pnative native:compile

Execute the image from within the root of the repository.

$ target/mygraalvmplanet

Invoke the hello endpoint.

$ curl http://localhost:8080/hello
Hello GraalVM!

And it just works! But how is this possible because we did not add a GraalVM metadata file? The answer can be found in the GraalVM documentation.

The analysis intercepts calls to Class.forName(String), Class.forName(String, ClassLoader), Class.getDeclaredField(String), Class.getField(String), Class.getDeclaredMethod(String, Class[]), Class.getMethod(String, Class[]), Class.getDeclaredConstructor(Class[]), and Class.getConstructor(Class[]).

GraalVM will be able to add the necessary classes in the executable when one of the above calls are being used. In the example you used, these calls were being used and therefore the Hello POJO was added to the native image.

6. Conclusion

In this post, you learned how to create a GraalVM native image for a Spring Boot 3 application. You noticed the faster startup time, lower CPU and memory consumption compared to using a jar-file in combination with a JVM. Some special attention is needed when Reflection is being used, but for many usages GraalVM will be able to generate a complete native image.