Using jlink to Build Java Runtimes for non-Modular Applications

The most important new feature of JDK 9 was the Java Platform Module System (JPMS) that divided the monolithic rt.jar and tools.jar files into 75 distinct modules (at least in the OpenJDK, the Oracle JDK contained a few more).

JDK 9 also eliminated the distinction between the Java Development Kit (JDK) and the Java Runtime Environment (JRE). In the past, there was a sub-directory in the JDK (unsurprisingly called jre) which only contained things that were required to run a Java application. On Linux, the full JDK 8 was 364 Mb, the JRE just 197 Mb. Users who were concerned about disk space could install the JRE and happily run their applications.

In JDK 9 and later, the idea is to build Java runtimes that are tailored to the requirements of a specific application. Rather than including all 75 modules, you need only include the java.base module (which all runtimes must include by definition) as well as any other modules the application references. All transitive module dependencies must also be included. JDK 9 and later provides the jlink command to assemble and optimize a set of modules and their dependencies into a custom runtime image.

Many people, when moving an application from JDK 8 or earlier to JDK 11, do not want to have to take on the additional work of migrating application code to use modules. Fortunately, application code and external libraries can all be left on the classpath and treated as part of the unnamed module. For simple applications, you can use the same command to start the application as before.

However, the assumption is that, because jlink only works with modules that it can’t be used to generate a runtime image for non-module based applications.

This article will take you through the steps necessary to build significantly smaller Java runtimes for applications that have not been converted to use modules. In addition, since JavaFX has been removed from the Oracle JDK (and was never part of OpenJDK binaries), we’ll include that in our application to demonstrate another part of using this tool.

The application we’ll be using is a simple data-entry application that presents a form to the user and then stores the fields into a MySQL database. How the application works is immaterial to this post so I won’t bother sharing the code.

The application was built using NetBeans, which generates a dist directory that contains all the compiled code required to run the application. Since the application uses some libraries the layout looks like this:

Using JavaFX with JDK 11

To run the application on JDK 8, all that was required was

java -jar FlightRecorder.jar

If we run this application with JDK 9, we need to make no changes.

However, if we try this with JDK 11, we get the following error message:

Error: Could not find or load main class flightrecorder.Main
Caused by: java.lang.NoClassDefFoundError: javafx/application/Application

As I mentioned earlier, this is because neither the Oracle JDK nor OpenJDK binaries include JavaFX. Luckily, the nice people at Gluon provide free builds of the OpenJFX project, so it’s easy to resolve this issue.

To start, we need to download the JavaFX SDK and unzip it into a directory on our machine. For simplicity, we’ll put it in /opt. Next, we need a couple of extra command line flags:

java --module-path /opt/javafx-sdk-11/lib --add-modules=javafx.controls -jar FlightRecorder.jar

The two extra flags are:

  1. --module-path: Equivalent of the classpath, telling the JVM where to search for modules.
  2. --add-modules: This specifies additional root modules to resolve. Because all the application code is on the classpath, we don’t have an initial module, so this option is required. We can specify this in two ways, either with an explicit list of modules as above, or we could indicate that everything on the module path is to be included by using the parameter ALL-MODULE-PATH.

We now have an application that runs on JDK 11 using external JavaFX modules but with all application code still on the classpath.

Building a Java Runtime

In order to use jlink in this situation, we need to generate a list of the JDK modules that the application uses. To do this, we can use the jdeps command that also comes with the JDK.

Interestingly, if we use the JDK 9 version of jdeps on the application we get this result:

> jdeps --list-deps FlightRecorder.jar
java.base
java.logging
java.sql
javafx.base
javafx.controls
javafx.graphics
not found
unnamed module: FlightRecorder.jar

Most of this is fine but the ‘not found’ message is confusing and not very useful (what was not found?) Thankfully, jdeps was improved in JDK 10 so if we use the JDK 11 version we get the following:

> jdeps --list-deps FlightRecorder.jar
java.base
java.logging
java.sql

Although this tells us which modules are used by the application, we’re missing some information. Remember, we’re providing the JavaFX modules separately so we need to analyse any JDK module dependencies they might have. The solution is to include the module details in the same way we did for the Java launcher:

> jdeps --module-path /opt/javafx-sdk-11/lib --add-modules=javafx.controls --list-deps FlightRecorder.jar
JDK removed internal API/com.sun.media.jfxmediaimpl.platform.ios
java.base
java.datatransfer
java.desktop/java.awt.dnd.peer
java.desktop/sun.awt
java.desktop/sun.awt.dnd
java.desktop/sun.swing
java.logging
java.scripting
java.sql
java.xml
jdk.jsobject
jdk.unsupported
jdk.unsupported.desktop
jdk.xml.dom

I find this output a bit odd, as it includes more than just the module dependencies (which is what we asked for). In the case of (only) the java.desktop module we also get some extra information about the packages used. I also find the first message about the JDK removed internal somewhat confusing.

In theory, if we want an output that can be used directly in jlink, we can use the--print-module-deps flag instead of --list-dependencies. However, when I tried this on both MacOS X and Linux, I got a NullPointerException. Having posted a Tweet about this it has been submitted as a bug and will hopefully be fixed soon (although it’s a P3 so it may be a while)

Now we are ready to use jlink to produce a runtime for the application.

> jlink --no-header-files --no-man-pages --compress=2 --strip-debug --add-modules java.datatransfer,java.desktop,java.logging,java.scripting,java.sql,java.xml,jdk.jsobject,jdk.unsupported,jdk.unsupported.desktop,jdk.xml.dom — output java-runtime

To keep the size of the runtime as small as possible, we use command line flags to suppress the inclusion of header files and man pages as well as stripping the debug information and compressing to zip file format where relevant. The list of modules to include is the one generated by jdeps.

At this point, you would be forgiven for thinking that we can now run the application using our jlink generated runtime. However, if we try this here is the output:

> java-runtime/bin/java --module-path /opt/javafx-sdk-11/lib --add-modules=javafx.controls -jar FlightRecorder.jar
SEVERE: Error connecting to database
SEVERE: java.lang.NoClassDefFoundError: javax/naming/RefAddr

This is because the application uses a MySQL JDBC driver packaged in the lib/mysql-connector-java-5.1.23-bin.jar file. Because this (and the two other jar files in the lib directory) are accessed via the classpath, they are not automatically analysed by jdeps. The jdeps command does allow you to specify a classpath via the -cp flag. Unfortunately, jdeps does not appear to use this to reference jar files that are required by the application. It is, therefore, necessary to make sure that all jar files used by the application are passed as arguments:

> jdeps --module-path /opt/javafx-sdk-11/lib --add-modules=javafx.controls --list-deps FlightRecorder.jar lib/*
JDK removed internal API/com.sun.media.jfxmediaimpl.platform.ios
java.base
java.datatransfer
java.desktop/java.awt.dnd.peer
java.desktop/sun.awt
java.desktop/sun.awt.dnd
java.desktop/sun.swing
java.logging
java.management
java.naming
java.rmi
java.scripting
java.sql
java.transaction.xa
java.xml
jdk.jsobject
jdk.unsupported
jdk.unsupported.desktop
jdk.xml.dom

As you can see, we need to include a few more modules in our runtime. The command for jlink becomes:

jlink --no-header-files --no-man-pages --add-modules java.datatransfer,java.desktop,java.logging,java.management,java.naming,java.rmi,java.scripting,java.sql,java.transaction.xa,java.xml,jdk.jsobject,jdk.unsupported,jdk.unsupported.desktop,jdk.xml.dom --output java-runtime

The application now runs without any problems using the generated runtime. On my Mac, the java-runtime directory is a very respectable 39Mb, compared to 286Mb for the full JDK.

To simplify redistribution, we can copy the dist directory with the application jar files into the java-runtime directory, create a simple script to execute Java with all the appropriate command line flags and we are all good to go.

Why not try it with your applications?