Lesson 67Phase 8: Tools of the trade
Packaging and shipping a Java app
Turning code into something others can run: JAR files, the module system, jlink runtimes and jpackage installers.
By BasithUpdated 45 min
- JAR
- manifest
- executable JAR
- module-info.java
- jlink
- jpackage
So far you have run your programs from an editor, from source with java Main.java, or through Maven. None of that works for someone who just wants to use your program. They need one thing they can run, ideally without installing anything first. This lesson packages a small shop program four ways, from a single JAR file to a native app with its own copy of Java inside, and shows what each one asks of the person running it.
- Make a JAR that runs with
java -jar, and read its manifest. - Explain why a JAR that uses a library fails on its own, and build a fat JAR that carries the library inside.
- Turn a program into a module with
module-info.java, and run it from the module path. - Build a small Java runtime for one program with
jlink, and an app withjpackage. - Choose how to ship a program, depending on who will run it.
jar tool and put it on the classpath. Maven and Gradle made one with mvn package. Logging added a library, Logback, that has to travel with the program. This lesson starts from there.The program is one class in a package. It logs a line, then greets whoever is named on the command line:
package com.example.shop; import java.util.logging.Logger; public class Main { private static final Logger log = Logger.getLogger(Main.class.getName()); public static void main(String[] args) { log.info("Shop started"); String customer = args.length > 0 ? args[0] : "friend"; System.out.println("Welcome to the shop, " + customer + "!"); }}A JAR that runs with java -jar#
Compile it into a folder and pack the folder into a JAR, as in packages and imports. Then try the command that is meant to run a JAR:
$ javac -d out src/com/example/shop/Main.java$ jar --create --file shop.jar -C out .$ java -jar shop.jarno main manifest attribute, in shop.jarjava -jar needs to know which class has the mainmethod, and nothing has told it. That information lives in the JAR's manifest, a small text file at META-INF/MANIFEST.MF inside every JAR. The --main-class option writes it:
$ jar --create --file shop.jar --main-class com.example.shop.Main -C out .$ java -jar shop.jar AdaOct 19, 2026 3:47:29 PM com.example.shop.Main mainINFO: Shop startedWelcome to the shop, Ada!$ unzip -p shop.jar META-INF/MANIFEST.MFManifest-Version: 1.0Created-By: 25.0.4.1 (Eclipse Adoptium)Main-Class: com.example.shop.MainNow it runs, and Ada, the word after the JAR's name, arrives in args[0]. A JAR is a zip file, so unzip -p can print a file from inside it; jar --list --file shop.jar lists everything it holds. The Main-Class line is the whole trick. The finished JAR is 1,492 bytes, and anyone with Java 25 or newer can run it.
When the program uses a library#
The shop from the logging lesson uses Logback. Maven can write the Main-Class line too, with a setting for the JAR plugin in pom.xml:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.5.1</version> <configuration> <archive> <manifest> <mainClass>com.example.shop.Main</mainClass> </manifest> </archive> </configuration></plugin>$ mvn -q package$ java -jar target/shop-1.0-SNAPSHOT.jarException in thread "main" java.lang.NoClassDefFoundError: org/slf4j/LoggerFactory at com.example.shop.Main.<clinit>(Main.java:7)Caused by: java.lang.ClassNotFoundException: org.slf4j.LoggerFactory ...The JAR starts, and then cannot find SLF4J. mvn packageputs only your own classes in the JAR; the libraries stay in Maven's folder on your computer, where nobody else has them. (The <clinit> in the trace is the class's static setup, which is where the static final Loggerfield is created.) There are two ways out: ship the library JARs next to yours and list them in the manifest's Class-Path line, or put everything into one JAR.
One JAR with everything: a fat JAR#
A fat JAR(also called an uber JAR) holds your classes and the classes of every library you use. Maven's Shade plugin builds one during package:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.6.2</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions></plugin>$ mvn package...[INFO] --- shade:3.6.2:shade (default) @ shop ---[WARNING] Discovered module-info.class. Shading will break its strong encapsulation....[INFO] Replacing original artifact with shaded artifact.[INFO] BUILD SUCCESS$ java -jar target/shop-1.0-SNAPSHOT.jar15:41:02.147 [main] INFO com.example.shop.Main -- Opening the shop15:41:02.152 [main] INFO com.example.shop.Main -- Loaded 1234 books from library.txt...It works, and one file is all you send. The price is size: the shaded JAR is 1,010,253 bytes, against 2,930 for the shop's own classes, because Logback and SLF4J are inside. The warnings say that files with the same name came from several JARs (each has a manifest, for example) and only one copy was kept, which is harmless here. This is how many command-line tools and servers are shipped: one JAR, started with java -jar on any machine that has Java.
Key ideaA JAR runs with java -jar when its manifest names the main class, and runs anywhere only when every class it needs travels with it.
Modules: module-info.java#
Since Java 9, the JDK itself is split into modules: named groups of packages that say which other modules they need and which of their packages others may use. Java 25 has 69 of them. java.base holds java.lang, java.util and the other basics; java.logging holds java.util.logging; java.sql holds JDBC. Your program can be a module too, by adding a file called module-info.java next to the top package folder:
module com.example.shop {}$ javac -d mods/com.example.shop src/module-info.java src/com/example/shop/Main.javasrc/com/example/shop/Main.java:3: error: package java.util.logging is not visibleimport java.util.logging.Logger; ^ (package java.util.logging is declared in module java.logging, but module com.example.shop does not read it)1 errorInside a module, nothing outside java.base is visible until you ask for it. That is the point: the module states, in one place, everything it depends on. Add the request with requires, and run the program from the module path, which is to modules what the classpath is to loose classes:
module com.example.shop { requires java.logging;}$ javac -d mods/com.example.shop src/module-info.java src/com/example/shop/Main.java$ java --module-path mods --module com.example.shop/com.example.shop.Main AdaOct 19, 2026 3:47:29 PM com.example.shop.Main mainINFO: Shop startedWelcome to the shop, Ada!--module com.example.shop/com.example.shop.Main names the module, a slash, then the main class. A module can also say which of its own packages other modules may use, with exports com.example.shop.api;. A package it does not export stays private to the module, even to code that has its JAR: public then means public inside the module only. A library can finally keep its inner workings hidden.
jlink: a Java runtime made for one program#
Every way so far needs Java already installed on the other person's computer, and the right version of it. jlink, a tool in the JDK, builds a runtime that contains only the modules a program needs, ready to copy anywhere with the same operating system:
$ jlink --module-path mods --add-modules com.example.shop \ --launcher shop=com.example.shop/com.example.shop.Main \ --strip-debug --no-header-files --no-man-pages --output shop-runtime$ shop-runtime/bin/java --list-modulescom.example.shopjava.base@25.0.4.1java.logging@25.0.4.1$ shop-runtime/bin/shop AdaOct 19, 2026 3:47:30 PM com.example.shop.Main mainINFO: Shop startedWelcome to the shop, Ada!$ du -sh shop-runtime 45M shop-runtime--add-modules com.example.shopstarts from the shop module, andjlinkfollows itsrequireslines to find the rest. The runtime has three modules instead of 69.--launcher shop=...adds a small script,bin/shop, that starts the program, so nobody has to type the long--moduleoption.--strip-debug,--no-header-filesand--no-man-pagesleave out what only developers need.
The result is 45 megabytes, against 294 for the whole JDK it came from, and adding --compress=zip-9 brought it down to 32. Copy the folder to another Mac and shop-runtime/bin/shop runs there with no Java installed. It only runs on the kind of system it was built on: a runtime made on a Mac does not run on Windows, so you build one per system.
jpackage: an app people can open#
jpackage, also in the JDK, wraps a runtime like that into an application in the form the operating system expects. With --type app-image it makes the application itself:
$ jpackage --type app-image --name Shop --runtime-image shop-runtime \ --module com.example.shop/com.example.shop.Main --dest dist$ ls distShop.app$ dist/Shop.app/Contents/MacOS/Shop AdaOct 19, 2026 3:47:30 PM com.example.shop.Main mainINFO: Shop startedWelcome to the shop, Ada!On a Mac that is Shop.app, 45 megabytes, the runtime plus a starter. Other types make an installer instead: --type dmg or pkg on macOS, msi or exe on Windows (which needs the free WiX toolset installed first), and deb or rpm on Linux. Each one is built on the system it is for. Options such as --icon, --app-version and --vendor fill in what the installer shows. For a program with a window, such as a JavaFX app, this is how it reaches people who have never heard of Java.
Which one should you ship?#
| Form | Size here | The person running it needs | Good for |
|---|---|---|---|
A JAR, java -jar | 1.5 KB | Java installed, and every library JAR | A program with no libraries, for people who have Java |
| A fat JAR | 1 MB | Java installed | Command-line tools and servers |
A jlink runtime | 45 MB (32 compressed) | Nothing, on the same kind of system | Handing a program to someone without Java |
A jpackage app or installer | 45 MB and up | Nothing: they open it like any app | Desktop programs for ordinary users |
| A container image | Varies | A container runtime such as Docker | Servers and web services, in a later lesson |
Web services, which the next phase builds, are usually shipped as the last kind, a container image holding a fat JAR and a runtime. That is the subject of Docker and GitHub Actions.
tool.jar with java -jar tool.jar and gets no main manifest attribute, in tool.jar. What is missing?module-info.java says only requires java.sql;, and you build it with jlink. Which modules end up in the runtime?Try it
Answer each one in your head or on paper first, then open the answer.
java -jar app.jar fails with NoClassDefFoundError: com/google/gson/Gson. Explain why, and give two fixes.requires java.logging; in module-info.java change, both when you compile and when you run jlink?jlink on your Mac and send the folder to a friend who uses Windows. What happens, and what would you do instead?Practice
Work through these on your own. Each one builds on this page.
java -jar library.jar. Check the manifest with unzip -p, then run it from a different folder and make sure it still finds (or creates) library.txt where you expect.module-info.java to the same program. Work out which requires lines it needs by compiling and reading javac's errors, then build a runtime with jlink and compare its size with and without --compress=zip-9.java --list-modules, then java --describe-module java.sql. Which modules does java.sql require, and which packages does it export? Explain how that output decides what jlink puts in a runtime for a program that uses JDBC.Further reading#
- The jar tool, on dev.java
- Modules, the dev.java tutorial
- jlink, from the JDK 25 tool reference
- Packaging overview, from the JDK 25 jpackage guide
Short guides on this topic
Quick answers to what people search for about it, with code to run.
Spotted a mistake, or a part that did not make sense? Report it by email