Skip to main content

Packaging Java Apps: Fat JAR with Shade Plugin vs Spring Boot JAR vs jpackage

Compare Maven Shade Plugin fat JARs, Spring Boot executable JARs, and jpackage native installers — with full Maven configurations, build commands, a side-by-side comparison table, and pitfall guide.

Distributing a Java application means choosing how to package it. Should you produce a thin JAR and let users manage the classpath? An über JAR (fat JAR) with all dependencies bundled in? A native installer via jpackage? Each approach has a place, and understanding all three lets you pick the right one for every deployment scenario — from a developer tool shipped on Maven Central to a desktop app distributed to non-technical users.

Option 1 — Executable Fat JAR with Maven Shade Plugin

The Maven Shade Plugin merges all dependency JARs into a single über JAR. The result is one file that can be run anywhere Java is installed:

java -jar my-app-1.0-shaded.jar
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-shade-plugin</artifactId>
      <version>3.5.2</version>
      <executions>
        <execution>
          <phase>package</phase>
          <goals><goal>shade</goal></goals>
          <configuration>
            <createDependencyReducedPom>false</createDependencyReducedPom>
            <transformers>
              <!-- Sets the Main-Class in MANIFEST.MF -->
              <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                <mainClass>com.ankurm.App</mainClass>
              </transformer>
              <!-- Merges META-INF/services files (needed for SPI) -->
              <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
            </transformers>
            <filters>
              <filter>
                <artifact>*:*</artifact>
                <excludes>
                  <!-- Remove signature files to avoid JAR verification errors -->
                  <exclude>META-INF/*.SF</exclude>
                  <exclude>META-INF/*.DSA</exclude>
                  <exclude>META-INF/*.RSA</exclude>
                </excludes>
              </filter>
            </filters>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Build and run:

mvn clean package
java -jar target/my-app-1.0-shaded.jar

Option 2 — Fat JAR with Spring Boot Maven Plugin

Spring Boot’s plugin produces a nested über JAR that avoids classpath conflicts because each dependency’s JAR remains intact inside the outer JAR:

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <configuration>
        <!-- Optional: exclude dev tools from the fat JAR -->
        <excludes>
          <exclude>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
          </exclude>
        </excludes>
      </configuration>
    </plugin>
  </plugins>
</build>
mvn clean package
java -jar target/my-app-1.0-SNAPSHOT.jar

Option 3 — Native Platform Installer with jpackage

jpackage (JDK 14+, stable in JDK 16+) bundles your application with a trimmed JRE and produces a platform-native installer: .msi/.exe on Windows, .dmg/.pkg on macOS, .deb/.rpm on Linux. Users do not need Java installed.

<plugin>
  <groupId>org.panteleyev</groupId>
  <artifactId>jpackage-maven-plugin</artifactId>
  <version>1.6.5</version>
  <configuration>
    <name>MyApp</name>
    <appVersion>1.0.0</appVersion>
    <vendor>AnkurM Studio</vendor>
    <destination>target/installer</destination>
    <module>com.ankurm/com.ankurm.App</module>
    <runtimeImage>${project.build.directory}/runtime</runtimeImage>
    <javaOptions>
      <option>-Dfile.encoding=UTF-8</option>
    </javaOptions>
    <!-- Windows-specific -->
    <winDirChooser>true</winDirChooser>
    <winMenu>true</winMenu>
    <winMenuGroup>AnkurM Studio</winMenuGroup>
    <winShortcut>true</winShortcut>
  </configuration>
</plugin>

Build a trimmed JRE first with jlink, then package:

# Step 1: Create a minimal runtime image
jlink --module-path $JAVA_HOME/jmods 
      --add-modules java.base,java.desktop,java.sql 
      --output target/runtime

# Step 2: Build the installer
mvn jpackage:jpackage
# produces target/installer/MyApp-1.0.0.msi (Windows)
# or target/installer/MyApp-1.0.0.dmg (macOS)

Side-by-Side Comparison

FeatureShade Fat JARSpring Boot JARjpackage Installer
Java required on target?YesYesNo (bundled JRE)
Output sizeMediumMediumLarge (JRE included)
Classpath conflicts?PossibleNo (nested JARs)N/A
Best forCLI tools, librariesSpring Boot appsDesktop apps, non-devs
Cross-platform?Yes (single JAR)Yes (single JAR)No (build per OS)
Requires JDK versionAnyAnyJDK 14+

Common Pitfalls

  • Shade: duplicate resources — two JARs with the same META-INF/services entry overwrite each other unless you include ServicesResourceTransformer.
  • Shade: signed JARs — always exclude *.SF, *.DSA, *.RSA signature files or the JVM will refuse to run the merged JAR.
  • jpackage: module path — if your app is not fully modularised, use --type app-image with a class-path mode instead of module mode.
  • jpackage: platform lock-in — you must run jpackage on the same OS you are targeting; use CI runners for each platform.

See Also

Conclusion

Choose the Maven Shade Plugin for CLI tools and library distributions where you want a single portable JAR without framework overhead. Use the Spring Boot Maven Plugin for any Spring Boot application — its nested-JAR format avoids classpath conflicts and pairs perfectly with Docker. Reach for jpackage when deploying a desktop or GUI application to end-users who have no Java installation and expect a familiar platform installer. All three approaches can coexist in different Maven profiles so you can build the right artifact for each deployment target from the same codebase.

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.