Uber JARs with Shade & Assembly

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Maven 3.10.0 with the Apache Maven Shade Plugin 3.6.2, the Apache Maven Assembly Plugin 3.8.0 and the Apache Maven JAR Plugin 3.5.1. Every example was run on JDK 26 on 2026-10-16 in scratch projects that mimic the tidepool build (parent POM io.tidepool:tidepool-parent with the modules tidepool-model, tidepool-engine, tidepool-cli and, new on this page, tidepool-dist). Maven 3.9.16 and Maven 4.0.0-rc-7 (a release candidate) were run where the page says so. Paths in the console output were shortened and long listings were filtered with grep, which is said each time. The tidepool command prints a number with the default locale of the JVM, so every run of it on this page used -Duser.language=en -Duser.country=US (through JAVA_OPTS for the launcher script). Plugin versions are the stable releases found on Maven Central on 2026-10-16. Nothing was published anywhere; artifacts were only installed into a scratch local repository.

A JAR built by Maven contains only the classes and resources of its own module. The libraries it needs are not inside, they are listed as dependencies in its POM, and whoever runs the program must supply them on the classpath. That is exactly what a Maven consumer does, but it is awkward for a command-line tool, an application server deployment or a download for a user who has never heard of Maven. This page covers the two plugins that solve it: the Shade plugin, which builds one JAR containing your classes and the classes of your dependencies (an uber JAR, also called fat JAR or shaded JAR), and the Assembly plugin, which builds an archive (ZIP, TAR, JAR) laid out as a descriptor says, which may be a fat JAR but is usually a distribution with scripts, libraries and data. The plain JAR and its manifest are on Packaging JARs, WARs & Manifests; this page starts where that one ends.

The problem, and three ways to solve it

The tidepool-cli module produces a thin JAR. Running it shows the two classic symptoms: without a Main-Class entry the JVM refuses to start it, and with the main class named on the command line it cannot find the first library it needs.

java -jar tidepool-cli/target/tidepool-cli-1.0.0-SNAPSHOT.jar data/stations.csv SF
java -cp tidepool-cli/target/tidepool-cli-1.0.0-SNAPSHOT.jar io.tidepool.cli.Main data/stations.csv SF
no main manifest attribute, in tidepool-cli/target/tidepool-cli-1.0.0-SNAPSHOT.jar
Exception in thread "main" java.lang.NoClassDefFoundError: picocli/CommandLine
	at io.tidepool.cli.Main.main(Main.java:36)
Caused by: java.lang.ClassNotFoundException: picocli.CommandLine

(The two commands were run one after the other; the output is in the same order.) There are three ways to fix it, and they differ in what you hand to the user:

Approach What you ship Typical use

Thin JAR plus a classpath

The JAR with Class-Path manifest entries and a directory of libraries beside it (maven-jar-plugin with addClasspath, maven-dependency-plugin:copy-dependencies); see Packaging JARs, WARs & Manifests

Internal tools; the libraries stay untouched and individually replaceable

Uber JAR with shade

One JAR with every class and resource merged, optionally rewritten (relocated, minimised)

java -jar tools, plugins loaded into a host with its own classpath (relocation avoids clashes), cloud functions

Distribution with assembly

An archive with a layout you design: bin/ scripts, lib/ JARs, data/, documentation, permissions

Applications installed on a server or downloaded by users; it can also produce an uber JAR

Two ways to ship tidepool-cli with its dependencies: the shade goal merges the resolved runtime classpath into one rewritten JAR, the assembly single goal lays the same JARs out in an archive with bin, lib and data directories

Both plugins read the resolved runtime classpath of the module they run in, both normally run in the package phase (the phases are on Build Lifecycle) and both can attach what they build as an additional artifact, so that install and deploy publish it. Ant does it with the jar and zipfileset tasks (Packaging Archives) and Gradle with the application distribution and third-party plugins; see Java Projects.

The Shade plugin

An executable JAR in one execution

The shade:shade goal takes the module’s JAR, adds the classes and resources of every dependency in the runtime classpath, and writes the result. It is bound to the package phase in an <execution>, so that mvn package builds it. The only configuration needed for an executable JAR is the Main-Class entry, which the ManifestResourceTransformer writes. The plugin version is pinned (see Maven Plugins & Their Configuration on why every plugin version must be pinned) in the pluginManagement of the parent POM, as it is for the rest of this page.

tidepool-cli/pom.xml
  <build>
    <plugins>
      <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>
            <configuration>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>io.tidepool.cli.Main</mainClass>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
mvn -B -DskipTests -pl tidepool-cli package

The part of the output that is specific to the plugin (the lines about the repository downloads and the build summary were left out):

[INFO] Dependency-reduced POM written at: ./tidepool-cli/dependency-reduced-pom.xml
[WARNING] commons-codec-1.19.0.jar, commons-csv-1.14.1.jar, commons-io-2.20.0.jar, picocli-4.7.7.jar, tidepool-cli-1.0.0-SNAPSHOT.jar, tidepool-engine-1.0.0-SNAPSHOT.jar, tidepool-model-1.0.0-SNAPSHOT.jar define 1 overlapping resource:
[WARNING]   - META-INF/MANIFEST.MF
[WARNING] commons-codec-1.19.0.jar, commons-csv-1.14.1.jar, commons-io-2.20.0.jar define 2 overlapping resources:
[WARNING]   - META-INF/LICENSE.txt
[WARNING]   - META-INF/NOTICE.txt
[WARNING] commons-codec-1.19.0.jar, commons-csv-1.14.1.jar, commons-io-2.20.0.jar, picocli-4.7.7.jar define 1 overlapping classes:
[WARNING]   - META-INF.versions.9.module-info
[WARNING] maven-shade-plugin has detected that some files are
[WARNING] present in two or more JARs. When this happens, only one
[WARNING] single version of the file is copied to the uber jar.
[WARNING] Usually this is not harmful and you can skip these warnings,
[WARNING] otherwise try to manually exclude artifacts based on
[WARNING] mvn dependency:tree -Ddetail=true and the above output.
[WARNING] See https://maven.apache.org/plugins/maven-shade-plugin/
[INFO] Replacing original artifact with shaded artifact.

The JAR now starts, and its manifest has the new entry:

java -Duser.language=en -Duser.country=US -jar tidepool-cli/target/tidepool-cli-1.0.0-SNAPSHOT.jar data/stations.csv SF
unzip -p tidepool-cli/target/tidepool-cli-1.0.0-SNAPSHOT.jar META-INF/MANIFEST.MF
San Francisco: -0.869 m
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.5.1
Java-Version: 21
Build-Jdk-Spec: 26
Main-Class: io.tidepool.cli.Main

The JAR grew from 3,899 bytes to 1,413,222 bytes and 947 entries (the plugin kept the original, thin one as original-tidepool-cli-1.0.0-SNAPSHOT.jar in target/). The messages are worth reading, because they are the plugin’s way of saying what it had to decide for you:

  • Overlapping resources and classes. Two or more of the merged JARs contain an entry with the same path. Only one copy goes into the uber JAR and the others are dropped. META-INF/MANIFEST.MF is harmless here, the plugin writes its own; LICENSE.txt and NOTICE.txt are a legal matter (see the transformers below); and META-INF.versions.9.module-info is the multi-release module-info.class of three Commons libraries and Picocli, which is meaningless in a merged JAR (and so is excluded in the full configuration below).

  • Replacing original artifact with shaded artifact. By default the shaded JAR replaces the project’s main artifact, so install and deploy publish the uber JAR under the plain coordinates. The alternative is to attach it with a classifier, covered a few sections down.

  • Dependency-reduced POM written at …​. The plugin also wrote a POM next to the module’s pom.xml; it too has a section below.

What the goal can do

mvn shade:help -Ddetail -Dgoal=shade lists every parameter of the goal. These are the ones of the 3.6.2 plugin, grouped by what they do (filtered with grep from the output; the descriptions are the plugin’s):

Parameters Purpose

transformers, filters, relocations, artifactSet

What goes into the JAR and how it is rewritten: resource transformers, include/exclude filters per artifact, class relocations, and the set of dependencies to include at all (artifactSet with includes and excludes of groupId:artifactId patterns).

minimizeJar, entryPoints

Remove the classes of dependencies that nothing reaches (see below); entryPoints names classes that must be kept.

shadedArtifactAttached, shadedClassifierName (default shaded), shadedArtifactId, finalName, outputFile, outputDirectory

Replace the main artifact or attach another one, and name the result.

createDependencyReducedPom (default true), dependencyReducedPomLocation, generateUniqueDependencyReducedPom, useDependencyReducedPomInJar, promoteTransitiveDependencies, keepDependenciesWithProvidedScope

The dependency-reduced POM.

createSourcesJar, createTestSourcesJar, shadeSourcesContent, shadeTestJar, extraJars, extraArtifacts

Shade the sources JAR, the test JAR, or additional JARs.

skip, useBaseVersion, shaderHint

Skip the goal, use 1.0.0-SNAPSHOT rather than a timestamped snapshot version in the name, pick another shader implementation.

Resource transformers

Merging JARs is not only copying classes. Many libraries have resource files that exist, with the same name, in several of them, and where copying "one of them" silently breaks the program. A resource transformer handles one kind of such file: it sees every entry with a matching name from every JAR, combines them and writes the result. The transformers of the 3.6.2 plugin (all in org.apache.maven.plugins.shade.resource):

Transformer What it does

ManifestResourceTransformer

Sets manifest entries: mainClass and manifestEntries (any other entry, for example Multi-Release)

ServicesResourceTransformer

Merges every META-INF/services/<interface> file into one (the Java ServiceLoader registry)

AppendingTransformer

Appends the contents of every matching resource into one file (resource names it)

XmlAppendingTransformer

Same for XML files, merging them into one document

ResourceBundleAppendingTransformer

Merges .properties resource bundles of the same name

PropertiesTransformer, OpenWebBeansPropertiesTransformer, MicroprofileConfigTransformer

Merge properties files with ordinals (configuration of OpenWebBeans and MicroProfile Config)

ComponentsXmlResourceTransformer, PluginXmlResourceTransformer

Merge Plexus components.xml and Maven plugin.xml

GroovyResourceTransformer

Merges Groovy extension-module descriptors

ApacheLicenseResourceTransformer, ApacheNoticeResourceTransformer

Prevent duplicate licence files; merge the NOTICE files

IncludeResourceTransformer

Adds a file from the project into the JAR (resource, file)

DontIncludeResourceTransformer

Drops resources matching a pattern

The service registry problem

ServiceLoader finds implementations through a file META-INF/services/<fully-qualified interface name>. When two libraries each provide an implementation of the same interface, both JARs contain a file with the same path, and merging "one of them" loses an implementation. A small reactor reproduces it: greeter-api declares the interface Greeter, greeter-en and greeter-es each implement it and register the implementation in their own services file, and greeter-app depends on both and loads them with ServiceLoader. First, greeter-app is shaded with only the manifest transformer.

greeter-app/pom.xml (build section)
  <build>
    <plugins>
      <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>
            <configuration>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>io.tidepool.demo.app.Main</mainClass>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
mvn -B package
java -jar greeter-app/target/greeter-app-1.0.0.jar
unzip -p greeter-app/target/greeter-app-1.0.0.jar META-INF/services/io.tidepool.demo.Greeter
[WARNING] greeter-en-1.0.0.jar, greeter-es-1.0.0.jar define 1 overlapping resource:
[WARNING]   - META-INF/services/io.tidepool.demo.Greeter
...
EnGreeter: Hello, tidepool!
io.tidepool.demo.en.EnGreeter

(Warning lines for the manifest and the closing advice of the plugin were replaced by ....) The build succeeded and printed only a warning, and the program silently lost the Spanish greeter: the services file of greeter-en won and the one of greeter-es was dropped. Adding the ServicesResourceTransformer inside <transformers> fixes it:

              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>io.tidepool.demo.app.Main</mainClass>
                </transformer>
              </transformers>
EnGreeter: Hello, tidepool!
EsGreeter: Hola, tidepool!
io.tidepool.demo.en.EnGreeter
io.tidepool.demo.es.EsGreeter

The warning about the services file is gone, because the transformer now claims that resource. The same problem exists with the Spring spring.factories files, reference.conf of Akka and Typesafe Config, and any other "registry" that libraries extend: when a shaded program works in the IDE and loses a feature in the JAR, look for an overlapping resource in the build output first.

Licences, notices and multi-release JARs

Commons Codec, CSV and IO each carry META-INF/LICENSE.txt and META-INF/NOTICE.txt. The Apache licence asks that the notices of all included works are preserved, and plain shading keeps one arbitrary copy. The ApacheNoticeResourceTransformer merges the NOTICE files into one META-INF/NOTICE (with addHeader set to false to omit the generated header line) and the ApacheLicenseResourceTransformer removes the duplicate licence files. In the full configuration below the merged notice began like this:

Apache Commons Codec
Copyright 2002-2025 The Apache Software Foundation

This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).

Apache Commons CSV
Copyright 2005-2025 The Apache Software Foundation
...

Note that the ApacheLicenseResourceTransformer dropped all the LICENSE files from the JAR in this build, so the licence text of your own project (and of the libraries, where their licences require a copy) must be added back, for example with the IncludeResourceTransformer. Whether a licence permits shading at all is for you to check; see Supply Chain Security for the wider picture.

A multi-release JAR (JEP 238) keeps version-specific classes under META-INF/versions/<n>/; the JVM uses them only if the manifest has Multi-Release: true. A shaded JAR does not inherit that entry from its dependencies. Bouncy Castle’s bcprov is multi-release; with manifestEntries the entry is written (the shaded JAR contained 168 entries under META-INF/versions/):

                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>demo.Main</mainClass>
                  <manifestEntries>
                    <Multi-Release>true</Multi-Release>
                  </manifestEntries>
                </transformer>
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.5.1
Java-Version: 21
Build-Jdk-Spec: 26
Main-Class: demo.Main
Multi-Release: true

Filters and signed JARs

A filter includes or excludes entries of the merged JARs. Its <artifact> is a groupId:artifactId pattern (*:* for all) and it holds <includes> and <excludes> with entry paths (wildcard patterns). The classic use is excluding signature files. A signed JAR carries META-INF/*.SF, *.DSA or *.RSA entries with the digests of its own classes; once its classes are merged into another JAR the digests no longer match, and the JVM refuses the result. A minimal project depends on Bouncy Castle (org.bouncycastle:bcprov-jdk18on:1.86, a signed JAR) and is shaded with only the manifest transformer:

mvn -B clean package
java -jar target/signed-1.0.0.jar
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.SecurityException: Invalid signature file digest for Manifest main attributes
	at java.base/sun.security.util.SignatureFileVerifier.processImpl(SignatureFileVerifier.java:339)
	at java.base/sun.security.util.SignatureFileVerifier.process(SignatureFileVerifier.java:281)
	at java.base/java.util.jar.JarVerifier.processEntry(JarVerifier.java:323)

The build succeeded; the failure appears only at run time. A filter on all artifacts removes the signature files:

              <filters>
                <filter>
                  <artifact>*:*</artifact>
                  <excludes>
                    <exclude>META-INF/*.SF</exclude>
                    <exclude>META-INF/*.DSA</exclude>
                    <exclude>META-INF/*.RSA</exclude>
                  </excludes>
                </filter>
              </filters>
provider BC

(In this JAR the signature files were named BCRSA204.SF and BCRSA204.RSA, which is why a pattern is needed; the .EC extension is used by some signers.) The same mechanism removes one unwanted class or resource of one library: a filter with the <artifact> of that library and the entry in <excludes>. Selecting an inclusive subset (<includes>) is the other face, and is also how a class is protected from minimizeJar, below.

Relocating classes

Uber JARs have a second classic problem: two versions of the same library. If tidepool is loaded into a host that has Commons IO 2.5 on its classpath, whichever version loads first wins for everybody, and a method the host’s version lacks throws NoSuchMethodError at the worst moment. Relocation moves the classes of a dependency to another package, org.apache.commons to io.tidepool.shaded.org.apache.commons here, and rewrites every reference to them, in the dependency’s own classes and in yours. The consumer then has a private copy, invisible to everything else. A relocation has a pattern (a package prefix), a shadedPattern, and optional includes and excludes (classes of the package to relocate or to leave alone).

The final configuration of tidepool-cli, with every feature of this section in it. It attaches the shaded JAR with the classifier all and does not write a dependency-reduced POM (the reasons are in the sections below):

tidepool-cli/pom.xml
  <build>
    <plugins>
      <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>
            <configuration>
              <createDependencyReducedPom>false</createDependencyReducedPom>
              <shadedArtifactAttached>true</shadedArtifactAttached>
              <shadedClassifierName>all</shadedClassifierName>
              <relocations>
                <relocation>
                  <pattern>org.apache.commons</pattern>
                  <shadedPattern>io.tidepool.shaded.org.apache.commons</shadedPattern>
                </relocation>
                <relocation>
                  <pattern>picocli</pattern>
                  <shadedPattern>io.tidepool.shaded.picocli</shadedPattern>
                </relocation>
              </relocations>
              <filters>
                <filter>
                  <artifact>*:*</artifact>
                  <excludes>
                    <exclude>META-INF/*.SF</exclude>
                    <exclude>META-INF/*.DSA</exclude>
                    <exclude>META-INF/*.RSA</exclude>
                    <exclude>META-INF/versions/9/module-info.class</exclude>
                  </excludes>
                </filter>
              </filters>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer"/>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer">
                  <addHeader>false</addHeader>
                </transformer>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>io.tidepool.cli.Main</mainClass>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
mvn -B -DskipTests package
java -Duser.language=en -Duser.country=US -jar tidepool-cli/target/tidepool-cli-1.0.0-SNAPSHOT-all.jar data/stations.csv HNL
javap -cp tidepool-cli/target/tidepool-cli-1.0.0-SNAPSHOT-all.jar -c io.tidepool.cli.Main | grep -m2 "io/tidepool/shaded"
Honolulu: -0.112 m
         0: new           #21                 // class io/tidepool/shaded/picocli/CommandLine
        11: invokespecial #180                // Method io/tidepool/shaded/picocli/CommandLine."<init>":(Ljava/lang/Object;)V

The compiled Main now calls io.tidepool.shaded.picocli.CommandLine, so even Picocli, which uses reflection on annotated classes, still works. Relocation also rewrites string constants that look like a class name: a class that calls Class.forName("org.apache.commons.codec.digest.DigestUtils") and is shaded with the pattern org.apache.commons.codec printed loaded io.tidepool.shaded.codec.digest.DigestUtils, and javap -c -constants showed the constant in the bytecode already rewritten. A name built at run time from pieces cannot be rewritten, and neither can a name read from a file outside the JAR.

Relocation is also the standard fix for an API your own library exposes: never relocate the types that appear in your public signatures, or callers can no longer pass them in.

Replacing or attaching the shaded artifact

With shadedArtifactAttached at its default false, the shaded JAR replaces the main artifact (the log said "Replacing original artifact with shaded artifact"), and the thin JAR is left behind as original-*.jar. With true, the shaded JAR is attached with the classifier shadedClassifierName (default shaded; all above) and the project’s main artifact stays the thin JAR. After mvn install, the local repository holds both:

tidepool-cli-1.0.0-SNAPSHOT-all.jar
tidepool-cli-1.0.0-SNAPSHOT.jar
tidepool-cli-1.0.0-SNAPSHOT.pom

(_remote.repositories and maven-metadata-local.xml were left out of the listing.) A consumer who wants the uber JAR asks for it with the classifier, <classifier>all</classifier>, the same way <classifier>sources</classifier> works. Which is better? Attaching keeps the coordinates meaning one thing: library users keep depending on a normal artifact with normal transitive dependencies, and only the command-line distribution uses the -all file. Replacing is simpler when the module is the application and nobody depends on it.

The dependency-reduced POM

When the JAR contains its dependencies, the POM published with it should no longer declare them, or consumers would pull in the same libraries twice, once inside the JAR and once as dependencies. So by default the plugin writes a dependency-reduced POM (createDependencyReducedPom is true) at dependency-reduced-pom.xml in the module directory (not in target/; dependencyReducedPomLocation changes that) with the shaded dependencies removed, and makes the project use it from then on, so that install and deploy publish it. With all the dependencies of tidepool-cli shaded, the POM that mvn install put in the local repository had none:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
  <parent>
    <artifactId>tidepool-parent</artifactId>
    <groupId>io.tidepool</groupId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>
  <artifactId>tidepool-cli</artifactId>
  <build>
    ...
              <transformers>
                <transformer />
                <transformer>
                  <addHeader>false</addHeader>
                </transformer>
                <transformer>
                  <mainClass>io.tidepool.cli.Main</mainClass>
                </transformer>
              </transformers>
    ...
  </build>
</project>

(Abbreviated with ... where marked.) Three observations from this run of 3.6.2:

  • The file has no <dependencies>. That is the point for the uber JAR.

  • The implementation attributes of the <transformer> elements are gone from the re-serialised configuration. It is harmless in the published POM, which nobody builds from, but it means the file must not be copied back over your own pom.xml.

  • The POM was used for the thin JAR as well in the attached mode. With shadedArtifactAttached set to true and the default createDependencyReducedPom, the installed tidepool-cli-1.0.0-SNAPSHOT.pom still had no dependencies, although the main artifact beside it was the thin JAR that needs them: a user of the library would get the JAR and none of its dependencies. Setting createDependencyReducedPom to false, as the configuration above does, made the installed POM list its three dependencies again, and no dependency-reduced-pom.xml was written.

Rules of thumb: replace the main artifact and keep the default for an application nobody depends on; attach the shaded JAR with a classifier and disable the reduced POM for anything libraries or other modules depend on. promoteTransitiveDependencies turns the dependencies of removed artifacts into direct dependencies of the reduced POM, for the case where a shaded dependency has a transitive one that is not shaded, and keepDependenciesWithProvidedScope keeps provided dependencies. Add the file to .gitignore when you use the default; it is a build product written outside target/.

Minimising the JAR

minimizeJar analyses which classes of the dependencies are reachable from the project’s classes and leaves out the rest. It is attractive because the tidepool uber JAR is mostly Commons IO and Codec classes that the program never touches. Adding <minimizeJar>true</minimizeJar> to the configuration above and running mvn package printed (extra lines removed):

[INFO] Minimizing jar io.tidepool:tidepool-cli:jar:1.0.0-SNAPSHOT
[INFO] Minimized 910 -> 594 (65%)

The JAR shrank from 1,458,217 bytes (944 entries) to 930,486 bytes (628 entries) and still ran (San Francisco: -0.869 m, --version and --help too). The analysis is static, however: it follows references that appear in bytecode. A class that is only reached through reflection, a ServiceLoader or a string name has no reference and is removed, and the program fails at run time with an error that mentions a class the source does not use directly. A reproducing project with commons-codec that loads a class by name:

Class<?> c = Class.forName("org.apache.commons.codec.digest.DigestUtils");
System.out.println("loaded " + c.getName());

This class is this page’s own reproducer, not an example from the Maven documentation.

[INFO] Minimizing jar io.tidepool.demo:reflect:jar:1.0.0
[INFO] Minimized 253 -> 145 (57%)
...
Exception in thread "main" java.lang.ClassNotFoundException: org.apache.commons.codec.digest.DigestUtils
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:502)
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:478)
	at java.base/java.lang.Class.forName(Class.java:468)
	at demo.Main.main(Main.java:5)

The build succeeded. The fix is a filter that includes what must stay; per the plugin documentation an include filter makes minimizeJar keep those classes, and implicitly leaves out the other classes of that artifact:

              <minimizeJar>true</minimizeJar>
              <filters>
                <filter>
                  <artifact>commons-codec:commons-codec</artifact>
                  <includes>
                    <include>org/apache/commons/codec/digest/**</include>
                  </includes>
                </filter>
              </filters>
[INFO] Minimized 27 -> 27 (100%)
loaded org.apache.commons.codec.digest.DigestUtils

Use minimizeJar only with tests that run the shaded JAR; a green unit test says nothing about it. The same applies to the Picocli example above, which survived minimising because its annotated classes are referenced from Main.

Duplicate classes: which one wins

The overlapping-class warning (define 1 overlapping classes) deserves attention, because the winner is not something to rely on. Two libraries lib-x and lib-y each contain dup.Dup (their who() methods answer lib-x and lib-y). The application depends directly on lib-x and on mid, which depends on lib-y:

[WARNING] lib-x-1.0.0.jar, lib-y-1.0.0.jar define 1 overlapping classes:
[WARNING]   - dup.Dup
dup.Dup comes from lib-y

The shaded JAR used the class of the transitive lib-y on Maven 3.9.16, 3.10.0 and 4.0.0-rc-7 alike, although the direct dependency lib-x is closer to the project and declared after mid. The plugin warns, and picks according to its own processing order. Do not guess. Say which one you mean with a filter that excludes the loser:

              <filters>
                <filter>
                  <artifact>io.tidepool.demo:lib-y</artifact>
                  <excludes>
                    <exclude>dup/Dup.class</exclude>
                  </excludes>
                </filter>
              </filters>
dup.Dup comes from lib-x

Duplicates usually come from a library that was repackaged under another artifact id. Find them before shading, with dependency:tree and the analysis tools of Dependency Analysis & Convergence, and ban them for good with the banDuplicateClasses rule of the MojoHaus extra enforcer rules (Enforcer).

The Assembly plugin

The Assembly plugin builds an assembly: an archive whose content and layout are described by an assembly descriptor, an XML file. It is the right tool when what you ship is more than one JAR: a start script, the libraries in lib/, a configuration directory, documentation, sample data, with executable permissions and the right line endings. It has a single working goal in 3.8.0, assembly:single, and that goal is meant for the lifecycle (as in the examples below) or for the command line when the files it needs are already built. The older assembly:assembly goal is not in 3.8.0:

[ERROR] Could not find goal 'assembly' in plugin org.apache.maven.plugins:maven-assembly-plugin:3.8.0 among available goals help, single -> [Help 1]

Books and blog posts from the Maven 2 era use mvn assembly:assembly and the jar-with-dependencies shortcut on the command line; today the goal is single and it is bound to a phase.

Predefined descriptors

The plugin ships four descriptors you can name with descriptorRefs, without writing a descriptor yourself:

Descriptor reference Content

bin

The project’s JAR plus README, LICENSE and NOTICE files, in a directory named after the artifact; formats tar.gz, tar.bz2 and zip

jar-with-dependencies

One JAR in which the project’s classes and all its runtime dependencies are unpacked (an uber JAR without relocation); the format is jar

src

The project’s source tree: pom.xml, src/, README, LICENSE, NOTICE

project

The whole project directory, excluding build output and version-control files

A scratch project predef (a copy of the tidepool-model sources, with a README.md, a LICENSE.txt and a NOTICE.txt in the project directory) lists the three archive-style descriptors in one descriptorRefs and binds single to package:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>3.8.0</version>
        <configuration>
          <descriptorRefs>
            <descriptorRef>bin</descriptorRef>
            <descriptorRef>src</descriptorRef>
            <descriptorRef>project</descriptorRef>
          </descriptorRefs>
          <tarLongFileMode>posix</tarLongFileMode>
        </configuration>
        <executions>
          <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals><goal>single</goal></goals>
          </execution>
        </executions>
      </plugin>
[INFO] --- assembly:3.8.0:single (make-assembly) @ predef ---
[INFO] Building tar: ./target/predef-1.0.0-bin.tar.gz
[INFO] Building tar: ./target/predef-1.0.0-bin.tar.bz2
[INFO] Building zip: ./target/predef-1.0.0-bin.zip
[INFO] Building tar: ./target/predef-1.0.0-src.tar.gz
[INFO] Building tar: ./target/predef-1.0.0-src.tar.bz2
[INFO] Building zip: ./target/predef-1.0.0-src.zip
[INFO] Building tar: ./target/predef-1.0.0-project.tar.gz
[INFO] Building tar: ./target/predef-1.0.0-project.tar.bz2
[INFO] Building zip: ./target/predef-1.0.0-project.zip

The ZIP files hold (unzip -Z1, directories left out):

== predef-1.0.0-bin.zip
predef-1.0.0/LICENSE.txt
predef-1.0.0/NOTICE.txt
predef-1.0.0/README.md
predef-1.0.0/predef-1.0.0.jar
== predef-1.0.0-src.zip
predef-1.0.0/LICENSE.txt
predef-1.0.0/NOTICE.txt
predef-1.0.0/README.md
predef-1.0.0/pom.xml
predef-1.0.0/src/main/java/io/tidepool/model/Constituent.java
predef-1.0.0/src/main/java/io/tidepool/model/Height.java
predef-1.0.0/src/main/java/io/tidepool/model/Station.java
predef-1.0.0/src/main/java/io/tidepool/model/TideEvent.java

(project held the same files as src in this tiny project, since no other files exist yet.) Each file name is <finalName>-<descriptor id>.<format>; appendAssemblyId (property assembly.appendAssemblyId) set to false leaves the id out, which is useful for a single descriptor and wrong for several, as they would overwrite each other.

jar-with-dependencies, and the same services problem

The jar-with-dependencies reference is a convenient uber JAR. With the manifest configured in <archive> (the same MavenArchiver configuration as the JAR plugin, see Packaging JARs, WARs & Manifests), it gives greeter-app a runnable -jar-with-dependencies.jar:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>3.8.0</version>
        <configuration>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
          <archive>
            <manifest>
              <mainClass>io.tidepool.demo.app.Main</mainClass>
            </manifest>
          </archive>
        </configuration>
        <executions>
          <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals><goal>single</goal></goals>
          </execution>
        </executions>
      </plugin>
mvn -B package
java -jar greeter-app/target/greeter-app-1.0.0-jar-with-dependencies.jar
EnGreeter: Hello, tidepool!

It has the same flaw as the plain shade configuration: the Spanish greeter is lost and the build did not complain. The assembly counterpart of the ServicesResourceTransformer is a container descriptor handler; the plugin has four built in, file-aggregator (merge files matching a regular expression into one), metaInf-services, metaInf-spring and plexus. The predefined descriptor cannot be configured, so you write your own, here a copy of jar-with-dependencies with the handler added:

greeter-app/src/assembly/fat.xml
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 https://maven.apache.org/xsd/assembly-2.2.0.xsd">
  <id>fat</id>
  <formats>
    <format>jar</format>
  </formats>
  <includeBaseDirectory>false</includeBaseDirectory>
  <dependencySets>
    <dependencySet>
      <outputDirectory>/</outputDirectory>
      <useProjectArtifact>true</useProjectArtifact>
      <unpack>true</unpack>
      <scope>runtime</scope>
    </dependencySet>
  </dependencySets>
  <containerDescriptorHandlers>
    <containerDescriptorHandler>
      <handlerName>metaInf-services</handlerName>
    </containerDescriptorHandler>
  </containerDescriptorHandlers>
</assembly>

The plugin configuration then says <descriptors><descriptor>src/assembly/fat.xml</descriptor></descriptors> instead of the descriptorRefs. (descriptors, descriptorRefs and descriptorSourceDirectory are alternatives, not combined.)

mvn -B package -pl greeter-app -am
java -jar greeter-app/target/greeter-app-1.0.0-fat.jar
unzip -p greeter-app/target/greeter-app-1.0.0-fat.jar META-INF/services/io.tidepool.demo.Greeter
EnGreeter: Hello, tidepool!
EsGreeter: Hola, tidepool!
io.tidepool.demo.en.EnGreeter
io.tidepool.demo.es.EsGreeter

(-am builds the sibling modules greeter-app needs; without it Maven looked for greeter-en in the remote repository and failed with "Path io/tidepool/demo/greeter-en/1.0.0/greeter-en-1.0.0.jar NOT allowed from central", see Multi-Module Builds & the Reactor.)

Shade or assembly for a fat JAR?

Both produce a runnable JAR. Shade is the better tool for it: it relocates, minimises, has a transformer for each kind of resource that must be merged, and produces a dependency-reduced POM. The assembly’s jar-with-dependencies unpacks the dependencies, has no relocation and no minimising, and its container descriptor handlers cover only four kinds of file. The assembly plugin documentation says so itself: jar-with-dependencies "provides only basic support for uber-jars" and, for more control, the Shade plugin should be used. Use the assembly plugin for what shade cannot do, which is a layout of several files.

Packaging tidepool as a distribution

The goal for tidepool is a download that a user unpacks and runs: a bin/tidepool.sh that works from anywhere, the JARs in lib/ and the sample data. A new module tidepool-dist with packaging pom (it produces no JAR of its own) depends on tidepool-cli; the assembly then finds all the runtime dependencies through it. The distribution uses the thin JARs (the main artifact of tidepool-cli, since the all JAR is attached with a classifier) and lib/* as the class path.

tidepool-parent/pom.xml (additions)
  <modules>
    <module>tidepool-model</module>
    <module>tidepool-engine</module>
    <module>tidepool-cli</module>
    <module>tidepool-dist</module>
  </modules>
  ...
        <plugin><artifactId>maven-shade-plugin</artifactId><version>3.6.2</version></plugin>
        <plugin><artifactId>maven-assembly-plugin</artifactId><version>3.8.0</version></plugin>
tidepool-dist/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>io.tidepool</groupId>
    <artifactId>tidepool-parent</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>
  <artifactId>tidepool-dist</artifactId>
  <packaging>pom</packaging>
  <dependencies>
    <dependency>
      <groupId>io.tidepool</groupId>
      <artifactId>tidepool-cli</artifactId>
      <version>${project.version}</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <executions>
          <execution>
            <id>make-dist</id>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
            <configuration>
              <descriptors>
                <descriptor>src/assembly/dist.xml</descriptor>
              </descriptors>
            </configuration>
          </execution>
          <execution>
            <id>make-modules</id>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
            <configuration>
              <descriptors>
                <descriptor>src/assembly/modules.xml</descriptor>
              </descriptors>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Two executions, each with its own descriptor, are the usual way to build several assemblies; the <id> of an execution and the <id> of a descriptor are different things. The launcher is a script in src/main/scripts; it is filtered, so Maven properties are replaced (the resource filtering rules are on Resources & Filtering):

tidepool-dist/src/main/scripts/tidepool.sh
#!/bin/sh
# tidepool ${project.version}: launcher filled in by the assembly (filtered=true)
HOME_DIR=$(cd "$(dirname "$0")/.." && pwd)
exec java $JAVA_OPTS -cp "$HOME_DIR/lib/*" io.tidepool.cli.Main "$@"

This launcher script is this page’s own lab setup, not an example from the Maven documentation.

tidepool-dist/src/assembly/dist.xml
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 https://maven.apache.org/xsd/assembly-2.2.0.xsd">
  <id>dist</id>
  <formats>
    <format>tar.gz</format>
    <format>zip</format>
  </formats>
  <baseDirectory>tidepool-${project.version}</baseDirectory>
  <fileSets>
    <fileSet>
      <directory>src/main/scripts</directory>
      <outputDirectory>bin</outputDirectory>
      <fileMode>0755</fileMode>
      <lineEnding>unix</lineEnding>
      <filtered>true</filtered>
    </fileSet>
    <fileSet>
      <directory>${project.parent.basedir}/data</directory>
      <outputDirectory>data</outputDirectory>
    </fileSet>
  </fileSets>
  <dependencySets>
    <dependencySet>
      <outputDirectory>lib</outputDirectory>
      <useProjectArtifact>false</useProjectArtifact>
      <scope>runtime</scope>
    </dependencySet>
  </dependencySets>
</assembly>
mvn -B -DskipTests clean package
tar tvzf tidepool-dist/target/tidepool-dist-1.0.0-SNAPSHOT-dist.tar.gz
[INFO] --- assembly:3.8.0:single (make-dist) @ tidepool-dist ---
[INFO] Building tar: .../tidepool-dist/target/tidepool-dist-1.0.0-SNAPSHOT-dist.tar.gz
[INFO] Building zip: .../tidepool-dist/target/tidepool-dist-1.0.0-SNAPSHOT-dist.zip
[INFO] --- assembly:3.8.0:single (make-modules) @ tidepool-dist ---
[INFO] Building zip: .../tidepool-dist/target/tidepool-dist-1.0.0-SNAPSHOT-modules.zip
...
[INFO] BUILD SUCCESS

drwxr-xr-x  0 root   root        0 Oct 16 02:00 tidepool-1.0.0-SNAPSHOT/bin/
-rwxr-xr-x  0 root   root      200 Oct 16 02:00 tidepool-1.0.0-SNAPSHOT/bin/tidepool.sh
drwxr-xr-x  0 root   root        0 Oct 16 02:00 tidepool-1.0.0-SNAPSHOT/data/
-rw-r--r--  0 root   root      239 Oct 16 02:00 tidepool-1.0.0-SNAPSHOT/data/stations.csv
-rw-r--r--  0 root   root     5313 Oct 16 02:00 tidepool-1.0.0-SNAPSHOT/lib/tidepool-model-1.0.0-SNAPSHOT.jar
-rw-r--r--  0 root   root   563971 Oct 16 02:00 tidepool-1.0.0-SNAPSHOT/lib/commons-io-2.20.0.jar
-rw-r--r--  0 root   root     4342 Oct 16 02:00 tidepool-1.0.0-SNAPSHOT/lib/tidepool-engine-1.0.0-SNAPSHOT.jar
-rw-r--r--  0 root   root     4142 Oct 16 02:00 tidepool-1.0.0-SNAPSHOT/lib/tidepool-cli-1.0.0-SNAPSHOT.jar
-rw-r--r--  0 root   root   374716 Oct 16 02:00 tidepool-1.0.0-SNAPSHOT/lib/commons-codec-1.19.0.jar
-rw-r--r--  0 root   root    60964 Oct 16 02:00 tidepool-1.0.0-SNAPSHOT/lib/commons-csv-1.14.1.jar
-rw-r--r--  0 root   root   417640 Oct 16 02:00 tidepool-1.0.0-SNAPSHOT/lib/picocli-4.7.7.jar

(The Reactor lines of the output were replaced by ...; the times are 00:00 UTC of the outputTimestamp, printed in the time zone of the machine.) Unpacked and run:

tar xzf tidepool-dist/target/tidepool-dist-1.0.0-SNAPSHOT-dist.tar.gz
cd tidepool-1.0.0-SNAPSHOT
JAVA_OPTS="-Duser.language=en -Duser.country=US" ./bin/tidepool.sh data/stations.csv BOS
./bin/tidepool.sh data/stations.csv XXX; echo "exit=$?"
Boston: -0.662 m
Unknown station: XXX
exit=1

The elements used, and the ones the example leaves out, in the order of the descriptor format:

Element Meaning

id, formats

The classifier of the attached artifact and the archive formats (zip, tar, tar.gz, tar.bz2, jar, war, dir and others)

baseDirectory, includeBaseDirectory

The single top-level directory inside the archive (default is the final name; false for none, as in a fat JAR)

fileSets

Copies directories: directory, outputDirectory, includes, excludes, fileMode and directoryMode (octal), lineEnding (unix, dos, keep), filtered (replace Maven properties in text files)

files

Copies single files with an optional destName

dependencySets

Copies dependencies: scope, useProjectArtifact, unpack, outputDirectory, outputFileNameMapping, includes and excludes of artifacts, useTransitiveFiltering

moduleSets

Includes the output of the modules of a multi-module build: sources, binaries, includes and excludes, useAllReactorProjects, includeSubModules

containerDescriptorHandlers, componentDescriptors

Merge descriptor-like files; reuse fragments of a descriptor from another file

moduleSets for a multi-module build

A moduleSet selects modules of the reactor rather than dependencies. Because tidepool-dist is not the aggregator POM, the descriptor sets useAllReactorProjects; sources copies source directories and binaries the module artifacts. ${module.artifactId} in the output directory mapping names each module’s directory:

tidepool-dist/src/assembly/modules.xml
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 https://maven.apache.org/xsd/assembly-2.2.0.xsd">
  <id>modules</id>
  <formats>
    <format>zip</format>
  </formats>
  <includeBaseDirectory>false</includeBaseDirectory>
  <moduleSets>
    <moduleSet>
      <useAllReactorProjects>true</useAllReactorProjects>
      <includes>
        <include>io.tidepool:tidepool-model</include>
        <include>io.tidepool:tidepool-engine</include>
      </includes>
      <sources>
        <outputDirectoryMapping>sources/${module.artifactId}</outputDirectoryMapping>
        <fileSets>
          <fileSet>
            <directory>src/main/java</directory>
          </fileSet>
        </fileSets>
      </sources>
      <binaries>
        <outputDirectory>modules</outputDirectory>
        <unpack>false</unpack>
        <includeDependencies>false</includeDependencies>
      </binaries>
    </moduleSet>
  </moduleSets>
</assembly>
sources/tidepool-model/io/tidepool/model/Constituent.java
sources/tidepool-model/io/tidepool/model/Height.java
sources/tidepool-model/io/tidepool/model/Station.java
sources/tidepool-model/io/tidepool/model/TideEvent.java
sources/tidepool-engine/io/tidepool/engine/StationReader.java
sources/tidepool-engine/io/tidepool/engine/TideEngine.java
modules/tidepool-model-1.0.0-SNAPSHOT.jar
modules/tidepool-engine-1.0.0-SNAPSHOT.jar

(unzip -Z1 of the ZIP, directory entries left out.) A first attempt that also set includeModuleDirectory to false put the sources at the top of the archive, and the outputDirectoryMapping was ignored: the mapping is the module directory, so switching the directory off switches the mapping off. The plugin documentation has examples for both parts.

Attached artifacts

attach (property assembly.attach, default true) attaches every archive to the project, with the assembly id as the classifier and the format as the type, so install and deploy publish them. After mvn install:

tidepool-dist-1.0.0-SNAPSHOT-dist.tar.gz
tidepool-dist-1.0.0-SNAPSHOT-dist.zip
tidepool-dist-1.0.0-SNAPSHOT-modules.zip
tidepool-dist-1.0.0-SNAPSHOT.pom

(Listing of the artifact directory in the scratch repository, maven-metadata-local.xml removed.) A consumer depends on the download with <type>tar.gz</type> and <classifier>dist</classifier>. Other parameters of assembly:single worth knowing (from assembly:help -Ddetail -Dgoal=single): finalName, outputDirectory, appendAssemblyId, dryRun (assembly.dryRun), skipAssembly (assembly.skipAssembly), tarLongFileMode (warn by default, posix or gnu for paths over 100 characters), ignorePermissions, includeProjectBuildFilters, overrideUid and its siblings, outputTimestamp (the project’s project.build.outputTimestamp) and runOnlyAtExecutionRoot.

Reproducible builds and the Maven version

Both plugins honour project.build.outputTimestamp (the property in the parent POM of tidepool, a default since Maven 3.10, see Security & Reproducible Builds): every entry of the shaded JARs listed above is dated 10-16-2026 00:00. The same sources were built on three Maven releases, with every plugin version pinned in the parent POM, and the SHA-256 digests of the outputs compared (shasum -a 256):

Maven Shaded tidepool-cli-...-all.jar tidepool-dist-...-dist.zip Digest prefix of the ZIP

3.10.0

identical

identical

24a21c9e...

3.9.16

identical

identical

24a21c9e...

4.0.0-rc-7 (release candidate)

identical

different

eebe80f4...

The shaded JAR was byte-for-byte equal on all three (42d91653...). The two ZIP files differ only in the order of the entries: Maven 4 listed commons-io-2.20.0.jar later in lib/, because the order of the resolved dependencies differs there. The contents are the same. The lesson is that reproducibility needs the whole toolchain pinned, which Enforcer can check. It also matters that the plugins are pinned: greeter-app was built without a pinned JAR plugin, and the manifest of the same sources differed between releases, because the lifecycle bindings of the Maven distribution select the plugin version when the POM does not:

Maven 3.9.16:  Created-By: Maven JAR Plugin 3.5.0
Maven 3.10.0:  Created-By: Maven JAR Plugin 3.5.1

A different manifest means a different digest. Maven 3.10’s changed classpath order (breadth-first, see Maven Versions & Release History) did not change any result on this page, but it is exactly the kind of change that can reorder an assembly or flip the winner of an overlapping class, which is one more reason to resolve overlaps explicitly with filters rather than by position.

Plugins of the Maven 3 line, such as the ones used here, ran unchanged on Maven 4.0.0-rc-7. Their Maven 4 successors are covered on Migrating to Maven 4.

Alternatives and when not to build a fat JAR

An uber JAR trades convenience for several costs: the libraries lose their identity (no security scanner or Software Bill of Materials sees them unless the META-INF/maven entries are kept, as they were here), a security fix means a rebuild and redeploy of the whole artifact, Docker layers cannot be cached (one 1.4 MB layer changes on every build instead of a dependency layer that rarely does), licences have to be reviewed for the combined work, and every overlapping resource is a decision. Consider these first:

Alternative When it fits

Thin JAR with Class-Path manifest and a lib/ directory

The JARs stay separate; the JAR plugin and dependency:copy-dependencies do it, see Packaging JARs, WARs & Manifests

Assembly distribution (above)

A product with scripts, configuration and several files

App Assembler (org.codehaus.mojo:appassembler-maven-plugin)

Generated start scripts for Unix and Windows in front of a repo/ directory laid out like a Maven repository; it is the plugin the Maven 2 books use for this

Spring Boot repackaging

A Spring Boot application, which has its own nested-JAR layout and layered images; see Spring Boot: Getting Started and Docker Images for Java & Spring Boot (not run here)

jlink and jpackage

A runtime image or native installer that includes a JVM; JDK tools (not run here)

An application server or container image that provides the libraries

WARs, EARs and layered images; see Packaging JARs, WARs & Manifests

App Assembler was run on tidepool-cli of the same project with version 2.1.0 (the latest, last published on Maven Central in 2019, so effectively dormant): the assemble goal produced bin/tidepool, bin/tidepool.bat and a repo/ holding each library at its Maven repository path (repo/info/picocli/picocli/4.7.7/picocli-4.7.7.jar and so on), and the script ran:

      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>appassembler-maven-plugin</artifactId>
        <version>2.1.0</version>
        <configuration>
          <programs>
            <program>
              <mainClass>io.tidepool.cli.Main</mainClass>
              <id>tidepool</id>
            </program>
          </programs>
        </configuration>
        <executions>
          <execution>
            <phase>package</phase>
            <goals><goal>assemble</goal></goals>
          </execution>
        </executions>
      </plugin>
Honolulu: -0.112 m

That output came from tidepool-cli/target/appassembler/bin/tidepool ../tidepool/data/stations.csv HNL. A custom assembly gives you the same layout with a plugin that is maintained; appassembler’s script generation is the only part you would miss.

Common errors and how to read them

Symptom Cause and fix

no main manifest attribute, in x.jar

No Main-Class: add the ManifestResourceTransformer (shade) or <archive><manifest><mainClass> (JAR and assembly plugins)

SecurityException: Invalid signature file digest for Manifest main attributes

A signed JAR was merged: filter META-INF/*.SF, META-INF/*.DSA, META-INF/*.RSA

A feature works in the IDE and not in the JAR; ServiceLoader finds fewer providers

Overlapping META-INF/services files: ServicesResourceTransformer or the metaInf-services handler

ClassNotFoundException or NoClassDefFoundError only after shading

minimizeJar removed a class reached by reflection: add an include filter; or a relocation changed a name read from a file

NoSuchMethodError of a library at run time

Two versions of the library on the class path: relocate the one you ship

define N overlapping classes

The same class in two artifacts, one is dropped: exclude the unwanted one with a filter

A user of the library gets NoClassDefFoundError after you published a shaded module

The dependency-reduced POM has no dependencies but the main artifact is the thin JAR: attach the shaded JAR with a classifier and set createDependencyReducedPom to false

Could not find goal 'assembly'

assembly:assembly does not exist any more: bind assembly:single to a phase

No assembly descriptors found

assembly:single ran in a module (here the aggregator POM) with no descriptor configured: configure descriptors or descriptorRefs in the <build> of the module that should build the assembly

NOT allowed from central for a sibling module

Built one module of a reactor without -am; see the reactor page

(The No assembly descriptors found error was printed when assembly:single was run from the command line over the whole reactor: the goal runs in every module, including the aggregator POM, which had no descriptor.)

Where the books are outdated

Maven 2.2 era statement Current status

mvn assembly:assembly -DdescriptorId=jar-with-dependencies builds a fat JAR

assembly:assembly is gone; bind assembly:single. The jar-with-dependencies descriptor still exists; the descriptor format is now http://maven.apache.org/ASSEMBLY/2.2.0 and the plugin is 3.8.0

Use the Assembly plugin for an uber JAR

Shade is the standard tool: it relocates, minimises and merges resources. Assembly remains the tool for distributions

App Assembler is the way to generate start scripts

It still works (2.1.0 ran on Maven 3.10.0) but has not been released since 2019; a custom assembly with a launcher script is a common replacement

Plugin versions need not be pinned

Pin them: the same sources produced a different JAR manifest on Maven 3.9.16 and 3.10.0 until the JAR plugin was pinned