Creating a Spring Boot 3 Native Image via Jersey: Boosting Performance and Efficiency

Tauseef Ameen
7 min readJul 19, 2023
Spring Boot Native along with GraalVM

Introduction:
In the ever-evolving world of Java development, optimizing the performance and efficiency of our applications is a constant pursuit. With the introduction of Spring Boot 3 and its native image support, developers now have an even more powerful tool at their disposal. In this article, we’ll explore the process of creating a Spring Boot 3 native image using the popular Jersey framework. By leveraging the power of ahead-of-time (AOT) compilation and GraalVM, we can unlock significant performance gains and reduce the memory footprint of our applications. So, let’s dive in and discover how we can take our Spring Boot applications to the next level with native image generation.

There are two main ways to build a Spring Boot native image application:

  • Using Spring Boot support for Cloud Native Buildpacks to generate a lightweight container containing a native executable.
  • Using GraalVM Native Build Tools to generate a native executable.

This article is limited to only first way i.e., we will build native image via build pack. I will write another article on second way soon.

The easiest way to start a new native Spring Boot project is to go to start.spring.io, add the “GraalVM Native Support” and “Jersey” dependency and generate the project. The included HELP.md file will provide getting started hints. Your dash board will look like this :

Click on Generate and open this project in your favorite IDE. I will use IntelliJ.

Spring Boot includes buildpack support for native images directly for Maven . This means we can just type a single command and quickly get a sensible image into our locally running Docker daemon. The resulting image doesn’t contain a JVM, instead the native image is compiled statically. This leads to smaller images. There are 3 types of buildpack image available for use:
1. paketobuildpacks/builder:tiny
2. paketobuildpacks/builder:base
3. paketobuildpacks/builder:full
In this article we will use paketobuildpacks/builder:tinyas it has small footprint and reduced attack surface, but you can also use paketobuildpacks/builder:base or paketobuildpacks/builder:full to have more tools available in the image if required.

System Requirements
Docker should be installed and running.

Create native image
The spring-boot-starter-parent declares a native profile that configures the executions that need to run in order to create a native image. GraalVM Native Support dependency generate this in pom. You can activate profiles using the -P flag on the command line. This dependency looks like this

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

Note: If you don’t want to use spring-boot-starter-parent you’ll need to configure executions for the process-aot goal from Spring Boot’s plugin and the add-reachability-metadata goal from the Native Build Tools plugin.

To build the image, we can run the spring-boot:build-image goal with the native profile active:

$ mvn -Pnative spring-boot:build-image

Now, sit back and relax this build take some time and it depends on your system. It took around 4:30 min for this very small project.

Build Time for this project

As we can see docker image is created, lets run it and check start up time

Start up time is 68 ms

Let’s start same application in traditional way:

It took around ~2226 ms

Indeed, there is huge difference in start up time .

Now, Let’s try to add endpoint in this project and then check behavior.
I added these two end point in class Resource and register it via Jersey Config.

@Path("/test")
@Component
public class Resource {
@GET
@Path("/int")
@Produces({ MediaType.APPLICATION_JSON })
public int testInteger() {return 2+2;}

@GET
@Path("/string")
@Produces({ MediaType.APPLICATION_JSON })
public String testString() {
return "Hello from jersey";
}
@Component
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
register(Resource.class);
}
}

Let’s create native image with these changes and try to hit endpoints and observe behavior.
Maven Native Image creation is successful, and application starts successfully too, but on the first request to sample end point (test/int), there seems to be an error. Error is :
java.lang.NoClassDefFoundError: Could not initialize class org.glassfish.jersey.server.internal.inject.FormParamValueParamProvider$MultipartFormParamValueProvider

2023-06-11T18:47:29.652Z ERROR 1 --- [nio-8080-exec-3] o.a.c.c.C.[Tomcat].[localhost].[/]       : Servlet.init() for servlet [com.example.demo.JerseyConfig] threw exception

java.lang.NoClassDefFoundError: Could not initialize class org.glassfish.jersey.server.internal.inject.FormParamValueParamProvider$MultipartFormParamValueProvider
at org.glassfish.jersey.server.internal.inject.FormParamValueParamProvider.<init>(FormParamValueParamProvider.java:82) ~[na:na]
at org.glassfish.jersey.server.internal.inject.ValueParamProviderConfigurator.init(ValueParamProviderConfigurator.java:90) ~[na:na]
at org.glassfish.jersey.server.ApplicationHandler.lambda$initialize$0(ApplicationHandler.java:307) ~[na:na]

Error is crystal clear, AOT generated image is not able to find a class in jersey package at runtime.

There are 2 ways to solve this error :
1. Using the tracing agent by generating hint in json format
2. By Customizing hint in your java Class

Using the Tracing Agent

Use the following command to launch the application with the native image tracing agent attached:

 java -agentlib:native-image-agent=config-output-dir=target/ -jar native-0.0.1-SNAPSHOT.jar 

Now you can exercise the code paths you want to have hints for and then stop the application with ctrl-c.
Now, hint is generated at path mentioned above target/target

Hint is generated

On application shutdown the native image tracing agent will write the hint files to the given config output directory. You can either manually inspect these files, or use them as input to the native image build process. To use them as input, copy them into the src/main/resources/META-INF/native-image/ directory. The next time you build the native image, GraalVM will take these files into consideration.
Let’s move these file to src/main/resources/META-INF/native-image folder.

Files are copied to META-INF/native-image folder

Now, re-create docker image and ceheck if hints are picked by GraalVM.
Surprisingly, this time it took ~10 minute to create docker image.

After hitting curl command on both endpoint, we got output witrh no error,
So, in this way we can avoid such error and ask GraalVM to explicitly include classes which are no part of default creation of native image.

Now, run docker image

docker run --rm -p 8080:8080 native:0.0.2-SNAPSHOT                                                                    

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.0)

2023-06-11T19:37:51.779Z INFO 1 --- [ main] com.example.demo.NativeApplication : Starting AOT-processed NativeApplication using Java 17.0.7 with PID 1 (/workspace/com.
example.demo.NativeApplication started by cnb in /workspace)
2023-06-11T19:37:51.780Z INFO 1 --- [ main] com.example.demo.NativeApplication : No active profile set, falling back to 1 default profile: "default"
2023-06-11T19:37:51.816Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2023-06-11T19:37:51.817Z INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-06-11T19:37:51.817Z INFO 1 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.8]
2023-06-11T19:37:51.833Z INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-06-11T19:37:51.833Z INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 53 ms
2023-06-11T19:37:51.897Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-06-11T19:37:51.898Z INFO 1 --- [ main] com.example.demo.NativeApplication : Started NativeApplication in 0.151 seconds (process running for 0.17)

Container is up and application is started.

Let’s hit both endpoint

Both end point is able to return response without any error

Conclusion:

The performance and memory usage implications are hard to ignore. As the community embraces Spring’s newfound AOT support, I think the inertia required to move to AOT will become easier and easier to obtain in the next days, weeks, months, and years.

When you use Native Image to build native executables it only includes the elements reachable from your application entry point, its dependent libraries, and JDK classes discovered through static analysis. However, the reachability of some elements (such as classes, methods, or fields) may not be discoverable due to Java’s dynamic features including reflection, resource access, dynamic proxies, and serialization. If an element is not reachable, it is not included in the generated executable at build time, which can lead to failures at run time. Native Image has built-in metadata for JDK classes but user code and dependencies may use dynamic features of Java that are undiscoverable by the Native Image analysis. For this reason, Native Image accepts additional reachability metadata in the form of JSON files. Since this metadata is specific to a specific code base, the JSON files providing the corresponding metadata can be shared for libraries and frameworks. This repository is a centralized place for sharing such files for libraries and frameworks that do not provide built-in metadata yet. It is also used to retrofit metadata for older versions of libraries and frameworks.

GraalVM native image technology could be a very powerful way to build applications better suited for production. GraalVM native images save costs and help you build more reliable systems. In addition, Spring’s AOT engine helps you introduce new possibilities, like serverless, embedded, and infrastructure, for which Spring may not have been considered ideal in the past.

--

--

Tauseef Ameen
0 Followers

Java Developer | ASML | NETHERLANDS