skip to content

How do you build a runnable executable fat jar with the assembly plugin, and what's needed to make `java -jar` work?

level: middleimportance: should knowfreq 50%

answer

  1. jar-with-dependencies + mainClass in <archive>
  2. no manifest -> 'no main manifest attribute'
  3. appendAssemblyId=false for clean name
  4. merges files: META-INF/services collide
  5. shade for SPI/relocation

basics

~10 s

Use the jar-with-dependencies descriptorRef so all dependencies are merged into one jar, and set a Main-Class in the manifest via <archive><manifest><mainClass>. Then java -jar can find both your code and its dependencies.

solid answer

~40 s

Configure the assembly plugin with `descriptorRef` = `jar-with-dependencies` so your classes plus all dependency classes land in one jar. Crucially, set the manifest's `Main-Class` using `<archive><manifest><mainClass>com.example.Main</mainClass></manifest></archive>`, otherwise `java -jar` fails with 'no main manifest attribute'. Bind the `single` goal to the `package` phase. The result is `target/<artifactId>-<version>-jar-with-dependencies.jar`, runnable directly. Caveats: jar-with-dependencies unpacks dependency jars and merges their files, so if two dependencies ship the same `META-INF/services/...` file or signed-jar metadata, one silently overwrites the other — that breaks SPI/ServiceLoader-based libraries (JDBC, Jackson modules). For those cases the shade plugin (with `ServicesResourceTransformer` and package relocation) is the correct tool. Assembly's fat jar is fine for simple CLI tools.

code

xml · 11 lines
xml
<configuration>
  <descriptorRefs>
    <descriptorRef>jar-with-dependencies</descriptorRef>
  </descriptorRefs>
  <archive>
    <manifest>
      <mainClass>com.example.Main</mainClass>
    </manifest>
  </archive>
  <appendAssemblyId>false</appendAssemblyId>
</configuration>

go deeper

for a junior

Knows jar-with-dependencies makes a fat jar.

for a middle

Adds Main-Class via <archive><manifest> and binds single to package.

for a senior

Anticipates file-merge/SPI collisions and knows when to switch to shade.

for a principal

Defines org policy: fat-jar tooling choice, signing/SPI safety, reproducible builds.

## Goal Produce a single jar a user can run with just `java -jar app.jar`, with no external classpath. Two things must be true: 1. **All needed classes are inside the jar** — yours and your dependencies'. 2. **The jar's manifest names the entry point** via the `Main-Class` attribute. ## Step 1 — bundle dependencies The predefined `jar-with-dependencies` descriptorRef does this: it takes every dependency in scope (compile + runtime), **unpacks** their `.class` files, and writes them into one output jar alongside yours. ## Step 2 — set the entry point The `<archive>` configuration controls the generated `META-INF/MANIFEST.MF`. Set: ```xml <archive> <manifest> <mainClass>com.example.Main</mainClass> </manifest> </archive> ``` Without this you get `no main manifest attribute, in app.jar`. ## Full configuration ```xml <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.7.1</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest><mainClass>com.example.Main</mainClass></manifest> </archive> <!-- drop the -jar-with-dependencies suffix if desired --> <appendAssemblyId>false</appendAssemblyId> </configuration> <executions> <execution> <id>make-fatjar</id> <phase>package</phase> <goals><goal>single</goal></goals> </execution> </executions> </plugin> ``` ## Output naming By default the file is `target/<artifactId>-<version>-jar-with-dependencies.jar`. The `jar-with-dependencies` part is the **assemblyId** suffix; set `<appendAssemblyId>false</appendAssemblyId>` to get a clean `<artifactId>-<version>.jar` (note this then overwrites/replaces the main artifact name — be deliberate). ## The big caveat: file merging Because dependency jars are *unpacked and merged*, files that exist at the **same path** in multiple jars collide and the last one wins. The classic victims: - `META-INF/services/*` (Java `ServiceLoader`/SPI — e.g. JDBC drivers, Jackson modules) — only one provider survives. - Signed-jar signature files in `META-INF` — break signature validation. - `application.properties` / `reference.conf` (Typesafe Config) — silently clobbered. The assembly plugin does **not** merge these intelligently. The **maven-shade-plugin** does, via transformers like `ServicesResourceTransformer` and `AppendingTransformer`, and can also relocate packages. So: assembly fat jar for simple, conflict-free CLIs; shade when SPI/relocation matters; spring-boot-maven-plugin for Boot apps.

  • You ship a fat jar that uses two libraries with ServiceLoader providers and only one works. Why, and how do you fix it?
    Both libraries have META-INF/services files at the same path; assembly overwrites one. Fix by switching to maven-shade-plugin with ServicesResourceTransformer, which concatenates the service files.
  • What does appendAssemblyId=false change?
    It removes the assemblyId suffix (e.g. -jar-with-dependencies) from the output filename, producing artifactId-version.jar.

saying these in an interview costs you the question

  • Forgetting the Main-Class manifest entry and expecting java -jar to work.
  • Assuming assembly merges META-INF/services correctly — it doesn't, it overwrites.
  • Believing the fat jar replaces all dependency management at runtime even for SPI-heavy stacks.

context