おおくまねこ

職業プログラマーです。興味のある話題を書いています。

Java /Gradle shadow プラグインの力で fat Jar を簡単に作る

はじめに

動機

今はあまりなくなってしまいましたが、Java のプログラムを fat Jar として作って頒布する機会があったので、その時に試した方法について記載します。

Java は長期的に動くサーバーサイドの開発などに使うことが多いと思うので、そういうケースは少ないと思いますが、

Java の資産を使ったモジュールを作ったり、古いソフトウェアのメンテナンスなんかでそういうユースケースがあるという認識です。

 

Gradle の shadow plugin*1 を使えば簡単に fat Jar を作れました。

環境

今回、私の実行するために利用した環境は以下になります

fat Jar について

fat Jar とは依存関係含む、すべての class を含んだ jar ファイルのことです。

 

plugins {
id 'java'
}

group 'com.github.keyno63'
version '1.0-SNAPSHOT'

repositories {
mavenCentral()
}

dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
}


// jar 実行の manifest の設定
jar {
manifest {
attributes "Main-Class": "com.github.keyno63.app.Main"
}
}

 

例えば、Jackson を使ってコマンドラインから渡した Json 文字列を分解するような、

以下のようなプログラムがあったとします。

package com.github.keyno63.app;

import ...;

public class Main {
public static void main(String[] args) throws JsonProcessingException {
final List<String> argsList = Arrays.asList(args);
if (argsList.size() > 0) {
ObjectMapper mapper =
new ObjectMapper();
final JsonData json = mapper.readValue(argsList.get(0), JsonData.class);
System.out.println(json);
}
}


public static class JsonData {
private final String value;

@JsonCreator
public JsonData(@JsonProperty("json_key") String value) {
this.value = value;
}

@Override
public String toString() {
return String.format("""
{
"json_key": "%s"
}
""", value);
}
}
}

単純にビルドして実行すると以下のように失敗するかと思います。

> .\gradlew clean build

BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 executed
> java -classpath ".\build\libs\gradle-shadow-1.0-SNAPSHOT.jar" com.github.keyno63.app.Main '{\"json_key\": \"json_value\"}'

エラー: メイン・クラスcom.github.keyno63.app.Mainを初期化できません
原因: java.lang.NoClassDefFoundError: com/fasterxml/jackson/core/JsonProcessingException

依存関係の com.fasterxml.jacksonが生成した jar に含まれていないので、実行時に依存関係が解決されずに失敗します。

 

 実行するためには依存関係のある jar への classpath を通すと実行することもできます。

> java -classpath "<jackson の jar への path>;.\build\libs\gradle-shadow-1.0-SNAPSHOT.jar" com.github.keyno63.app.Main '{\"json_key\": \"json_value\"}'
{
"json_key": "json_value"
}

 

それでも動くのですが、頒布する場合とかに利用してもらう人に依存関係の jar ファイルを用意してもらうのもハードルが高いので、fat Jar を作ることにしました。

 

fat Jar にすれば、依存関係をすべて含むその jar のみで完結して実行することができます。

> java -jar .\build\libs\gradle-shadow-1.0-SNAPSHOT-all.jar '{\"json_key\":\"json_value\"}'
{
"json_key": "json_value"
}

 

fat Jar を作る選択肢

fat Jar を作る他の選択肢としてあがるのは以下になるかと思います。

  • build.gradle 設定を変更して作る*2
  • gradle-fatjar-pluginを使う*3
  • gradle shadow plugin を使う

 

build.gradle 設定を変更する方法は複雑かつややこしいように感じたので、できれば自動化に近い仕組みを使いたかったので選択肢から外れました。

gradle-fatjar は 2015 年以降更新されていないので、選択肢から外れました。

 

以上の理由から shadow プラグインを使うようしました。

Gradlew にshadow プラグインを使う

すること

対応することは以下の2点です。

  • plugin に shadow を追加する
  • gradle shadow コマンドでfatJar を作る

build.gradle の編集

build.gradle にすることは plugins に shadows を追加するだけです。

plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '7.1.0' // 追加
}

jar をそのまま実行できるように manifest の設定を追加しておくのもお勧めです。

// jar 実行の manifest の設定
jar {
manifest {
attributes "Main-Class": "com.github.keyno63.app.Main"
}
}

 

fat Jar 生成

あとはターミナル、およびIDEのコマンドから gradle shadowJar相当を実行します。

デフォルト設定のままであれば .\build\libs配下に実行用の jar ができています。今回であれば gradle-shadow-1.0-SNAPSHOT-all.jar というのができています。

> .\gradlew clean shadowJar
> dir .\build\libs\gradle-shadow-1.0-SNAPSHOT-all.jar

jar の中身をみると、依存関係のパッケージのクラスも含まれているのがわかります。

> jar tf .\build\libs\gradle-shadow-1.0-SNAPSHOT-all.jar
META-INF/
META-INF/MANIFEST.MF
com/
com/github/
com/github/keyno63/
com/github/keyno63/app/
com/github/keyno63/app/Main$JsonData.class
com/github/keyno63/app/Main.class
META-INF/LICENSE
META-INF/maven/
META-INF/maven/com.fasterxml.jackson.core/
:

動作確認

生成した jar を指定して、実行可能なのが確認できます。

> java -jar .\build\libs\gradle-shadow-1.0-SNAPSHOT-all.jar '{\"json_key\": \"json_value\"}'
{
"json_key": "json_value"
}

以上で無事やりたいことが達成できました。

 

最後に

かなり簡単に目的を達成できる方法だと感じました。

追加の設定も特に不要なのがありがたいですね。