パッケージ管理していなかった既存システムに後付けで Gradle を導入した話

# この資料について

<!-- TODO: 各種リンク先と表記が 2018 のものになっていないか要注意。特に twitter リンク先に注意！！ -->
[JJUG CCC 2019 Spring](http://www.java-users.jp/ccc2019spring/#/time-table) の表題セッションの発表資料を Qiita にて公開したものです (twitter: [#ccc_a2](https://twitter.com/search?q=%23ccc_a2))。

<!-- TODO: Speakers deck のスクショ画像に置き換える -->
<!-- TODO: リンク先要修正！！ -->
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52205/77301c33-b2ad-62b6-b801-074e125ef907.png)](https://speakerdeck.com/saiya_moebius/how-to-setup-gradle-to-improve-legacy-java-system)

内容は上記のスライドとほぼ同じですので、見やすい方で見ていただければと思います。





# はじめに


## 今でこそ一般化している Maven や Gradle 等ですが...

<!-- TODO: 時系列の裏をとっておく -->

- 2004 : Maven 1.0
- 2005 : Maven 2.0
- 2006 : Ivy 1.4.1 が Apache Incubator に入る
   - <small>※ Ivy = Scala (sbt) などでも使われているパッケージ管理ツール</small>
- 2007 : Gradle の Initial release
- 2010 : Maven 3.0

弊社 エムスリー(株) の創業は 2000 年 (`J2SE` こと Java 2 あたりの時代)。
Maven, Gradle, Ivy がまだ無い時代から Java を使っていました。


## Maven, Gradle がない時代に Java をフル活用した結果...

`git clone` すると以下のようなファイルが...

```
lib/app/hibernate-validator-4.3.0.Final.jar
lib/app/aws-java-sdk-core-1.10.27.jar
lib/app/aws-java-sdk-s3-1.10.27.jar
lib/app/generator-runtime-0.3.0-20050901.1909.jar
lib/app/xalan.jar
lib/app/crimson.jar
                 ... 以下延々と続く ...
```

歴史とともに利用ライブラリが増えた結果、182 個の `.jar` が転がっていた。


## `.jar` を直接管理するオペレーション

このようなプロセスでメンテナンスしている状態だった:

![適当なところから手作業で jar をダウンロードして Git に入れるという闇のオペレーションの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52205/2e46ee1c-eaa4-7cd7-4912-5093505af9a9.png)



## ライブラリのバージョンアップのつらみ

例: 新機能を使うために `aws-java-sdk` のバージョンを上げたい

<center>↓</center>

aws-java-sdk 系は複数のライブラリに間接依存

<center>↓</center>

182 個の `.jar` をどうバージョンアップするべきか分からない

<center>↓</center>

AWS の新しい機能を使うのを諦める :cry:


## 間接依存のつらみ

ある jar を使っていないので消したい

<center>↓</center>

もしかしたら、いずれかの jar がそれに依存しているかも...

<center>↓</center>

不要 jar の掃除ができない

<center>↓</center>

JVM バージョンアップや脆弱性対応などの足かせに :cry:


## 各種ツールを使えないつらみ

脆弱性情報をチェックしたい

<center>↓</center>

定期的に目検で 182 個の `.jar` と脆弱性情報を照合

<center>↓</center>

しんどい & 限界がある

<center>↓</center>

放置しがちになってしまう



Gradle, maven 等であれば OWASP `dependency-check` plugin で自動化できるのに...


## こうなってしまっていた背景

当該システム以外は maven, Gradle 導入済みであるのに対して...

- 当該システムは創業時からのものであり、特に歴史が長い
- Ant のビルドスクリプトがガッツリ組まれており、一括リプレースはつらい
- 普段の開発作業は問題なく回るので... <small>(※ ライブラリ周りの闇を無視している限りは)</small>


こういった事情でパッケージ管理ツールが入っていない事例は世の中にもあるのではないでしょうか...。


## パッケージ管理ツール(Gradle, maven など)を入れましょう！

とはいえ、既存のプロジェクトにどうやって導入するのか...？

- 作業の負担が大きくなりがちな点をどうするのか？
- 入れることによる副作用や影響をどうするのか？

→ 次章へ




<!--==================================== # Section: 戦略パート (Gradle) ====================================-->

## パッケージ管理ツール導入の戦略

〜 歴史あるシステムの改善の __実現可能性のある__ スコープ定義 〜


## パッケージ管理ツールの導入での課題

既存のシステムへの後付け導入では、課題がいろいろ現れる:

- ツール導入前後で jar が変わってしまうことを良しとするか？
- 再入手困難な jar や central と一致しない jar をどうするか？
- ビルドプロセスにも手を入れるか？

課題に振り回されないために、ポリシーがあったほうが良い。


## 弊社事例で設定したポリシー

- パッケージ管理ツール導入前後で、`.jar` ファイルに差分を出さないようにする
  - アプリへの影響懸念・テスト負担を抑えるため
  - 差分を回避不能なものは、無害な差分しかないことを確かめる <small>(手法は後述)</small>
  - 棚卸しや ver up は次の改善のスコープにする
- ビルドプロセスも手を入れない
  - まずはパッケージ管理を改善する！


パッケージ管理ツールの恩恵を得ることにスコープを絞り、
小さく・意味のあるチャレンジをする状況を作り出した。


## パッケージ管理ツール導入前の状態

![適当なところから手作業で jar をダウンロードして Git に入れるという闇のオペレーションの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52205/2e46ee1c-eaa4-7cd7-4912-5093505af9a9.png)


## パッケージ管理ツール導入でこうなった

![jar のダウンロードだけを Gradle 化した図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52205/a3fe3dcb-595f-63db-9d35-1d35bd07a435.png)


## Git に `.jar` を上げることの是非

:+1: メリット:

- Ant などからの移行が容易
- 常に確実に同じ jar ファイルを使える

:-1: デメリット:

- Ant が残る
  - Ant で真に困り事が発生したらその時にリプレース
- `.jar` の更新を繰り返すとレポジトリが肥大化するであろう
  - そのうち Git submodule なりに分離する


## レガシー改善の戦略

真に `困っていること` ・ `変えるメリットがあること` を見極める

<center>↓</center>

そのために必要な `必要十分な最小スコープ` を考える

<center>↓</center>

`小さいチャレンジ` で目的を達成する <small>(コスパ高, 失敗してもダメージ小)</small>


「レガシーだから全部抹殺するしかない！！！」ではなく、
今ある良い点や変える必要がない点は踏襲しつつ改善。




# テクニック(戦術)パート

## パッケージ管理ツール導入テクニック: Gradle を後付け導入する方法

〜 後付け導入でありがちな諸課題への具体的な対処方法 〜


## Gradle or Maven

今回のような用途には Gradle の方がオススメ:

- 柔軟性: Groovy や Kotlin DSL で柔軟な処理を記述可能
  - 歴史的なプロジェクトの構成に maven は対応できないことがある
- 調査性: Groovy Javadoc のほうが読みやすい <small>(主観)</small>
- 生産性: IDE のデバッガをフル活用できる
  - ブラックボックス的に格闘しなくて済む
- 再現性: Gradle wrapper があるため、動作の再現性を(比較的)確保しやすい



<small>※ 一般的には Maven の方が優れている事柄もある</small>


## Gradle によるファイルコピー

既存ビルドシステムを活かしつつ Gradle 導入するため、
以下のファイルを Gradle で所定のディレクトリにコピーしたい:

- コンパイル時・実行時に依存する jar
- 単体テスト時に依存する jar
- Source jar

また、 `.classpath` ファイル<small>(後述)</small>も自動でメンテナンスしたい。


## Gradle で jar ファイルをダウンロード

意外と簡単:

```groovy
def compileJarCopyTo = 'development/lib/app'

task downloadJars() {
  doLast {
    // コピー先にある *.jar を消す
    delete fileTree(dir: compileJarCopyTo, include: '*.jar')

    // コピーする
    copy {
      from configurations.compile
      into compileJarCopyTo
    }
  }
}
```


## `copy` task

コピー対象 jar の指定や include, exclude なども出来て便利:

```groovy
copy {
  // testCompile = test で依存する jar をコピー
  from configurations.testCompile

  into testCompileJarCopyTo

  // 指定したパターンに該当する jar は無視
  exclude 'jmockit-*.jar'
}
```


## Source jar のコピーもやれば出来る

Source jar をコピーするためには、これらゴリゴリ操作するコードを書く:

```groovy
import org.gradle.jvm.JvmLibrary
import org.gradle.language.base.artifact.SourcesArtifact
import org.gradle.api.artifacts.query.ArtifactResolutionQuery
import org.gradle.internal.component.external.model.DefaultModuleComponentIdentifier
```

<small>( `org.gradle.internal` も現れてしまっていますが... )</small>

→ 先人の Gradle 2.x 向けのコードを参考に、Gradle 5.x 対応版を<br> [Copy source jar files into directory](https://qiita.com/saiya_moebius/items/4d9604d754c45e75c406) (Qiita) へ投稿してあります


## `.classpath` ファイルの生成

IDE に jar やソースコードを読み込ませるための設定ファイル。
もともと Eclipse 用の形式だが、InteliJ IDEA にも対応している。

せっかくなのでこのファイルも Gradle でメンテナンスさせたい。

Gradle の `eclipse` plugin を適用するだけで生成できるが...


## `.classpath` 絶対パス問題

Plugin が生成する `.classpath` 中のパスは絶対パス。
環境依存のパスになってしまい、Git などで共有できず不便。

解決策:
- 各自の手元で `gradle eclipseClasspath` して `.classpath` を都度生成する
  - :-1: 全員が毎回実行する手間が発生
  - :-1: ビルドの再現性が下がる (手元では動いたのに！現象)
- 相対パス表記の `.classpath` を Gradle で生成し、jar ファイルと共に commit する
  - :+1: Jar ファイルのコピーなどとセットでまとめて実行するだけで OK


## `.classpath` を相対パス表記で生成する

`eclipse` plugin の `pathVariables '.': projectDir` ではなく、[参考サイト](http://nobeans.hatenablog.com/entry/20120706/1341554790) などにあるように plugin のフックを使って拡張すると良い:

```groovy
eclipse.classpath.file {
    whenMerged { classpath ->
        classpath.entries.findAll { entry -> entry.kind == 'lib' }.each {
            it.path = it.path.replaceFirst("${projectDir.absolutePath}/", '')
            it.exported = false
        }
    }
}
```


## `eclipse.classpath.file` の `whenMerged`

`whenMerged` で `.classpath` ファイルの内容を自由に制御できる

例えば:

- jar ファイルの参照先のパスを任意のロジックで書き換える
  - 弊社事例では、一部ファイルだけ異なるディレクトリに置いている
- `classpath.entries.sort` で `.classpath` の並び順を制御
  - JMockit のようなロード順依存するライブラリ対策で有用
- `classpath.getEntries().removeAll { return (条件式) }`
  - IDE に読み込ませたくない jar の除外が可能


## jar のファイル名の変化

手作業管理の jar ファイル名と、Gradle 由来の jar はファイル名が一致しないことが多い。例えば:

- 手動管理のファイル名: `crimson.jar`
- Gradle でのダウンロード結果: `crimson-1.1.3.jar`

ファイル名決め打ちで参照している場合に問題が出る。
特に以下がありがち:

- Ant などのビルドツールの `classpath` におけるファイル名の決め打ち
- プログラムの起動時の JVM の `classpath` オプションでのファイル名の決め打ち


## jar のファイル名が変わることへの対処策

1. (おすすめしない) 後処理で旧来のファイル名になるようにリネームする
   - :-1: リネーム元の group, artifact 名が変わった場合に事故る
   - :-1: 新しい jar が増えたときの対応に困る
2. 個別のライブラリを意識しないようにする
   - :+1: 特定ディレクトリ以下の jar をすべて読み込むだけで OK な設計にする
     - Ant, JVM `classpath` (>= 6), シェルなどの `*` ワイルドカードを活用
     - `*` の解釈がそれぞれ微妙に異なる点には要注意

後者は手間ではあるが、一過性の手間なので対応してしまったほうが長期的に良い。




<!--==================================== # Section: 戦略パート (Jar 管理) ====================================-->


## パッケージ管理ツール導入テクニック: 諸事情ある jar の管理

〜 歴史の彼方からやってきた jar ファイルを扱うための手法 〜


## 古い jar ファイルにありがちなこと

<!-- - 全部入り jar に内包される jar がカブってる -->

- jar を再入手できたがバイナリが一致しない
  - `SNAPSHOT` 版
  - 非 `SNAPSHOT` なのに差し替えリリースされている
  - Maven repository 間で違うバイナリが置いてある
  - 経緯不明の独自パッチ :imp:
- 公式ページが消えており、jar やソースを再入手できない

<!--

## 全部入り jar (fat jar)問題

`*-all.jar`, `*-with-dependency.jar` などの類。

依存するライブラリすべてを含んだ jar。

一見便利だが、意図せずに重複する class を読み込んでしまう原因にしかならないので、使わないほうが良い。

だいたい `*-core` といった名前で代替手段が用意されているはず。
-->


## jar のバイナリ不一致の原因を確かめる

不一致は放置せずに、具体的な差分を確認すると良い:

- パッケージ管理ツール導入の影響範囲・リスクを限定できる
- テストすべき範囲やテスト手法も明らかになる

筆者は Gradle 化前後で jar ファイルの SHA-1 ハッシュを出し、
ハッシュが一致しない jar について具体的な差分を調べた。


## jar ファイルの差分の調べ方

jar ファイルは zip であり、展開すると以下のファイルが出てくる:

1. `.class` ファイル (Java バイトコード)
2. `META-INF` などのメタデータファイル
3. その他、プロパティファイルなどのリソースファイル

`.class` 以外はほとんどテキスト比較ツールで比較できる。

<small>`lucene-analyzers-2.4.0` の `META-INF/MANIFEST.MF` で差分が出たときの例:</small>

```
8c8
< Implementation-Version: 2.4.0 701827 - 2008-10-05 16:46:27
> Implementation-Version: 2.4.0 701827 - 2008-10-05 16:44:47
```


## jar の差分の調べ方: class ファイル

`.class` ファイルは Java バイトコードのバイナリである。

JDK に入っている `javap -c` でテキスト表現に変換することで、
テキスト比較ツールで比較可能になる。

`guice-servlet-1.0.jar` で差分が出たときの例:

```
2c2
< final class com.google.inject.servlet.ServletScopes$1 implements com.google.inject.Scope {
> class com.google.inject.servlet.ServletScopes$1 implements com.google.inject.Scope {
```
<small>(`final` を足した jar で差し替えられている)</small>


## 再入手不能なライブラリ

当該ライブラリの利用停止以外の対策:

- (:imp: 非推奨) Central 等の公開レポジトリにアップロードする
   - :-1: ライセンスや道義的に好ましくない
   - :-1: 公知の jar と一致する確証のない jar を公開することに
- (:thinking: 微妙) 対象のライブラリの `.java` ファイルを直接使う
   - :-1: ソースを直接使うため、独自改造や密結合が起きがち
   - :-1: ライセンスによっては問題あり
- (おすすめ) 内部用のプライベートなレポジトリで管理
   - :+1: 権利問題が起きにくい
   - :+1: 当該ライブラリの利用状況の把握や削除がやりやすい


## プライベートレポジトリ

再入手不能 jar に限らず、内製のライブラリの管理などでも
プライベートなレポジトリがあると便利。

よくある構築方法:

1. ローカル上の maven レポジトリを `file://` で参照する
   - マシン間の同期が必要
   - ビルドの再現性を損ないがち
   - jar の紛失リスクやバックアップの手間もある
2. [Nexus](https://www.sonatype.com/nexus-repository-sonatype) や [Artifactory](https://jfrog.com/artifactory/) などのレポジトリサーバーを動かす (便利)
    - 詳細なセットアップ方法については触れないが、新規でセットアップするならば Nexus の方が楽


## プライベートレポジトリの利用

1. 世の中に存在するライブラリは、今までどおり maven central などから取得
2. プライベートなものは、プライベートレポジトリから取得

Gradle ならば以下のようにするだけで実現できる:

```groovy
repositories {
  maven {
    url 'http://central.maven.org/maven2/'    
  } // mavenCentral() という短縮記法はあえて使っていない (後述)

  maven {
    url "http://プライベートなレポジトリのURL"
  }
}
```


## レポジトリへ jar をアップロード

<!-- TODO: 読者レベルの想定値によってはこの記述は不要かも...。でも Gradle しっていて maven 知らない人向けとかに書いておくといいか？ -->

メタデータを Maven の XML (`pom.xml`) 形式で書いて jar と共にアップロードする。

一見とっつきにくいかもしれないが、今回の目的に絞れば簡単:

```xml
<project>
  <modelVersion>4.0.0</modelVersion>

  <!-- この jar 自体の識別情報 -->
  <groupId>com.m3</groupId> <artifactId>m3generator</artifactId>
  <version>1.0.0</version>

  <!-- この jar が依存する jar の情報 -->
  <dependencies>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    ...
```

<!--

`<dependency>` に使うための XML は https://mvnrepository.com から簡単に得られる:

![https://mvnrepository.com/artifact/log4j/log4j/1.2.17 のスクリーンショット](./img/mvnrepository.com/log4j.maven-sig.png)

-->


## <small>(Artifactory)</small> Auto generated POM には注意

Artifactory が POM を自動生成してくれるので便利に見えるが...

 `<dependencies>` が空だったり間違っていたりすることが多い

<center>↓</center>

アップロードした jar の依存ライブラリの情報を得られなくなる

<center>↓</center>

パッケージ管理ツールによる依存関係管理のメリットが失われる

自動生成に丸投げせずに `<dependencies>` をちゃんと書こう。


## レポジトリ間で重複するライブラリ

同名だが一致しない(差分がある)ライブラリがある場合、
どちらが読み込まれるか明示的に制御すべし:

- プライベートレポジトリへ別 version でアップロード
- または、Gradle 側で制御する (下記)

```groovy
repositories {
  maven { // Maven central
    url 'http://central.maven.org/maven2/'    

    content {
      excludeGroup 'com.sun.jmx' // このグループをここから取得しない
      excludeModule 'jdom', 'jdom' // このライブラリをここから取得しない
    }
    // 上記記述をするため mavenCentral() といった短縮記法は使っていない
  }
```

<!-- 
## `classifier` について

ライブラリには `group`, `artifact`, `version` (`GAV`) に加えて `classfier` も付けられる。
例えば、独自パッチを当てたライブラリをオリジナルと区別するのに便利な機能...に思える。

しかし、`classfier` の異なるライブラリは別ライブラリ扱いされてしまうので、
オリジナルのライブラリと独自パッチ版ライブラリを _両方_ 読み込んでしまうといったリスクが発生する。
片方を丁寧に `exclude` することで回避は可能だが面倒な上に漏れやすい。

`version` で区別するようにすれば、片方の `version` しか読み込まれないのが確実なので安全かつ楽。
-->

<!--

## `force` <small>(maven での dependencyManagement に類似)</small>

```groovy
configurations.all {
  resolutionStrategy {
    failOnVersionConflict()

    force 'commons-collections:commons-collections:3.1'
  }
}
```
-->



# 最後に


## Next Step

パッケージ管理ツールを入れたら特にやっておきたいこと:

- Version 衝突の検知
- classpath 上の重複の検知
- OWASP dependency check による脆弱性情報のチェック

JJUG 2018 Spring のスライドでより深く説明しています:

- タイトル: `Spring Boot と一般ライブラリの折り合いのつけかた`
  - [Speaker Deck](https://speakerdeck.com/saiya_moebius/spring-boot-to-ban-raiburarifalsezhe-rihe-ifalsetukekata) と [Qiita](https://qiita.com/saiya_moebius/items/200b072b9ba7b86e9407) 両方に同じ内容で掲載しています

※ 資料の後半は Spring Framework 関係なく使える情報です


## まとめ

今回、パッケージ管理ツールを入れた際のプロセス:

- ゴールを定義
  - パッケージ管理でどう良くなるかのビジョンを描く
- 戦略を構築
  - 実現のために何をするか・何をあえて「しない」のか
- テクニックで課題を解決: 今回言及した各種工夫
  - Gradle の活用
  - jar の差分チェック
  - プライベートレポジトリの活用

「闇が深くて手がつけられない」とも思える状況でも、
__適切なスコープを設定し、技術で実現する__ ことで前に進める。