Learn Java

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 Updated 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.

By the end of this lesson
  • 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 with jpackage.
  • Choose how to ship a program, depending on who will run it.
From before
Packages and imports made a JAR with the 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:

src/com/example/shop/Main.java
read along
1package com.example.shop;
2
3import java.util.logging.Logger;
4
5public class Main {
6 private static final Logger log = Logger.getLogger(Main.class.getName());
7
8 public static void main(String[] args) {
9 log.info("Shop started");
10 String customer = args.length > 0 ? args[0] : "friend";
11 System.out.println("Welcome to the shop, " + customer + "!");
12 }
13}

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:

Terminal
read along
1$ javac -d out src/com/example/shop/Main.java
2$ jar --create --file shop.jar -C out .
3$ java -jar shop.jar
4no main manifest attribute, in shop.jar

java -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:

Terminal
read along
1$ jar --create --file shop.jar --main-class com.example.shop.Main -C out .
2$ java -jar shop.jar Ada
3Oct 19, 2026 3:47:29 PM com.example.shop.Main main
4INFO: Shop started
5Welcome to the shop, Ada!
6$ unzip -p shop.jar META-INF/MANIFEST.MF
7Manifest-Version: 1.0
8Created-By: 25.0.4.1 (Eclipse Adoptium)
9Main-Class: com.example.shop.Main

Now 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:

pom.xml (inside <build><plugins>)
read along
1<plugin>
2 <groupId>org.apache.maven.plugins</groupId>
3 <artifactId>maven-jar-plugin</artifactId>
4 <version>3.5.1</version>
5 <configuration>
6 <archive>
7 <manifest>
8 <mainClass>com.example.shop.Main</mainClass>
9 </manifest>
10 </archive>
11 </configuration>
12</plugin>
Terminal
read along
1$ mvn -q package
2$ java -jar target/shop-1.0-SNAPSHOT.jar
3Exception in thread "main" java.lang.NoClassDefFoundError: org/slf4j/LoggerFactory
4 at com.example.shop.Main.<clinit>(Main.java:7)
5Caused by: java.lang.ClassNotFoundException: org.slf4j.LoggerFactory
6 ...

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:

pom.xml (another plugin, next to the JAR plugin)
read along
1<plugin>
2 <groupId>org.apache.maven.plugins</groupId>
3 <artifactId>maven-shade-plugin</artifactId>
4 <version>3.6.2</version>
5 <executions>
6 <execution>
7 <phase>package</phase>
8 <goals>
9 <goal>shade</goal>
10 </goals>
11 </execution>
12 </executions>
13</plugin>
Terminal
read along
1$ mvn package
2...
3[INFO] --- shade:3.6.2:shade (default) @ shop ---
4[WARNING] Discovered module-info.class. Shading will break its strong encapsulation.
5...
6[INFO] Replacing original artifact with shaded artifact.
7[INFO] BUILD SUCCESS
8$ java -jar target/shop-1.0-SNAPSHOT.jar
915:41:02.147 [main] INFO com.example.shop.Main -- Opening the shop
1015:41:02.152 [main] INFO com.example.shop.Main -- Loaded 1234 books from library.txt
11...

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 idea

A 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:

src/module-info.java (first try)
read along
1module com.example.shop {
2}
Terminal
read along
1$ javac -d mods/com.example.shop src/module-info.java src/com/example/shop/Main.java
2src/com/example/shop/Main.java:3: error: package java.util.logging is not visible
3import java.util.logging.Logger;
4 ^
5 (package java.util.logging is declared in module java.logging, but module com.example.shop does not read it)
61 error

Inside 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:

src/module-info.java
read along
1module com.example.shop {
2 requires java.logging;
3}
Terminal
read along
1$ javac -d mods/com.example.shop src/module-info.java src/com/example/shop/Main.java
2$ java --module-path mods --module com.example.shop/com.example.shop.Main Ada
3Oct 19, 2026 3:47:29 PM com.example.shop.Main main
4INFO: Shop started
5Welcome 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.

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:

Terminal
read along
1$ jlink --module-path mods --add-modules com.example.shop \
2 --launcher shop=com.example.shop/com.example.shop.Main \
3 --strip-debug --no-header-files --no-man-pages --output shop-runtime
4$ shop-runtime/bin/java --list-modules
5com.example.shop
6java.base@25.0.4.1
7java.logging@25.0.4.1
8$ shop-runtime/bin/shop Ada
9Oct 19, 2026 3:47:30 PM com.example.shop.Main main
10INFO: Shop started
11Welcome to the shop, Ada!
12$ du -sh shop-runtime
13 45M shop-runtime
  • --add-modules com.example.shop starts from the shop module, and jlink follows its requires lines 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 --module option.
  • --strip-debug, --no-header-files and --no-man-pages leave 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:

Terminal (macOS)
read along
1$ jpackage --type app-image --name Shop --runtime-image shop-runtime \
2 --module com.example.shop/com.example.shop.Main --dest dist
3$ ls dist
4Shop.app
5$ dist/Shop.app/Contents/MacOS/Shop Ada
6Oct 19, 2026 3:47:30 PM com.example.shop.Main main
7INFO: Shop started
8Welcome 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?#

FormSize hereThe person running it needsGood for
A JAR, java -jar1.5 KBJava installed, and every library JARA program with no libraries, for people who have Java
A fat JAR1 MBJava installedCommand-line tools and servers
A jlink runtime45 MB (32 compressed)Nothing, on the same kind of systemHanding a program to someone without Java
A jpackage app or installer45 MB and upNothing: they open it like any appDesktop programs for ordinary users
A container imageVariesA container runtime such as DockerServers 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.

Quick check
A friend runs your tool.jar with java -jar tool.jar and gets no main manifest attribute, in tool.jar. What is missing?
Quick check
Your program's 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.

1
Your JAR runs on your computer from inside your editor, but on a friend's computer java -jar app.jar fails with NoClassDefFoundError: com/google/gson/Gson. Explain why, and give two fixes.
2
What does requires java.logging; in module-info.java change, both when you compile and when you run jlink?
3
You built a runtime with 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.

1
Take the library manager from the console app project and package it as a JAR that runs with 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.
2
Add a 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.
3
Run 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#

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