gradleで作るfatJarをカスタマイズしたい

# 概要

fatJarとは、Javaアプリケーション本体と依存ライブラリのすべてを1つのJarに固めたものです。今回「特定のライブラリは除外したfatJarを作りたい」という事情が発生しました。

fatJarはそれ単体で実行可能であることが大きなメリットなので、特定のものを除外する必要はあまりないかもしれませんが、今回調べたことをまとめておこうと思います。

# 環境

下記の環境でfatJarの作成を行いました。

```
Java : 1.8
gradle : 7.6.1
shadow : 7.1.2
```

shadowとはgradleでfatJarを作る際に広く利用されているpluginです。

https://github.com/johnrengelman/shadow

上記のgithubと[UserGuide](https://imperceptiblethoughts.com/shadow/introduction/)を参考にfatJarの作成を行いました。

# 利用したコード

私が利用したbuild.gradleの大まかなコードを記載しておきます。以下ではこのコードを変更しながら成果物がどのように変わるのかを見ていきます。

```gradle
plugins {
  id "java"
  id 'com.github.johnrengelman.shadow' version '7.1.2'
}

repositories {
  mavenCentral()
}

apply plugin: "java"
apply plugin: "com.github.johnrengelman.shadow"

sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
compileJava.options.encoding = 'UTF-8'
compileTestJava.options.encoding = 'UTF-8'

dependencies {
  // dependency1
}

shadowJar {
  baseName = project.name
  dependencies {
    // dependency2
  }
}
```

# jarタスクに関連する依存関係

上記のサンプルコードにおける`dependency1`の部分です。ここに依存関係を追記します。下記の3つdependencyの場合において、どのような挙動になるのかを見てみます。

- implementation
- runtimeOnly
- compileOnly

dependency1の部分を下記のように書き換えてみます。

```gradle
dependencies {
  implementation ("commons-lang:commons-lang:2.6")
  runtimeOnly ("org.apache.httpcomponents:httpclient:4.5.13") {
    exclude group: 'commons-logging', module: 'commons-logging'
  }
  compileOnly ("com.google.guava:guava:32.1.2-jre")
}
```

`./gradlew shadowJar`コマンドを実行し、fatJarを作ってみます。

## implementation

implementationはjarを実行する際に必要なものなので、成果物のfatJarには含まれていそうな気がしますが、実際にどうなったのかでコンパイルして見てみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/960294/40cee7eb-a44a-78eb-2c72-a1b804c071d0.png)

`org.apache.commons.lang`のパッケージが存在するので、implementationで定義したcommons-langは成果物に含まれていました。

## runtimeOnly

runtimeOnlyはjarをコンパイルするときは不要ですが、実行時には必要なものなのでfatJarに含まれてほしい気がします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/960294/f7dbbcf8-9fb9-c9e2-56d4-df1435f2dd13.png)

`mozila`と`org.apache.http`のパッケージが存在するので、runtimeOnlyで定義したhttpclientは成果物に含まれていました。

httpclientは推移的な依存関係を持っていて、下記のようになっています。

```
+--- org.apache.httpcomponents:httpclient:4.5.13
|    +--- org.apache.httpcomponents:httpcore:4.4.13
|    +--- commons-logging:commons-logging:1.2
|    \--- commons-codec:commons-codec:1.11
```

この中で`commons-logging:commons-logging`は意図的にexcludeしました。よって下記のようになることが期待されます。

- commons-loggingは成果物の中に含まれない
- httpcoreとcommons-codecは成果物に含まれる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/960294/af161fbe-b13a-dddc-12ca-e8c3912ab6f2.png)

`org.apache.commons.logging`パッケージが存在せず、`org.apache.commons.codec`、`org.apache.http.annotation`等のhttpclientに含まれないパッケージが存在しているので予想通りの結果になっていることがわかります。

## compileOnly

compileOnlyはコンパイル時にのみ必要で、実行時には不要なのでfatJarに含まれている必要はないと思われます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/960294/f22644ca-7154-e174-adac-ba9f982b5712.png)

`com.google`のパッケージが存在しないため、compileOnlyで定義した`guava`は成果物に含まれていませんでした。

## ここまでのまとめ

- implementation、runtimeOnlyでdependencyを定義した場合はfatJarに内包される
- compileOnlyでdependencyを定義した場合はfatJarに内包されない
- 推移的な依存ライブラリも考慮され、excludeした場合は除外される

# shadowJarタスクでのカスタマイズ

jarタスクのdependencyとは別に、shadowJarのタスクにもいくつかのオプションがあり、成果物をカスタマイズすることができます。

## filtering

下記のように、exclude、includeを記述して成果物に含めるファイルをフィルタリングすることができます。

```gradle
shadowJar {
  baseName = project.name
  exclude "qiita/QiitaObject.class"
}
```

こうすることでexcludeに指定したclassファイルは成果物のfatJarから除外されます。いろいろ調べてみた感じだと下記のようなものをexcludeするために使われることが多いようです。

```gradle
  exclude 'META-INF/*.SF'
  exclude 'META-INF/*.DSA'
  exclude 'META-INF/*.RSA'
```

## dependency

成果物のfatJarから除外する依存関係を定義することができます。`dependency2`の部分を下記のように書き換えます。

```gradle
  dependencies {
    exclude(dependency("commons-lang:commons-lang"))
  }
```

このようにしてshadowJarを作成すると、`commons-lang`が成果物の中から消えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/960294/4f7c0132-fc0a-2c25-755f-350f300eb9f6.png)

`org.apache.commons.lang`のパッケージがなくなりました。

公式によると、推移的な依存関係を含んだexcludeはサポートされていないようです。

## relocate

shaded-jarのように、内包するライブラリのパッケージを変更することができます。

shadowJarのタスクにrelocateの記述を追記します。

```gradle
shadowJar {
  baseName = project.name
  dependencies {
    exclude(dependency("commons-lang:commons-lang"))
  }
  relocate 'org.apache.http.annotation', 'qiita.annotation'
}
```

`httpcore`の`org.apache.http.annotation`のパッケージを`qiita.annotation`パッケージに移動します。成果物のfatJarを確認するとパッケージが移動していることが確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/960294/732ded37-6996-b408-c07a-add5d8ea5972.png)

# 参考文献

- https://imperceptiblethoughts.com/shadow/
- https://github.com/johnrengelman/shadow