jpackage Tutorial: Packaging Java Apps (Java 16)

Introduction Shipping a Java desktop or command-line application used to mean asking users to install a matching version of Java first, and then giving them a JAR file to run with java -jar. Since Java Web Start and the browser plugin were removed in Java 11Click to read: Java 11, there was no standard way to install Java applications at all. jpackage, incubated in Java 14 and made a standard tool in Java 16Click to read tutorial: Overview of New Features in Java 16 (JEP 392), fills this gap: it packages an application together with its own Java runtime, either as a self-contained folder or as a native installer - .msi or .exe on Windows, .pkg or .dmg on macOS, .deb or .rpm on Linux. This jpackage tutorial goes through it step by step: building an application image, making it smaller with a custom runtime, creating installers, and the most useful options. Step 1: The application jpackage works with an ordinary application in a JAR file. Our example prints which Java it is running on -
The application to package
//HelloApp.java
import java.nio.file.Path;

public class HelloApp {
  public static void main(String[] args) {
    Path javaHome = Path.of(System.getProperty("java.home"));
    System.out.println("Hello from a packaged app!");
    System.out.println("Running on Java " + Runtime.version().feature()
        + " from the folder: " + javaHome.getParent().getFileName() + "\\" + javaHome.getFileName());
  }
}
javac -d classes HelloApp.java
jar --create --file lib/app.jar --main-class HelloApp -C classes .
Step 2: Create an application image The simplest output type is an application image (--type app-image): a folder containing a native launcher, the application's JARs, and a Java runtime. It needs no extra tools, so it is the best way to start -
jpackage --type app-image --name HelloApp --input lib --main-jar app.jar --win-console --dest dist
  • --input lib is the folder with the application's JAR files - all of them are copied into the package - and --main-jar names the JAR whose manifest gives the main class. For a modular application, use --module-path and --module instead.
  • --win-console makes the Windows launcher a console program, so its output appears in the terminal. Without it, the launcher is a GUI program, which suits Swing and JavaFX applications.
  • --dest is where the output is written.
The image contains the launcher, an app folder with the JAR, and a runtime folder with a complete Java runtime -  Contents of dist\HelloApp
app
HelloApp.exe
runtime
Running the launcher runs the application on the bundled runtime, not on any Java installed on the machine -
dist\HelloApp\HelloApp.exe
 OUTPUT of the above command
Hello from a packaged app!
Running on Java 24 from the folder: HelloApp\runtime
The bundled runtime is taken from the JDK that runs jpackage - here a Java 24 JDK - so build your packages with the Java version you want to ship.
Step 3: Make it smaller with a custom runtime By default, jpackage bundles a large runtime with most of the JDK's modules, so even a "Hello" application is big -  Size of dist\HelloApp
152 MB
Thanks to the module systemClick to read: module system, you can bundle only the modules the application needs. jdeps lists them, jlink builds a runtime containing just those modules, and --runtime-image tells jpackage to use it -
jdeps --print-module-deps lib/app.jar
jlink --add-modules java.base --strip-debug --no-header-files --no-man-pages --output small-runtime
jpackage --type app-image --name HelloSmall --input lib --main-jar app.jar --win-console --runtime-image small-runtime --dest dist
 OUTPUT of jdeps
java.base
 Size of dist\HelloSmall
43 MB
Our application uses only java.base, so the package shrinks to a fraction of its size. A real application usually needs a few more modules, such as java.desktop for Swing or java.sql for JDBC - jdeps tells you which. If the application loads classes by reflection, check that the modules they come from are included too. Step 4: Create a native installer To produce an installer, change --type. The installer types depend on the operating system, and jpackage can only build installers for the system it runs on - so build Windows installers on Windows, and so on -
jpackage --type msi --name HelloApp --input lib --main-jar app.jar --win-menu --win-shortcut --dest installers
  • Windows - msi or exe. These need the free WiX Toolset to be installed. Options such as --win-menu, --win-shortcut and --win-dir-chooser add a Start menu entry, a desktop shortcut and a choice of installation folder.
  • macOS - pkg or dmg. Applications distributed outside the App Store should be signed (--mac-sign) and notarized.
  • Linux - deb or rpm, which need the dpkg or rpm tools.
Other useful options
  • --app-version, --vendor, --description, --copyright - metadata shown by the installer and the operating system.
  • --icon - the application's icon (.ico on Windows, .icns on macOS, .png on Linux).
  • --java-options - JVM options which the launcher always passes, for example --java-options "-Xmx512m".
  • --arguments - default arguments to the main method.
  • --file-associations - register the application to open certain file types.
  • @options.txt - read the options from a file, which keeps long jpackage commands manageable in a build script.
Build tools have plugins which run jpackage for you, such as the jpackage Maven plugins and the Badass JLink plugin for Gradle. Summary In this tutorial we saw how jpackage, standard since Java 16, packages a Java application with its own runtime so that users do not need to install Java. We created an application image with a console launcher and ran it on its bundled runtime, then used jdeps and jlink to build a custom runtime containing only the needed modules, which made the package much smaller. Lastly, we looked at creating native installers for each operating system, and at the most useful options. Next, read about the strong encapsulation of JDK internalsClick to read tutorial: Strong Encapsulation of JDK Internals in Java 16 and 17.