Building Spring Boot Applications with GraalVM: Starter guide

Explore ways to boost app performance and cut resource usage. Compare traditional JVM and GraalVM methods

Building Spring Boot Applications with GraalVM: Starter guide
Standard JVM vs Graal VM racing

Hello everyone! In this article, I will show you how you can use Spring Boot with Graal VM.

Introduction

Let’s start with the reason why you need to scale in and scale out your microservices. The reason is the effective use of computational power. In this article, we will develop two versions of the Spring Boot service. The developers will build the first version as a Java Spring Boot service. They will create the second version with GraalVM.

Let’s roll

We will start with basic implementation and add more features later.

Let’s start with building standard Spring Boot, first, we need to declare dependencies in pom.xml or build.gradle.

<?xml version="1.0" encoding="UTF-8"?> 
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 
    <modelVersion>4.0.0</modelVersion> 
    <parent> 
        <groupId>org.springframework.boot</groupId> 
        <artifactId>spring-boot-starter-parent</artifactId> 
        <version>3.3.4</version> 
        <relativePath/> <!-- lookup parent from repository --> 
    </parent> 
    <groupId>me.vrnsky</groupId> 
    <artifactId>standard</artifactId> 
    <version>0.0.1-SNAPSHOT</version> 
    <properties> 
        <java.version>17</java.version> 
    </properties> 
    <dependencies> 
        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-starter-web</artifactId> 
        </dependency> 
        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-starter-actuator</artifactId> 
        </dependency> 
        <dependency> 
            <groupId>io.micrometer</groupId> 
            <artifactId>micrometer-registry-prometheus</artifactId> 
            <scope>runtime</scope> 
        </dependency> 
        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-starter-test</artifactId> 
            <scope>test</scope> 
        </dependency> 
    </dependencies> 
 
    <build> 
        <plugins> 
            <plugin> 
                <groupId>org.springframework.boot</groupId> 
                <artifactId>spring-boot-maven-plugin</artifactId> 
            </plugin> 
        </plugins> 
    </build> 
</project>

At the moment, we will create only the starter class and nothing else.

package me.vrnsky; 
 
import org.springframework.boot.SpringApplication; 
import org.springframework.boot.autoconfigure.SpringBootApplication; 
 
@SpringBootApplication 
public class StandardAppStarter { 
 
    public static void main(String[] args) { 
        SpringApplication.run(StandardAppStarter.class, args); 
    } 
}

Let’s compile and run our project.

mvn clean package 
java -jar target/standard-1.0.0-SNAPSHOT.jar

Observing the following log we can see the startup time of this application.

INFO 3183 --- [           main] me.vrnsky.StandarAppStarter              : No active profile set, falling back to 1 default profile: "default" 
INFO 3183 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http) 
INFO 3183 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat] 
INFO 3183 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.30] 
INFO 3183 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext 
INFO 3183 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 548 ms 
INFO 3183 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 1 endpoint beneath base path '/actuator' 
INFO 3183 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/' 
INFO 3183 --- [           main] me.vrnsky.StandarAppStarter              : Started StandarAppStarter in 1.066 seconds (process running for 1.312)

So the startup time for the non-Graal VM Spring Boot application is 1.066 seconds.

Before proceeding to GraalVM, let’s capture the building project time.

[INFO] The original artifact has been renamed to /Users/vrnsky/IdeaProjects/vrnsky/effective-utilization/standard/target/standard-0.0.1-SNAPSHOT.jar.original 
[INFO] ------------------------------------------------------------------------ 
[INFO] BUILD SUCCESS 
[INFO] ------------------------------------------------------------------------ 
[INFO] Total time:  1.040 s 
[INFO] Finished at: 2024-10-20T15:51:15+08:00 
[INFO] ------------------------------------------------------------------------

Now moving to Graal VM, we will stick with a simple implementation of Spring Boot first — just a starter class. In our pom.xml we have to mention the plugins required for building native artifacts.

<?xml version="1.0" encoding="UTF-8"?> 
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 
    <modelVersion>4.0.0</modelVersion> 
    <parent> 
        <groupId>org.springframework.boot</groupId> 
        <artifactId>spring-boot-starter-parent</artifactId> 
        <version>3.3.4</version> 
        <relativePath/> <!-- lookup parent from repository --> 
    </parent> 
    <groupId>me.vrnsky</groupId> 
    <artifactId>graal-vm</artifactId> 
    <version>0.0.1-SNAPSHOT</version> 
    <properties> 
        <java.version>17</java.version> 
    </properties> 
    <dependencies> 
        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-starter-web</artifactId> 
        </dependency> 
        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-starter-actuator</artifactId> 
        </dependency> 
        <dependency> 
            <groupId>io.micrometer</groupId> 
            <artifactId>micrometer-registry-prometheus</artifactId> 
            <scope>runtime</scope> 
        </dependency> 
        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-starter-test</artifactId> 
            <scope>test</scope> 
        </dependency> 
    </dependencies> 
 
    <build> 
        <plugins> 
            <plugin> 
                <groupId>org.graalvm.buildtools</groupId> 
                <artifactId>native-maven-plugin</artifactId> 
            </plugin> 
            <plugin> 
                <groupId>org.springframework.boot</groupId> 
                <artifactId>spring-boot-maven-plugin</artifactId> 
            </plugin> 
        </plugins> 
    </build> 
</project>

After that let’s build the project.

mvn compile:native -Pnative

The time of building Graal VM application takes a longer time.

Finished generating 'graal-vm' in 1m 35s. 
[INFO] ------------------------------------------------------------------------ 
[INFO] BUILD SUCCESS 
[INFO] ------------------------------------------------------------------------ 
[INFO] Total time:  01:40 min 
[INFO] Finished at: 2024-10-20T17:05:07+08:00 
[INFO] ------------------------------------------------------------------------

Now we can run our service with the following command.

./target/graal-vm

The app startup time is 0.066 seconds.

INFO 3425 --- [           main] me.vrnsky.GraalVMAppStarter              : Started GraalVMAppStarter in 0.066 seconds (process running for 0.086)

Now let’s build a Docker image for the Graal VM service. Since I’m using a Mac with ARM architecture, we’ll need a two-step Docker image. The first step will build our project, and the second step will build the final image.

FROM --platform=linux/amd64 findepi/graalvm:java17-all as builder 
WORKDIR /app 
RUN apt-get update && apt-get install -y maven 
COPY pom.xml . 
COPY src ./src 
RUN mvn clean native:compile -Pnative 
 
FROM --platform=linux/amd64 oraclelinux:9-slim 
WORKDIR /app 
COPY --from=builder /app/target/graal-vm . 
EXPOSE 8080 
CMD ["./graal-vm"]

The building of a Docker image also takes longer than the non-Graal VM Spring Boot application. On my Mac M1, the time required to build a Docker image is around 6–8 minutes.

Below, we provide the Docker image for the standard service.

FROM eclipse-temurin:17 
COPY target/standard-0.0.1-SNAPSHOT.jar /service.jar 
CMD java -jar service.jar 
EXPOSE 8080
Image by author

Conlusion

In this starter guide, we explored building Spring Boot applications using This starter guide explores building Spring Boot apps. We used both traditional JVM and GraalVM methods. GraalVM native images start up faster and use less memory. However, they take longer to build. This trade-off makes GraalVM particularly suitable for:

  1. Serverless environments where fast startup is crucial
  2. Microservices that need to scale rapidly
  3. Applications with memory constraints
  4. Container-based deployments where image size matters

However, the increased build time might impact development cycles, so teams should carefully consider their specific requirements when choosing between traditional Spring Boot and GraalVM native images.

References

  1. Spring Boot Native Documentation
  2. GraalVM Documentation
  3. Spring Boot Docker Documentation
  4. Native Image Build Report
  5. Docker Multi-stage Builds

Subscribe to Egor Voronianskii | Java Development and whatsoever

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe