Building Small JREs and Docker Images with Recent Versions of OpenJDK – Part 1

1 Sep

How we can use small JREs in productive environments to keep our systems small and secure.

I originally published this article in German in my employer’s blog.

The recent version of AdoptOpenJDK and even the current LTS version do not distinguish between a development environment (Java Development Kit – JDK) and a Runtime Environment (Java Runtime Environment anymore – JRE) anymore. The first one did contain the compiler and debugging tools, the latter only the stuff that was necessary to run the application. Let’s explore how we can get back our JRE!

Motivation

Since the JRE vanished from the Download pages, I noticed that many new installations in a production environment are bundled with the full JDK. I think that’s kind of a clumsy solution which leads to two drawbacks: the installations are getting bigger. This also applies to docker containers. The JDK itself indeed got smaller over the time, but I think the argument still counts: I like small artifact on my development machine and in my Continuous Delivery pipeline.

The second drawback is weaker security: In an productive environment, I expect to find as few installed software as possible. On the one hand, this minimizes the potential number of security flaws. On the other hand, it makes it harder for an intruder to get deeper into the system or to escape from containers if debugging tools and compilers are not already provided.

In this article, I like to show you how you can build small JREs with the build-in JDK tools. We will explore the module- and the classpath. In the second part I will show how to build small Docker images using this technique. We’ll start with the most simple case: the new module path.

JREs with the Module Path

Let’s start with a very simple project which consists of a single Java class:

package de.jowisoftware.learning.jre; import java.net.URI; import java.net.http.*; import static java.net.http.HttpResponse.BodyHandlers.ofLines; public class Main { public static void main(String[] args) throws Exception { URI uri = new URI("https://www.jowisoftware.de"); System.out.printf("Running in module path? %s%n", Main.class.getModule().isNamed() ? "yes" : "no"); HttpClient client = HttpClient.newBuilder().build(); HttpRequest request = HttpRequest.newBuilder() .GET().uri(uri).build(); long lines = client.send(request, ofLines()) .body().count(); System.out.printf( "The website '%s' has %d lines of HTML code%n", uri, lines); } }

Let’s use Maven to compile the project:

?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.jowisoftware.learning</groupId> <artifactId>jre</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding> UTF-8 </project.build.sourceEncoding> </properties> <build> <finalName>app</finalName> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <release>13</release> </configuration> </plugin> <plugin> <artifactId>maven-jar-plugin</artifactId> <version>3.1.2</version> <configuration> <archive> <manifest> <mainClass> de.jowisoftware.learning.jre.Main </mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build> </project>

In this first part, we build the project as a module using the following module-info.java:

module jowisoftware.jre { requires java.net.http; }

Executing mvn package builds a file named app.jar in the target directory. We’re able to start the application with the usual java invocation:

$ java -p app.jar \ -m jowisoftware.jre/de.jowisoftware.learning.jre.Main Running in module path? yes The website 'https://www.jowisoftware.de' has 85 lines of HTML code

Now we can utilize jlink, which was introduced to derive a tailored JRE for a given java module. jlink only includes the java binary, our module and the module’s transitive dependencies:

$ jlink \ -p target/app.jar \ --add-modules jowisoftware.jre \ --output target/jlink1a \ --launcher \ app=jowisoftware.jre/de.jowisoftware.learning.jre.Main

The result gets even smaller if we throw away the man-pages or additional debug-information. That’s a decision each project has to make on its own:

$ jlink \ -p target/app.jar \ --add-modules jowisoftware.jre \ --output target/jlink1b \ --launcher app=jowisoftware.jre/de.jowisoftware.learning.jre.Main \ --no-header-files \ --no-man-pages \ --strip-debug \ --compress=2

The resulting directory is about 36 Megabytes. The app can be started by invoking target/jlink1b/bin/app. The command jimage list target/jlink1b/lib/modules tells us, which classes got in our JRE. The complete directory could now go into production.

JREs with the Class Path

To be honest, we started with the simplest situation: jlink works this way only for modules, so we just build one. It won’t work for the class path and it does not even work with automatic modules.

The reality looks a bit different: Spring (to give just one example) scans jar-files for annotated components on startup. It won’t even find a single jar file when we’re using the new jimage format, so Spring won’t work with this solution – at least not out of the box. Other projects simply aren’t modules yet. Do we still have to deliver the full JDK in these cases?

No! But it gets a little more complicated. For the next experiments, we start our application on the class path instead of using the module path:

$ java -jar app.jar Running in module path? no The website 'https://www.jowisoftware.de' has 85 lines of HTML code

Now we use a simple trick: jlink only works for modules. But nothing stops us from building a JRE which consists of a few modules (without the real application) and start that JRE with our custom class path later:

$ jlink \ --add-modules java.base \ --output target/jlink2a \ --no-header-files \ --no-man-pages \ --strip-debug \ --compress=2 $ cp target/app.jar target/jlink2a/app.jar $ target/jlink2a/java -jar target/jlink2a/app.jar Running in module Path? no Exception in thread "main" java.lang.NoClassDefFoundError: java/net/http/HttpClient at de.jowisoftware.learning.jre.Main.main(Main.java:14) Caused by: java.lang.ClassNotFoundException: java.net.http.HttpClient at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source) at java.base/java.lang.ClassLoader.loadClass(Unknown Source) ... 1 more

Ouch. This attempt failed. But let’s first talk about our „trick“: we used jlink to create a JRE which contains only the module java.base. We did not even create a launcher (but bin/java will still exists as binary in the bin directory). We then added our application into the JRE and started the provided java binary with the usual classpath parameters (in this case -jar, but -cp and a main class would have yielded the same result).

However, something went wrong: the class HttpClient is missing in our JRE! That’s logic! Because HttpClient is not part of java.base. A quick look into the documentation reveals that HttpClient is part of the module java.net.http (java --list-modules tells us which modules exists in general).

We could also use jshell to determine the module name:

$ jshell | Welcome to JShell -- Version 14.0.2 | For an introduction type: /help intro jshell> java.net.http.HttpClient.class.getModule().getName() $1 ==> "java.net.http" jshell>

Let’s try again (java.net.http depends on java.base. So in this case, the first --add-modules could be omitted):

$ jlink \ --add-modules java.base \ --add-modules java.net.http \ --output target/jlink2b \ --no-header-files \ --no-man-pages \ --strip-debug \ --compress=2 $ cp target/app.jar target/jlink2b/app.jar $ target/jlink2b/java -jar target/jlink2b/app.jar Running in module path? no The website 'https://www.jowisoftware.de.de' has 85 lines of HTML code

Voilá! If we know which modules are needed by our application, we can build our own JRE for this application. This works for the module path and the class path. If we don’t like to walk the extra mile, we can simply depend on the module java.se. This results in a JRE which is very similar to the „old“ JRE with all Java SE classes (i.e. the compiler is not included). This JRE can be then used independently of the concrete dependencies of the project:

$ jlink \ --add-modules java.se \ --output target/jlink3 \ --no-header-files \ --no-man-pages \ --strip-debug \ --compress=2

This result is 56 Megabytes big. This is way less than the 333 MiB of the JDK itself.

By the way: a simple Spring Boot application with Spring Web MVC requires java.naming, java.desktop, java.management, java.security.jgss and java.instrument.

In contrast to the module path, this solution requires some fine tuning. However, I think that the effort is reasonable and the tool support is really great. In the end, every project has to make its own decision about the worth of a small and clean Java installation.

Summary

Older Java versions had JDKs and JREs for download. In the newer versions, there are no JREs anymore. However, jlink allows us to generate exactly the JRE we need. So there is not one JRE but many JREs – depending on the required modules. If our application is fully modularized, jlink is very easy to use. In all other cases, the developer must either list the required modules by hand or use java.se as dependency.

The result is a very small and tailored Java Runtime Edition which lacks compilers and debugging tools and is suitable to be installed in production.

In the next post, we’ll have a look at small docker images.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.