With 81% of companies having a multi-cloud strategy planned or in the works, and 67% of corporate infrastructure being cloud-based, cloud computing has become a new norm. But everything comes at a price, and cloud resources are no exception. Despite minuscule prices for machines and cloud capacities, cloud bills can be fifty pages long. Why? One of the main reasons is heavy underperforming containers. They devour time, memory, and company’s money. The solution is to minimize the size of containers and at the same time optimize the performance. In this article, we will learn how to do that by building Java microservices with Liberica JDK and MicroProfile and creating microcontainers with jlink. Prepare your microscope, we are talking about tiny numbers here!
Table of Contents
Create a Java application with MicroProfile
Brief introduction to MicroProfile
MicroProfile is an open-source specification for building scalable and secure Java microservices. MicroProfile rests upon Jakarta EE standards, so it allows you to develop microservices without the need to define key components from scratch. In addition, MicroProfile evolves rapidly, which enables companies to take advantage of the newest technologies as soon as possible. And the open-source nature of MicroProfile eliminates vendor lock-in and makes it possible to create microservices using both MicroProfile and Jakarta EE features. Moreover, due to the loosely-coupled nature of microservices, you can develop them using different frameworks — MicroProfile, Spring, etc. Your opportunities are limitless.
Let’s get down to business and create a microservice using this robust tool!
Build a Java microservice with MicroProfile
To create and run our demo application, we will be using Liberica JDK, a progressive open-source Java runtime. Liberica JDK simplifies the creation and maintenance of microservices as it supports the widest range of platforms and configurations, including the Apple Silicon. Moreover, it is developed by a top-5 OpenJDK enterprise contributor and a member of the OpenJDK Vulnerability Group, so your applications are safe and sound at all times.
We will use the latest release of LTS Liberica JDK 17 for our project due to the jdeps bug, which was fixed in this version. You can download Liberica JDK 17 directly from BellSoft’s website or through a package manager.
Let’s start with building a simple Quarkus application with Maven. Quarkus is the MicroProfile implementation perfect for building Java microservices. It allows the developers to use the existing enterprise APIs and adjust them to their purposes. We have already seen Quarkus in action when we built native images with Liberica Native Image Kit. This time, the framework will help us explore MicroProfile.
In the Terminal, navigate to the folder you want to create your project in and run this command:
mvn io.quarkus.platform:quarkus-maven-plugin:2.7.3.Final:create \
-DprojectGroupId=mpdemo \
-DprojectArtifactId=jrushmp
Open the newly created application in your favorite IDE. If you look closely at the pom.xml file, you will see the dependencies for Quarkus, but not MicroProfile, so we need to add them manually:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-opentracing</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-metrics</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client</artifactId>
</dependency>
Navigate to the folder containing the pom.xml (in our case, it is jrushmp) and run
mvn compile quarkus:dev -Dquarkus.test.continuous-testing=disabled
This command will compile the service. Run
mvn package
And then
java -jar target/quarkus-app/quarkus-run.jar
As a result, you will get a loaded application with all the standard APIs.
You can now enhance and personalize your microservice. Go to the GreetingsResource class of your application and add the following code:
package mpdemo;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.faulttolerance.Retry;
import org.eclipse.microprofile.metrics.annotation.Metered;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
@Inject
@ConfigProperty(name = "message", defaultValue = "Hello from MicroProfile!")
String message;
@GET
@Retry
@Metered
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return this.message;
}
}
Note that the @Metered annotation enables you to track throughput/frequency data.
Run this again:
mvn package quarkus:dev -Dmaven.test.skip=true -Dquarkus.test.continuous-testing=disabled
Then you can check the correctness of the output by running
curl localhost:8080/hello
It should give you
Hello from MicroProfile!
You can input the following command to get the metrics of your application:
curl localhost:8080/q/metrics/application
That’s it! You now have a working Java microservice with MicroProfile APIs. You can create an application with MicroProfile only, and without being bound to a specific framework. This app will then work with any technology that implements MicroProfile. However, we used Quarkus to demonstrate that you can keep to the Jakarta EE standards and at the same time take advantage of the novelties offered by modern tools.
We can now proceed to containerizing our application and sending it to the cloud.
Create a microcontainer using jlink
We will now pack our app into a container and ship it to the cloud. Normally, we would create a Docker image by simply running
./mvnw package
and
docker build -f src/main/docker/Dockerfile.jvm -t quarkus/jrushmp-jvm .
However, a standard Docker image is heavy because it includes the whole Java Development Kit. For example, the size of our application containerized this way is approx. 457MB (the actual size may vary). But we can drastically reduce the size of our image by modularizing the microservice. Modularization is the process of dividing an application into modules, i.e. only necessary classes and dependencies get bundled. This means that you get a custom trimmed-down JRE in your container, thus minimizing its size and increasing the performance. And jlink is the tool made for cutting out a custom Java runtime image from a standard JDK by leaving only necessary modules for your application. Let’s see it in action!
First, we need to turn our application into a module system. For that purpose, we need to add several dependencies to the pom.xml file:
To begin with, we add a configuration for building an uber-jar:
<configuration>
<quarkus.package.uber-jar>true</quarkus.package.uber-jar>
</configuration>
We also add the maven-dependency-plugin, which collects all jar files into one directory (in our case, target/lib).
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<excludeArtifactIds>jboss-jaxb-api_2.3_spec</excludeArtifactIds>
</configuration>
</execution>
</executions>
</plugin>
Finally, we exclude the tests because we don’t need them for our purposes.
<exclusions>
<exclusion>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-common</artifactId>
</exclusion>
</exclusions>
Note that you will need to add the
<exclusions>
<exclusion>
<groupId>org.jboss.spec.javax.xml.bind</groupId>
<artifactId>jboss-jaxb-api_2.3_spec</artifactId>
</exclusion>
</exclusions>
section if you get the Error: Two versions of module java.xml.bind
. In this case, we exclude the external JBoss API and use Jakarta EE API.
Remember, you can use the mvn-jlink plugin to execute tools in the JDK/bin folder. If the plugin needs to make an image of a specific JDK, it downloads the required JDK distro from a provider, Liberica JDK among others.
The next step is to create an uber-jar. Don’t forget that you need to use the latest Maven version to perform this action with JDK 17.
From the project directory, run
JAVA_HOME="/home/User/java/jdk-17.0.4.1" ~/apache-maven-3.8.5/bin/mvn package -Dquarkus.package.type=uber-jar
Now, we need to use jdeps to analyze the modules we need with the following command
~/java/jdk-17.0.4.1/bin/jdeps --multi-release 11 -cp target/lib/*:target/quarkus-app/lib/boot/*:target/quarkus-app/lib/* --ignore-missing-deps --list-deps target/jrushmp-1.0.0-SNAPSHOT-runner.jar
which will give you the list of modules the application needs.
JDK removed internal API/com.sun.tools.javac.code
java.base/sun.security.x509
java.compiler
java.datatransfer
java.desktop
java.logging
java.management
java.naming
java.rmi
java.security.jgss
java.security.sasl
java.sql
java.transaction.xa
java.xml
jdk.compiler/com.sun.tools.javac.code
jdk.compiler/com.sun.tools.javac.tree
jdk.compiler/com.sun.tools.javac.util
jdk.management
jdk.unsupported
Note that the jdeps command will be slightly different for macOS users:
~/java/jdk-17.0.4.1/bin/jdeps --multi-release 11 -cp "target/lib/*:target/quarkus-app/lib/boot/*:target/quarkus-app/lib/*" --ignore-missing-deps --list-deps target/jrushmp-1.0.0-SNAPSHOT-runner.jar
Finally, let’s use jlink to cut out a custom JRE:
~/java/jdk-17.0.4.1/bin/jlink --compress 2 --strip-debug --no-header-files --no-man-pages --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.logging,java.management,java.naming,java.rmi,java.security.sasl,java.security.jgss,java.sql,java.transaction.xa,java.xml,jdk.compiler,jdk.management,jdk.unsupported,jdk.zipfs --output target/jlink-runtime
The size of jlink-runtime is 67MB. To put the application into a container, we need a Dockerfile to build an image with jlink and custom JRE.
We will use Liberica Runtime Container based on Liberica Lite and Alpaquita Linux, BellSoft’s lightweight Linux distro with remarkable performance characteristics.
FROM bellsoft/liberica-runtime-container:jdk-all-17-musl as builder
# Create custom JRE
RUN jlink --compress 2 --strip-java-debug-attributes --no-header-files --no-man-pages --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.logging,java.management,java.naming,java.rmi,java.security.sasl,java.security.jgss,java.sql,java.transaction.xa,java.xml,jdk.compiler,jdk.management,jdk.unsupported,jdk.zipfs --output /jlink-runtime
FROM bellsoft/alpaquita-linux-base:stream-musl
COPY --from=builder /jlink-runtime /jlink-runtime
COPY target/quarkus-app/lib/ /opt/quarkus-app/lib/
COPY target/quarkus-app/*.jar /opt/quarkus-app/
COPY target/quarkus-app/app/ /opt/quarkus-app/app/
COPY target/quarkus-app/quarkus/ /opt/quarkus-app/quarkus/
EXPOSE 8080
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENTRYPOINT ["/jlink-runtime/bin/java", "-jar", "/opt/quarkus-app/quarkus-run.jar"]
Copy the file into your project. You can now build a Docker image and run it:
docker build -f Dockerfile -t jlink:1.0 .
docker run -it --rm -p 8080:8080 jlink:1.0
If the COPY target/jlink-runtime/ /jlink-runtime/
command fails with “file not found error”, add !target/jlink-runtime/*
to the end of the file.
Check your Docker images by running docker images
.
The container size is 109MB, which is 4 times less than a Docker container image built by default!
Conclusion
In this article, we discovered the power of the jlink tool when it comes to creating small but powerful containers with Liberica JDK. Such containers require four times less cloud resources but don’t affect the performance of your application.
Great news is that you can now build your own microcontainers. Or use BellSoft’s ready-made solution for your application — Liberica Runtime Container. Choose the package that suits your needs, put your app into the container and you are good to go!
Discover Liberica Runtime Container
The article was inspired by Adam Bien's talk at JRush in December, where he explained how to use MicroProfile to built resilient Java microservices. Head over to the JRush page to listen to the full presentation.