【Maven】Fat JAR( Uber JAR )の作成方法

Java

実行可能JARファイル

JARファイルには、依存関係を持つものや単一の実行可能JARなどがあります。

依存関係を持つ実行可能JARや単一の実行可能JARについて詳細はこちらで説明しています。

Mavenでは、依存関係のある実行可能JARファイル単一の実行可能JARファイル( Fat JAR )を簡単に作成することができます。( 実行可能JARファイル

Mavenを利用した基本的なJARファイルの作成手順はこちらでも整理しています。

Mavenによる実行可能JARの作成方法

依存関係を持つ実行可能JARのサンプル

ここからは、こちらのサンプルプログラムを使い、実行可能JARファイルを作成していきます。pom.xmlこちらにも記載している以下の通りです。

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>app</artifactId>
    <version>1.0.0</version>

    <!-- 最小構成のPOMに以下を追加 -->
    <build>
        <plugins>
            <!--
             | Maven JAR Plugin による Maven Archiver 設定を行い
             | マニフェストファイルにMain-Class属性が追加されるよう構成する
            -->
            <plugin>
                <!-- Maven JAR Plugin -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.1</version>
                <!-- Maven Archiver 構成 -->
                <configuration>
                    <archive>
                        <manifest>
                            <!-- Main-Class: com.example.Main -->
                            <mainClass>com.example.Main</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

こちらと同様 Person.java を次のように書き換え、依存関係のある実行可能JARファイルとします。依存ライブラリは commons-lang3-3.20.0.jar です。

Person.java
package com.example.model;

// import文を追加(Apache Commons Lang)
import org.apache.commons.lang3.StringUtils;

public class Person {

    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String printName() {
        // StringUtilsを使用した処理に変更
        return StringUtils.defaultString(name);
    }
}

こちらの手順と同様、packageフェーズを指定し、mvnコマンドで実行可能JARファイルを作成してみます。(ここでは成果物であるtargetディレクトリに不要なファイルが残らないよう、事前にCleanライフサイクルも実行するようにしておきます。)

Cleanライフサイクルは、前回のビルドで生成された全てのファイルを削除するため、clean に続けて次のフェーズを指定することで、過去に作成した成果物の残骸に影響を受けることなく、正常に目的のフェーズを実行することができます。( Usual Command Line Calls

> mvn clean package
・・・略・・・
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] /C:/Users/xxx/project/src/main/java/com/example/model/Person.java:[3,32] パッケージorg.apache.commons.lang3は存在しません
[ERROR] /C:/Users/xxx/project/src/main/java/com/example/model/Person.java:[14,16] シンボルを見つけられません
  シンボル:   変数 StringUtils
  場所: クラス com.example.model.Person
[INFO] 2 errors
[INFO] -------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
・・・略・・・

先に示したリンク先のページでは、これにより実行可能JARを作成できていましたが、この例ではエラーが発生してしまいます。これは、Person.javaorg.apache.commons.lang3.StringUtils を使用しており、commons-lang3-3.20.0.jar に依存するようになったためです。必要なJARファイルが存在していないため、依存関係を解決することができず、エラーとなっています。これは、こちらと同じ事象です。

依存関係の設定追加

そこで pom.xml へ次のように commons-lang3-3.20.0.jar の依存関係を追加します。( Apache Commons Lang > Maven Coordinates

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>app</artifactId>
    <version>1.0.0</version>

    <!-- 最小構成のPOMに以下を追加 -->
    <build>
        <plugins>
            <!--
             | Maven JAR Plugin による Maven Archiver 設定を行い
             | マニフェストファイルにMain-Class属性が追加されるよう構成する
            -->
            <plugin>
                <!-- Maven JAR Plugin -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.1</version>
                <!-- Maven Archiver 構成 -->
                <configuration>
                    <archive>
                        <manifest>
                            <!-- Main-Class: com.example.Main -->
                            <mainClass>com.example.Main</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <!-- 依存関係を追加 -->
    <dependencies>
        <!-- Apache Commons Lang -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.20.0</version>
        </dependency>
    </dependencies>
</project>

この変更によりJARファイルの作成は成功するようになります。しかし、作成された実行可能JARを実行しようとすると、今度は実行時エラーが発生してしまいます。

# JARファイルの作成は成功
> mvn clean package
・・・略・・・
[INFO] Building jar: C:\xxx\project\target\app-1.0.0.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
・・・略・・・

# 作成したJARファイルを実行しようとするとエラー
> java -jar target\app-1.0.0.jar
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/lang3/StringUtils
        at com.example.model.Person.printName(Person.java:14)
        at com.example.Main.main(Main.java:10)
Caused by: java.lang.ClassNotFoundException: org.apache.commons.lang3.StringUtils
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490)
        ... 2 more

これはこちらと同様の事象です。つまり、Class-Path属性が追加されていないことが問題であるということです。試しに app-1.0.0.jar の中身を確認してみると、Mavenによって作成されたマニフェストファイルには、次のようにMain-Class属性のみが記述されており、Class-Path属性が存在しないことが分かります。

MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.4.1
Build-Jdk-Spec: 25
Main-Class: com.example.Main

Class-Path属性の追加

そこで、Mavenによって作成される実行可能JARのマニフェストファイルに、Class-Path属性が記述されるように設定を追加する必要があります。

これには <addClasspath> を使用します。デフォルトでは false となっている <addClasspath>true を設定することで、マニフェストファイルにClass-Path属性が追加されます。( Make The Jar Executable

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>app</artifactId>
    <version>1.0.0</version>

    <!-- 最小構成のPOMに以下を追加 -->
    <build>
        <plugins>
            <!--
             | Maven JAR Plugin による Maven Archiver 設定を行い
             | マニフェストファイルにMain-Class属性が追加されるよう構成する
            -->
            <plugin>
                <!-- Maven JAR Plugin -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.1</version>
                <!-- Maven Archiver 構成 -->
                <configuration>
                    <archive>
                        <manifest>
                            <!--
                             | Class-Path: commons-lang3-3.20.0.jar
                             | <dependencies>に記述した依存関係をClass-Path属性に追加する
                            -->
                            <addClasspath>true</addClasspath>
                            <!-- Main-Class: com.example.Main -->
                            <mainClass>com.example.Main</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <!-- 依存関係を追加 -->
    <dependencies>
        <!-- Apache Commons Lang -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.20.0</version>
        </dependency>
    </dependencies>
</project>

再度JARを作成( > mvn clean package )すると、今度はClass-Path属性が追加されていることが分かります。

MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.4.1
Build-Jdk-Spec: 25
Class-Path: commons-lang3-3.20.0.jar
Main-Class: com.example.Main

ところが、マニフェストファイルが正しくなっても、この状態で実行してみると、先程と変わらず次のようなエラーのままです。

> java -jar target\app-1.0.0.jar
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/lang3/StringUtils
        at com.example.model.Person.printName(Person.java:14)
        at com.example.Main.main(Main.java:10)
Caused by: java.lang.ClassNotFoundException: org.apache.commons.lang3.StringUtils
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490)
        ... 2 more

依存先JARファイルの配置

これはこちらと同じ原因です。つまりマニフェストファイルは正しくなったものの、そこに記述されているクラスパス参照先に実体となる依存先のJARファイルが存在していないということです。従って、app-1.0.0.jar が出力されているのと同じ target ディレクトリ下に、commons-lang3-3.20.0.jar を配置することで、正常に実行できるようになります。

ローカルリポジトリのデフォルトは ${user.home}/.m2/repository/ です。( Configuring your Local Repository

アーティファクトがパスになるため、この場合、C:/Users/xxx/.m2/repository/org/apache/commons/commons-lang3/3.20.0 にMavenビルド時にダウンロードされた commons-lang3-3.20.0.jar が配置されています。

そのため、これを target ディレクトリ下にコピーして活用することができます。( Maven Repository Layout

> java -jar target\app-1.0.0.jar
Person01 has been created!

この例では、依存先のJARは commons-lang3-3.20.0.jar だけですが、実際には複数の依存関係があり、Mavenビルド後に毎回手動で依存関係のあるJARを全てtargetディレクトリ下に配置するのは大変です。ローカルリポジトリには依存するJARファイルの実体が存在しており、Mavenの管理下にあるため、これらをコピーして配置するところまで自動化できると便利です。

Maven Dependency Plugin

これを可能にするのが、Maven Dependency Plugin です。maven-dependency-plugin を使うことで、依存関係のあるJARファイルを全て target ディレクトリ下に配置するところまでをまとめて実行することができます。

dependency:copy-dependenciesゴールがこの機能を持っているため、これをpackageフェーズに紐づけます。pom.xml へ次のような記述を追加することで、targetディレクトリ下にJARファイルのコピーが作成されることを確認できます。

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>app</artifactId>
    <version>1.0.0</version>

    <!-- 最小構成のPOMに以下を追加 -->
    <build>
        <plugins>
            <!--
             | Maven JAR Plugin による Maven Archiver 設定を行い
             | マニフェストファイルにMain-Class属性が追加されるよう構成する
            -->
            <plugin>
                <!-- Maven JAR Plugin -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.1</version>
                <!-- Maven Archiver 構成 -->
                <configuration>
                    <archive>
                        <manifest>
                            <!--
                             | Class-Path: commons-lang3-3.20.0.jar
                             | <dependencies>に記述した依存関係をClass-Path属性に追加する
                            -->
                            <addClasspath>true</addClasspath>
                            <!-- Main-Class: com.example.Main -->
                            <mainClass>com.example.Main</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

            <!--
             | Maven Dependency Plugin のcopy-dependenciesゴールを
             | デフォルトライフサイクルのpackageフェーズに紐づけ
             | 依存関係に含まれるJARファイルがコピーされるよう構成する
            -->
            <plugin>
                <!-- Maven Dependency Plugin -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.10.0</version>
                <!-- ビルドライフサイクルのpackageフェーズにcopy-dependenciesゴールを紐づける -->
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <!-- 依存関係を追加 -->
    <dependencies>
        <!-- Apache Commons Lang -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.20.0</version>
        </dependency>
    </dependencies>
</project>

実際にJARを作成( > mvn clean package )してみると、target直下ではなく、その下にdependencyディレクトリが作成され、その中にJARファイルがコピーされていることが分かります。従ってこのままではまた実行時エラーになってしまうため、Class-Path属性をこれに合わせ、Class-Path: commons-lang3-3.20.0.jar でなく Class-Path: dependency/commons-lang3-3.20.0.jar とする必要があります。

classpathPrefix

Mavenビルド後に MANIFEST.MF を手動で書き換えることも可能ですが、<classpathPrefix> を使えばClass-Path属性にパスを追加できます。

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>app</artifactId>
    <version>1.0.0</version>

    <!-- 最小構成のPOMに以下を追加 -->
    <build>
        <plugins>
            <!--
             | Maven JAR Plugin による Maven Archiver 設定を行い
             | マニフェストファイルにMain-Class属性が追加されるよう構成する
            -->
            <plugin>
                <!-- Maven JAR Plugin -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.1</version>
                <!-- Maven Archiver 構成 -->
                <configuration>
                    <archive>
                        <manifest>
                            <!--
                             | Class-Path: commons-lang3-3.20.0.jar
                             | <dependencies>に記述した依存関係をClass-Path属性に追加する
                            -->
                            <addClasspath>true</addClasspath>
                            <!-- 依存するJARファイルの配置先に合わせてClass-Path属性に記述する接頭辞を追加する -->
                            <classpathPrefix>dependency/</classpathPrefix>
                            <!-- Main-Class: com.example.Main -->
                            <mainClass>com.example.Main</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

            <!--
             | Maven Dependency Plugin のcopy-dependenciesゴールを
             | デフォルトライフサイクルのpackageフェーズに紐づけ
             | 依存関係に含まれるJARファイルがコピーされるよう構成する
            -->
            <plugin>
                <!-- Maven Dependency Plugin -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.10.0</version>
                <!-- ビルドライフサイクルのpackageフェーズにcopy-dependenciesゴールを紐づける -->
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <!-- 依存関係を追加 -->
    <dependencies>
        <!-- Apache Commons Lang -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.20.0</version>
        </dependency>
    </dependencies>
</project>

このように pom.xml を修正することで、Class-Path属性には、接頭辞としてdependency/が付加された Class-Path: dependency/commons-lang3-3.20.0.jar が追加されます。( Altering The Classpath: Defining a Classpath Directory Prefix

実際に作成されたマニフェストファイルを確認してみると、想定通りになっています。

MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.4.1
Build-Jdk-Spec: 25
Class-Path: dependency/commons-lang3-3.20.0.jar
Main-Class: com.example.Main

これにより、依存関係JARの実体のコピー及びそのパスのClass-Path属性への追加が、Mavenビルドによって全て自動で行われるため、一切の手動操作を行うことなく、ビルド後すぐに実行可能JARファイルを起動することができるようになります。

> mvn clean package
> java -jar target\app-1.0.0.jar
Person01 has been created!

outputDirectory

また、maven-dependency-plugin のコピー先のデフォルト値は、<outputDirectory> > Default より、${project.build.directory}/dependency です。

${project.build.directory} は、build > directoryThe BaseBuild Element Set > directory より、targetディレクトリのことです。従って、${project.build.directory}/dependency とは、./target/dependency を指しています。

また、help:evaluate ゴールを使用することにより、変数にどのような値が入っているか直接ローカル環境の状態を確認することも可能です。

> mvn help:evaluate -Dexpression=project.build.directory
・・・略・・・
C:\xxx\project\target
・・・略・・・

従って、依存JARファイルの出力先をデフォルトの dependency から変えたい場合、<outputDirectory> にパス名を指定すれば変更可能です。その場合、Class-Path属性の接頭辞も併せて修正する必要がある点には注意が必要になります。

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>app</artifactId>
    <version>1.0.0</version>

    <!-- 最小構成のPOMに以下を追加 -->
    <build>
        <plugins>
            <!--
             | Maven JAR Plugin による Maven Archiver 設定を行い
             | マニフェストファイルにMain-Class属性が追加されるよう構成する
            -->
            <plugin>
                <!-- Maven JAR Plugin -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.1</version>
                <!-- Maven Archiver 構成 -->
                <configuration>
                    <archive>
                        <manifest>
                            <!--
                             | Class-Path: commons-lang3-3.20.0.jar
                             | <dependencies>に記述した依存関係をClass-Path属性に追加する
                            -->
                            <addClasspath>true</addClasspath>
                            <!-- 依存するJARファイルの配置先に合わせてClass-Path属性に記述する接頭辞を追加する -->
                            <classpathPrefix>lib/</classpathPrefix>
                            <!-- Main-Class: com.example.Main -->
                            <mainClass>com.example.Main</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

            <!--
             | Maven Dependency Plugin のcopy-dependenciesゴールを
             | デフォルトライフサイクルのpackageフェーズに紐づけ
             | 依存関係に含まれるJARファイルがコピーされるよう構成する
            -->
            <plugin>
                <!-- Maven Dependency Plugin -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.10.0</version>
                <!-- ビルドライフサイクルのpackageフェーズにcopy-dependenciesゴールを紐づける -->
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <!-- 依存関係コピー設定の構成 -->
                        <configuration>
                            <!-- 出力先ディレクトリ -->
                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <!-- 依存関係を追加 -->
    <dependencies>
        <!-- Apache Commons Lang -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.20.0</version>
        </dependency>
    </dependencies>
</project>
MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.4.1
Build-Jdk-Spec: 25
Class-Path: lib/commons-lang3-3.20.0.jar
Main-Class: com.example.Main

Mavenによる Fat JAR の作成方法

前述 の Maven Dependency Plugin で作成した実行可能JARファイルは、-jarオプションで起動できる実行可能JARファイルではありますが、Class-Path属性に記述された依存JARが無ければ実行することのできない、依存関係を持つ実行可能JARファイルです。単一で実行できるよう Fat JAR として作成するには、別のプラグインで作成する必要があります。

Maven Assembly Plugin

ここで利用できるのが、Maven Assembly Plugin です。maven-dependency-plugin に代わり maven-assembly-plugin を使うことで、依存関係を有する単一の実行可能JAR( Fat JAR )を作成することができます。( ※ Fat JAR 作成の一点に限れば、依存関係が多く衝突した場合にリネームできる機能を持つ、Fat JAR 作成特化の Maven Shade Plugin も有用です。)

Dependency Plugin と共存させることができる(成果物として別々の異なるJARが作成される)ため、ここでは pom.xml に Assembly Plugin の設定を追加で記述します。

また、Maven JAR Plugin の設定を参照するわけではないため、実行可能JARファイルとするためのMain-Class属性用 <mainClass> 指定などは、別途 Assembly Plugin 用に個別に記述しておく必要があります。( Advanced Configuration

pom.xml への Assembly Plugin の設定記述は、次のようになります。( Execution: Building an Assembly )ここでは <descriptorRefs> に、事前定義済み記述子の jar-with-dependencies を指定することで、依存関係を含むJARファイルを作成しています。

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>app</artifactId>
    <version>1.0.0</version>

    <!-- 最小構成のPOMに以下を追加 -->
    <build>
        <plugins>
            <!--
             | Maven JAR Plugin による Maven Archiver 設定を行い
             | マニフェストファイルにMain-Class属性が追加されるよう構成する
            -->
            <plugin>
                <!-- Maven JAR Plugin -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.1</version>
                <!-- Maven Archiver 構成 -->
                <configuration>
                    <archive>
                        <manifest>
                            <!--
                             | Class-Path: commons-lang3-3.20.0.jar
                             | <dependencies>に記述した依存関係をClass-Path属性に追加する
                            -->
                            <addClasspath>true</addClasspath>
                            <!-- 依存するJARファイルの配置先に合わせてClass-Path属性に記述する接頭辞を追加する -->
                            <classpathPrefix>lib/</classpathPrefix>
                            <!-- Main-Class: com.example.Main -->
                            <mainClass>com.example.Main</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

            <!--
             | Maven Dependency Plugin のcopy-dependenciesゴールを
             | デフォルトライフサイクルのpackageフェーズに紐づけ
             | 依存関係に含まれるJARファイルがコピーされるよう構成する
            -->
            <plugin>
                <!-- Maven Dependency Plugin -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.10.0</version>
                <!-- ビルドライフサイクルのpackageフェーズにcopy-dependenciesゴールを紐づける -->
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <!-- 依存関係コピー設定の構成 -->
                        <configuration>
                            <!-- 出力先ディレクトリ -->
                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <!--
             | Maven Assembly Plugin の Fat JAR 向け組み込み記述子を利用し
             | packageフェーズで独自のアーカイブ設定に基づき実行可能JARファイルが作成されるよう構成する
            -->
            <plugin>
                <!-- Maven Assembly Plugin -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.8.0</version>
                <!--
                 | アセンブリ記述子とアーカイブ設定の構成
                 | ここでは Fat JAR 向けの事前定義済み記述子を利用する
                -->
                <configuration>
                    <descriptorRefs>
                        <!-- 依存関係を含むJARファイル用の組み込み記述子 -->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <!-- Maven JAR Plugin のアーカイブ設定とは別で独自にMain-Class属性を指定 -->
                        <manifest>
                            <!-- Main-Class: com.example.Main -->
                            <mainClass>com.example.Main</mainClass>
                        </manifest>
                    </archive>
                </configuration>
                <!-- ビルドライフサイクルのpackageフェーズにsingleゴールを紐づける -->
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <!-- 依存関係を追加 -->
    <dependencies>
        <!-- Apache Commons Lang -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.20.0</version>
        </dependency>
    </dependencies>
</project>

packageフェーズにsingleゴールを紐づけているため、> mvn clean package により、targetディレクトリ下に app-1.0.0-jar-with-dependencies.jar が作成されます。Dependency Plugin による app-1.0.0.jar とは別に、Assembly Plugin により app-1.0.0-jar-with-dependencies.jar が作られ、これが Fat JAR です。

# Dependency Plugin と Assembly Plugin による二種類の実行可能JARを作成
> mvn clean package

# 依存関係を持つ実行可能JARを実行
> java -jar target\app-1.0.0.jar
Person01 has been created!

# 依存関係を含む実行可能JARを実行
> java -jar target\app-1.0.0-jar-with-dependencies.jar
Person01 has been created!

マニフェストファイルを比較してみると、単一の実行可能JAR(Fat JAR)には、Class-Path属性が記述されていないことが分かります。代わりに、commons-lang3-3.20.0.jar がなくても良いようorg.apache.commons.lang3パッケージの全てのクラスファイルが同梱されます。

app-1.0.0.jar/META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.4.1
Build-Jdk-Spec: 25
Class-Path: lib/commons-lang3-3.20.0.jar
Main-Class: com.example.Main

app-1.0.0-jar-with-dependencies.jar/META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven Archiver 3.6.5
Build-Jdk-Spec: 25
Main-Class: com.example.Main

appendAssemblyId

依存関係を持つ依存先JARが必要なJARファイルと、Fat JAR の両方を分けて作成する必要がない場合、Assembly Plugin の成果物名を変更することもできます。デフォルトでは、指定した記述子( ここでは jar-with-dependencies )が接尾辞に付加されるため、app-1.0.0 + jar-with-dependencies = app-1.0.0-jar-with-dependencies.jar となっています。

<appendAssemblyId>false を指定することで、この挙動を変更し、Assembly Plugin により作成された記述子なしのJARファイルを、メインプロジェクトの成果物として置き換えることが可能です。

Fat JAR 作成用 pom.xml( Maven Assembly Plugin )

さらにこの場合、もともと依存関係を持つ実行可能JARファイル用に記述していた JAR Plugin と Dependency Plugin の設定は不要になるため、単に Fat JAR だけを作成したい場合、最終的に Fat JAR 作成用の pom.xml 最小構成は次のようになります。

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>app</artifactId>
    <version>1.0.0</version>

    <!-- 最小構成のPOMに以下を追加 -->
    <build>
        <plugins>
            <!--
             | Maven Assembly Plugin の Fat JAR 向け組み込み記述子を利用し
             | packageフェーズで独自のアーカイブ設定に基づき実行可能JARファイルが作成されるよう構成する
            -->
            <plugin>
                <!-- Maven Assembly Plugin -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.8.0</version>
                <!--
                 | アセンブリ記述子とアーカイブ設定の構成
                 | ここでは Fat JAR 向けの事前定義済み記述子を利用する
                -->
                <configuration>
                    <descriptorRefs>
                        <!-- 依存関係を含むJARファイル用の組み込み記述子 -->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <!-- Maven JAR Plugin のアーカイブ設定とは別で独自にMain-Class属性を指定 -->
                        <manifest>
                            <!-- Main-Class: com.example.Main -->
                            <mainClass>com.example.Main</mainClass>
                        </manifest>
                    </archive>
                    <!--
                     | 作成されるJARの接尾辞からアセンブリ記述子を除外することで Fat JAR をメインの成果物とする
                     | これにより Maven JAR Plugin と Maven Dependency Plugin の成果物は上書きされるため設定自体不要となる
                    -->
                    <appendAssemblyId>false</appendAssemblyId>
                </configuration>
                <!-- ビルドライフサイクルのpackageフェーズにsingleゴールを紐づける -->
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <!-- 依存関係を追加 -->
    <dependencies>
        <!-- Apache Commons Lang -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.20.0</version>
        </dependency>
    </dependencies>
</project>

別の選択肢( Maven Shade Plugin )

ここまでは比較のため Maven Assembly Plugin を例に挙げてきましたが、Fat JAR の作成に特化した Maven Shade Plugin を使用する選択肢もあります。

Fat JAR は、文脈によって Uber JAR や Shaded JAR といった表現で説明されることもあります。

実行可能 jar ( “uber jar” または “fat jar” と呼ばれることもあります) は、コンパイルされたクラスとコードの実行に必要なすべての jar 依存関係を含むアーカイブです。

初めての Spring Boot アプリケーションの開発 > 実行可能 Jar の作成

この問題を解決するために、多くの開発者は「シェーディングされた」jar ファイルを使用します(原文:many developers use “shaded” jars.)。シェーディングされた jar は、すべての jar からのすべてのクラスを単一の “uber jar” にパッケージ化します。

実行可能な Jar 形式 > ネストされた JAR

This plugin provides the capability to package the artifact in an uber-jar, including its dependencies and to shade – i.e. rename – the packages of some of the dependencies.

このプラグインは、依存関係を含めたアーティファクトを「uber-jar」としてパッケージ化し、一部の依存関係のパッケージ名を「シェード」(つまりリネーム)する機能を提供します。

Maven Shade Plugin

Fat JAR 作成用 pom.xml( Maven Shade Plugin )

Shade Plugin を使用すれば、pom.xml も比較的少ない記述量でシンプルに Fat JAR を作成することが可能です。( Executable JAR )また Assembly Plugin と異なり、依存関係に競合が生じた場合にパッケージ名をリネームする機能も有しています。

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>app</artifactId>
    <version>1.0.0</version>

    <!-- 最小構成のPOMに以下を追加 -->
    <build>
        <plugins>
            <!--
             | Maven Shade Plugin を利用し
             | packageフェーズで単一の実行可能JARファイルが作成されるよう構成する
            -->
            <plugin>
                <!-- Maven Shade Plugin -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.6.2</version>
                <!-- 実行構成 -->
                <executions>
                    <execution>
                        <!-- ビルドライフサイクルのpackageフェーズにshadeゴールを紐づける -->
                        <phase>package</phase>
                        <goals>
                          <goal>shade</goal>
                        </goals>
                        <configuration>
                              <transformers>
                                  <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                      <!-- Main-Class属性を指定 -->
                                      <mainClass>com.example.Main</mainClass>
                                  </transformer>
                              </transformers>
                          </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <!-- 依存関係を追加 -->
    <dependencies>
        <!-- Apache Commons Lang -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.20.0</version>
        </dependency>
    </dependencies>
</project>
> mvn clean package
> java -jar target\app-1.0.0.jar
Person01 has been created!

Dependency Plugin と Assembly Plugin の比較

二つのプラグインによって作成される実行可能JARファイルは、前述 した依存関係のある実行可能JARファイル単一の実行可能JARファイル( Fat JAR )の違いを分かりやすく説明するものでもあります。ここでは、依存先のライブラリJARを外部に必要とする自作クラスのみをアーカイブした実行可能JARを「依存関係を持つ実行可能JARファイル」、依存先のライブラリを全て展開して同梱した Fat JAR を「依存関係を含む実行可能JARファイル」として整理しています。

  • Maven Dependency Plugin( maven-dependency-plugin
    • 依存関係を持つ実行可能JARファイル
    • 依存先のJARはClass-Path属性で指定したパスに配置する必要あり
  • Maven Assembly Plugin( maven-assembly-plugin
    • 依存関係を含む実行可能JARファイル
    • Fat JAR であり単一で実行可能
  • ※ ここではどちらも実行可能JARファイル
    • <mainClass>によりMain-Class属性が追加されているため)
  • ※ Fat JAR の作成
    • Maven Assembly Plugin( maven-assembly-plugin )以外の選択肢も有
    • Maven Shade Plugin( maven-shade-plugin )でも作成可能
ライブラリと自作クラスについての整理はこちらでも詳細を説明しています。