posts

Creating Java microcontainers with MicroProfile, jlink, and Liberica JDK

figure
Nov 3, 2022
Dmitry Chuyko

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!

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.

Discover Liberica JDK

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.

Further reading

Are you looking for a unified solution for your cloud-natove Java apps? Discover Alpaquita Cloud Native Platform

Experiencing issues with containers? Find out about side effects of containerization

Curious to know about other useful Java features apart from jlink? Read about eight features you should start using now

posts
Alpaquita vs Alpine: a head-to-head comparison
figure
Nov 10, 2022
Dmitry Chuyko
shorts
Critical vulnerabilities in OpenSSL 3.0
Nov 11, 2022
Sergey Chernyshev

Find out about the newest CVEs discovered in OpenSSL 3.0 and how to eliminate the risk of exploits

Subcribe to our newsletter

figure

Read the industry news, receive solutions to your problems, and find the ways to save money.