Auto-updatable, self-contained CLI with Java 11

Photo by Joshua Sortino on Unsplash

Introduction

Over the course of the last 11 months, we have seen two major releases of Java — Java 9 and Java 10. Come September, we will get yet another release in the form of Java 11, all thanks to the new 6 month release train. Each new release introduces exciting features to assist the modern Java developer. Let’s take some of these features for a spin and build an auto-updatable, self-contained command line interface.

The minimum viable feature-set for our CLI is defined as follows:

  • Display the current bitcoin price index by calling the free coin desk API
  • Check for new updates and if available, auto update the CLI
  • Ship the CLI with a custom Java runtime image to make it self-contained

Prerequisites

To follow along, you will need a copy of JDK 11 early-access build. You will also need the latest version (4.9 at time of writing) of gradle. Of course, you can use your preferred way of building Java applications. Though not required, familiarity with JPMS and JLink can be helpful since we are going to use the module system to build a custom runtime image.

Off we go

We begin by creating a class that provides the latest bitcoin price index. Internally, it reads a configuration file to get the URL of the coin desk REST API and builds an http client to retrieve the latest price. This class makes use of the new fluent HTTP client classes that are part of “java.net.http” module.

var bpiRequest = HttpRequest.newBuilder()
.uri(new URI(config.getProperty("bpiURL")))
.GET()
.build();

var bpiApiClient = HttpClient.newHttpClient();

bpiApiClient
.sendAsync(bpiRequest,
HttpResponse.BodyHandlers.ofString())
.thenApply(response -> toJson(response))
.thenApply(bpiJson ->
bpiJson.getJsonObject("usd").getString("rate"));

Per Java standards, this code is actually very concise. We used the new fluent builders to create a GET request, call the API, convert the response into JSON, and pull the current bitcoin price in USD currency.

In order to build a modular jar and set us up to use “jlink”, we need to add a “module-info.java” file to specify the CLI’s dependencies on other modules.

module ud.bpi.cli {
requires java.net.http;
requires org.glassfish.java.json;
}

From the code snippet, we observe that our CLI module requires the http module shipped in Java 11 and an external JSON library.

Now, let’s turn our attention to implement an auto-updater class. This class should provide a couple of methods. One method to talk to a central repository and check for the availability of newer versions of the CLI and another method to download the latest version. The following snippet shows how easy it is to use the new HTTP client interfaces to download remote files.

CompletableFuture<Boolean> update(String downloadToFile) {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("http://localhost:8080/2.zip"))
.GET()
.build();

return HttpClient.newHttpClient()
.sendAsync(request, HttpResponse.BodyHandlers
.ofFile(Paths.get(downloadToFile)))
.thenApply(response -> {
unzip(response.body());
return true;
});

} catch (URISyntaxException ex) {
return CompletableFuture.failedFuture(ex);
}
}

The new predefined HTTP body handlers in Java 11 can convert a response body into common high-level Java objects. We used the HttpResponse.BodyHandlers.ofFile() method to download a zip file that contains the latest version of our CLI.

Let’s put these classes together by using a launcher class. It provides an entry point to our CLI and implements the application flow. Right when the application starts, this class calls its launch() method that will check for new updates.

void launch() {
var autoUpdater = new AutoUpdater();

try {

if (autoUpdater.check().get()) {
System.exit(autoUpdater.update().get() ? 100 : -1);
}

} catch (InterruptedException | ExecutionException ex) {
throw new RuntimeException(ex);
}
}

As you can see, if a new version of the CLI is available, we download the new version and exit the JVM by passing in a custom exit code 100. A simple wrapper script will check for this exit code and rerun the CLI.

#!/bin/sh
...
start
EXIT_STATUS=$?

if [ ${EXIT_STATUS} -eq 100 ]; then
start
fi

And finally, we will use “jlink” to create a runtime image that includes all the necessary pieces to execute our CLI. jlink is a new command line tool provided by Java that will look at the options passed to it to assemble and optimize a set of modules and their dependencies into a custom runtime image. In the process, it builds a custom JRE — thereby making our CLI self-contained.

jlink --module-path build/libs/:${JAVA_HOME}/jmods \
--add-modules ud.bpi.cli,org.glassfish.java.json \
--launcher bpi=ud.bpi.cli/ud.bpi.cli.Launcher \
--output images

Let’s look at the options that we passed to jlink:

  • “ module-path” tells jlink to look into the specified folders that contain java modules
  • “ add-modules” tells jlink which user-defined modules are to be included in the custom image
  • “launcher” is used to specify the name of the script that will be used to start our CLI and the full path to the class that contains the main method of the application
  • “output” is used to specify the folder name that holds the newly created self-contained custom image

When we run our first version of the CLI and there are no updates available, the CLI prints something like this:

Say we release a new version (2) of the CLI and push it to the central repo. Now, when you rerun the CLI, you will see something like this:

Voila! The application sees that a new version is available and auto-updates itself. It then restarts the CLI. As you can see, the new version adds an up/down arrow indicator to let the user know how well the bitcoin price index is doing.

Head over to GitHub to grab the source code and experiment with it.