Optimising Performance with GraalVM: A Guide to Migrating a Spring Boot Project to Native Image

Abhishek Anand
CodeX
Published in
4 min readFeb 11, 2023

Spring Boot and GraalVM are two popular technologies in the Java world, and the combination of both has created a lot of buzz lately. GraalVM enables you to create native images, which are executable files that run like any other binary. They are known for their fast startup time and low memory footprint.

If you are using Spring Boot 3, creating native images is now possible out of the box. In this article, I’ll walk you through the process of converting one of my micro-services into a native image using Spring Boot and GraalVM.

Existing Project

Before we dive into the conversion process, let’s take a quick look at my project. This micro-service is based on Spring Boot 2.7.6 and Java 17. It connects to Active Directory through the Spring LDAP starter, uses Spring Data Redis for connecting to Redis Sentinel, and also connects to a MySQL database using Spring Data JPA.

Old pom.xml

Upgrading to Spring Boot 3 was a straightforward process. I simply had to upgrade the Spring Boot starter to version 3.0.1 and the Spring Cloud to version 2022.0.0. Also replace all javax.* packages with jakarta.* packages. Follow this article to prepare for migration.

Creating native image

First of all , we need to have GraalVM JDK installed on our machine.

SDKMAN is a tool for managing parallel versions of software development kits, including GraalVM. With SDKMAN, you can easily install, switch between different versions, and manage GraalVM installations on your system.

Before installing GraalVM, you must have SDKMAN installed on your system.

To install GraalVM using SDKMAN, follow these steps:

  1. Open a terminal window on your Mac OS system.
  2. Run the following command to list the available versions of GraalVM
sdk list java

You will see a list of available versions of GraalVM. Select the version you want to install by running the following command, replacing “version” with the version number:

sdk install java version

Wait for the installation process to complete.

To verify the installation, run the following command:

java -version

You should see the version of GraalVM you just installed.

Setting up GraalVM as the Default Java Version To set GraalVM as the default Java version for your system, run the following command:

sdk default java version

Replace “version” with the version number of GraalVM that you installed.

Next, we need to add the GraalVM native plugin to our build. This will enable us to create a native executable. The exact process of adding the plugin depends on the build tool you are using. For example, if you are using Maven, you will need to add the following to your pom.xml file:

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

The spring-boot-starter-parent declares a native profile that configures the executions that need to run in order to create a native image. You can activate profiles using the -P flag on the command line.

To build the image, you can run the native:compile goal with the native profile active:

./mvnw -Pnative native:compile

This will create an executable inside the target directory. You can run it like

./target/<native-executable>

Challenges :

GraalVM’s static code analysis during build time can result in errors like ClassNotFound, MethodNotFound, and ResourceNotFound. To overcome these challenges, hints need to be provided to let GraalVM know about the dynamic elements of the code. To achieve this, I declared a static inner class ‘MyRuntimeHints’ within the main class.

@SpringBootApplication
@ImportRuntimeHints(LdapServiceApplication.MyRuntimeHints.class)
public class LdapServiceApplication {

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

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}

@PostConstruct
void postConstruct() {
System.setProperty("javax.net.ssl.trustStore", System.getProperty("javax.net.ssl.trustStore"));
System.setProperty("javax.net.ssl.trustStorePassword", System.getProperty("javax.net.ssl.trustStorePassword"));
}

static class MyRuntimeHints implements RuntimeHintsRegistrar {

@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// Register serialization
hints.serialization().registerType(HashMap.class).registerType(LinkedList.class);
hints.reflection().registerType(TypeReference.of("javax.net.ssl.SSLSocketFactory"),
builder -> builder.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS));
hints.resources().registerPattern("db/migration/*.sql");
}

The class contains hints to register the HashMap and LinkedList class for deserialisation, which are used when fetching cached data from Redis. It also contains a hint to register the public methods of the SSLSocketFactory class, which I am using for a custom truststore to store CA certificates for the corporate AD server. The third hint is to let GraalVM know the location of my flyway scripts.

To import the hints, I used the annotation @ImportRuntimeHints(LdapServiceApplication.MyRuntimeHints.class). I also set the system property for dynamically loading the custom truststore and associated password in the PostConstruct method.

However, I had to comment out the dependencies related to Spring Cloud Kubernetes as it didn’t support AOT at the time of writing this article.

After adding these modifications, I built the native executable again and it worked smoothly, reducing the startup time by 80%. Woah !!

./target/ldap-service -Djavax.net.ssl.trustStore=/Users/abhi-codes/Downloads/truststore.jks -Djavax.net.ssl.trustStorePassword=changeit

Instead of building image and then running it to test your changes every single time, you can enable AOT processing like below. Refer https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html#native-image.testing.with-the-jvm for more details.

$ java -Dspring.aot.enabled=true -jar ldap-service.jar

In conclusion, converting a Spring Boot micro-service to a native image using GraalVM is a simple process that can bring significant performance benefits. With the built-in support in Spring Boot 3, it’s now easier than ever to create native images for your applications.

In the next article , I will walk you though how to build a native image using Dockerfile and deploy the docker image onto Kubernetes.

--

--

Abhishek Anand
CodeX
Writer for

Spring Boot Advocate | Senior Software Engineer @ Intuit | ex-VMware