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 設定を変更する方法は複雑かつややこしいように感じたので、できれば自動化に近い仕組みを使いたかったので選択肢から外れました。
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"
}
以上で無事やりたいことが達成できました。
最後に
かなり簡単に目的を達成できる方法だと感じました。
追加の設定も特に不要なのがありがたいですね。