Navigation
FIDA Blog
Knowledge - Success Stories - Whitepaper
newspaper Overview chevron_right Software Development chevron_right Blog chevron_right Cross-industry
SerPhoto
Blog

Fast, lean, cloud-capable: Java Native Build with Quarkus

Quarkus is now one of the most popular Java frameworks for modern cloud and container environments. It impresses with its lean API, strong extensibility and excellent documentation. The native build feature, which allows applications to be translated into high-performance, compact binaries, is particularly exciting - ideal for use in the cloud. In this article, we provide a compact overview: We explain the underlying technologies, show how to integrate the native build into an existing Quarkus application, what to look out for - and finally provide metrics that prove the potential of this feature.

What is a native build and what do we expect from it?

Native build refers to a compilation profile that results in a native executable that can be executed without the Java Virtual Machine (JVM) and therefore does not require a separate JDK installation. For this purpose, the existing JVM bytecode is translated directly into machine code using a special application called Mandrel.

The idea of having code executed directly by the operating system instead of having it compiled by the JVM each time is not entirely new, and is already used by the JVM itself by means of just-in-time compilation (JIT). An acceleration of the individual routines is not to be expected. Nevertheless, we are hoping for gains, particularly in terms of application startup and memory consumption, two issues that are particularly important in the cloud sector. Of course, this could also be achieved using C++ or modern low-level languages such as Rust or Go, but on the one hand the frameworks and libraries in the Java environment are much more mature, and on the other hand we can draw on the large pool of Java developers and have to train fewer people.

Mandrel or GraalVM?

In the context of native builds, the name "GraalVM" is often used interchangeably with "Mandrel". In fact, these are two distributions of the same software: GraalVM uses the Oracle JDK as a basis, while Mandrel is based on the OpenJDK. We will always refer to Mandrel in the following, as this variant is supplied with the Quarkus container images, among other things. In general, however, both packages are interchangeable in use, just as the JDK versions are.

How to create a native build?

Native executables are usually Linux executables, and since cross-compilation is not provided for, Linux users have a home advantage. But Windows is also well catered for thanks to the Windows Subsystem for Linux (WSL). However, compiling on the host system is not really useful, as a lot of additional packages are required, which clutter up the system, if they can be installed at all. Certain WSL setups in particular, e.g. if they are controlled by Rancher or Docker Desktop, are designed for minimal functionality and are unsuitable for this process. It is therefore better to use container mode. However, caution is required here, as the necessary flag name depends on whether the Docker daemon is addressed locally or remotely. The latter is particularly the case with Windows hosts, as Docker runs there either in WSL or a virtual machine and is therefore considered remote. With our Maven version (3.9.6), we also encountered the problem that the flag parameter -D used in the Quarkus documentation did not get along with the flag name, which is why we recommend using the long version --define instead:

java code

What is actually the problem?

You would think that there should be no difference, since containers should encapsulate the entire compilation process. But somehow our source code (or the JAR file built in the intermediate step) has to reach the compiler, and this is exactly where the crux lies: local containers can simply include it via a volume, while remote containers receivethe data via docker cp. However, the latter creates additional versioning layers, which should be avoided for local containers.

Docker images

Native Build is even able to produce Docker images in addition to the pure executable. All you need to do is set the quarkus.container-image.build flag to true. If the project was created with the Quarkus CLI tool, you will also receive ready-made Dockerfiles. These can be used directly for building by executing docker build -f src/main/docker/Dockerfile.native, which gives you better control over the image name.

What about Windows?

In general, it is possible to create native Windows executables, so-called PE files. Unfortunately, there are various obstacles in our way. Firstly, you need Visual Studio Community 2022 with the C++ workload, as the simple build tools do not include a C++ compiler. Then you have to either run the build process in an environment console provided by VSC or activateit via a batch call in the file %MANDREL_HOME%/bin/native-image.cmd :

call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"

Finally, in most cases the antivirus will be given a good talking to. It does not like the fact that executables are copied to the user temp and executed during the build:

From our point of view, this effort is not worthwhile, as Windows has no relevance as a target platform. Linux is primarily used in the cloud sector in particular.

What needs to be considered in terms of code?

In general, if it is possible to build a standalone JAR file that runs on a virgin JDK installation, you can turn it into a native executable. Artefacts that produce pure JVM bytecode are packed directly into the final JAR file in Quarkus, and since this is used for the native build, everything important is already there. Nevertheless, there are a few small pitfalls.

SSL

Encryption has become an integral part of the modern web. No website today can do without SSL, not to mention APIs. This makes it all the more frustrating that SSL is not activated by default. You first have to enter the following line in applications.properties for it to work:

quarkus.ssl.native=true

There are in fact a whole series of Quarkus extensions that set this flag, but you should not rely on them.

Reflection

In contrast to the JVM, a general reflection is no longer possible with the native build, as Mandrel removes unused code paths. Unfortunately, reflection is used extensively, e.g. for serialization and ORMs such as Hibernate. Fortunately, Quarkus offers a way to register classes for reflection, the appropriately named annotation @RegisterForReflection . This can either be applied to the corresponding classes or using the targets parameter, which is particularly helpful for classes from external artifacts:

reflections

Dependency injection

Dependency injection does not cause any problems as long as it takes place in the main code, but if services are to be loaded from an artifact, they must be registered in applications.properties:

quarkus.index-dependency.kafka-quickstart-models.group-id=en.fida

quarkus.index-dependency.kafka-quickstart-models.artifact-id=kafka-quickstart-models

This problem also occurs with JVM builds and is not a native-specific problem, but in all cases it produces opaque error messages that could be wrongly attributed to the native build.

AWS Lambda

An additional artifact must be entered in the pom.xml for AWS Lambda:

This artifact offers additional plugins that hook into the build process and make the result more pleasing for AWS Lambda. Instead of a pure executable, a function.zip is created here, which can be uploaded directly in the AWS Control Panel. A handler class must also be created:

TestLambda.java

It is also necessary to store the handler in the application.properties:

application.properties

quarkus.lambda.handler=test

These adjustments are not specific to Native Build and are also required for JVM-based Lambdas.

What needs to be considered during execution?

While the executable can run on any modern Linux, encapsulation using Docker is better in most cases. Deployment is also child's play thanks to the image built above. A few more settings are required for AWS Lambda. Firstly, the runtime must be changed to "Amazon Linux 2023", as it is a regular Linux executable and no JVM is required. Secondly, the environment variable DISABLESIGNALHANDLERS must be set to true to fix some incompatibilities between Quarkus and the AWS Lambda Custom Runtime environment. The function.zip can then simply be uploaded for deployment.

What about the benefits?

As a test case, we have come up with a simple application that generates n random UUIDs and sorts them, and is controlled via a simple REST endpoint or a Lambda.

The first great strength of native builds becomes apparent at the beginning: the reduced start time. The native build of the Lambda variant starts consistently in a quarter of the time required by the JVM build. With the REST variant, the divergence is even greater, due to the less optimized JVM compared to AWS.

Start time

Rest

Startzeit

AWS Lambda

Init

There are also advantages in terms of execution times, at least as long as the application is still "cold". Once warmed up, the JVM can keep up with the native builds again thanks to JIT compilation.

Execution times

Rest

Erzeugung
Sortierung

AWS Lambda

Erzeugung
Sortierung

Now we come to the big disadvantage of native builds: the build time. Due to the way Mandrel works, the source code is compiled twice: first to JVM bytecode, which is then compiled to native bytecode in the second step. This extends the build time to several minutes even for our simple examples. Fortunately, this step only has to be carried out once per deployment, which is manageable in most situations.

Aren't there measurements missing?

Even the most inattentive reader should have noticed that not all measurements were carried out for every test case. This can simply be explained by the great similarity of the cases: the locally tested services behave like a warm lambda after startup, both in terms of memory consumption and execution time. Deployment on Kubernetes, for example, would not change this, as services once deployed outside the cloud environment are not shut down. This happens with commercial cloud providers, as many more users share the hardware that actually exists.

On the other hand, the Lambda test cases do not differ in complexity from the local cases when building, so no surprises are to be expected in the build time.

Conclusion

The use of Quarkus for Java Native Builds offers considerable advantages, especially in modern cloud and container environments. The compact and high-performance execution of applications without a JVM increases efficiency, especially in terms of startup times and memory consumption. Despite the longer build time and some technical challenges, the advantages outweigh the disadvantages for many use cases, especially in the cloud area. Would you like to learn more about the possibilities and advantages of Java Native Builds with Quarkus? Contact us at FIDA to make your projects more efficient and future-proof.

About the Author

Benjamin Kleiner ist Backend-Entwickler bei der FIDA und sorgt dafür, dass im Hintergrund alles reibungslos funktioniert. Mit Expertise in Java, OpenAPI, SQL, Kafka und AWS Lambda schafft er das stabile Rückgrat moderner Webanwendungen. Seine Leidenschaft für Accessibility und gutes UX treibt ihn an, benutzerfreundliche und leistungsfähige Lösungen zu entwickeln.