この資料について
JJUG CCC 2019 Spring の表題セッションの発表資料を Qiita にて公開したものです (twitter: #ccc_a2)。
内容は上記のスライドとほぼ同じですので、見やすい方で見ていただければと思います。
はじめに
今でこそ一般化している Maven や Gradle 等ですが...
- 2004 : Maven 1.0
- 2005 : Maven 2.0
- 2006 : Ivy 1.4.1 が Apache Incubator に入る
- ※ Ivy = Scala (sbt) などでも使われているパッケージ管理ツール
- 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
を直接管理するオペレーション
このようなプロセスでメンテナンスしている状態だった:
ライブラリのバージョンアップのつらみ
例: 新機能を使うために aws-java-sdk
のバージョンを上げたい
aws-java-sdk 系は複数のライブラリに間接依存
↓182 個の .jar
をどうバージョンアップするべきか分からない
AWS の新しい機能を使うのを諦める
間接依存のつらみ
ある jar を使っていないので消したい
↓もしかしたら、いずれかの jar がそれに依存しているかも...
↓不要 jar の掃除ができない
↓JVM バージョンアップや脆弱性対応などの足かせに
各種ツールを使えないつらみ
脆弱性情報をチェックしたい
↓定期的に目検で 182 個の .jar
と脆弱性情報を照合
しんどい & 限界がある
↓放置しがちになってしまう
Gradle, maven 等であれば OWASP dependency-check
plugin で自動化できるのに...
こうなってしまっていた背景
当該システム以外は maven, Gradle 導入済みであるのに対して...
- 当該システムは創業時からのものであり、特に歴史が長い
- Ant のビルドスクリプトがガッツリ組まれており、一括リプレースはつらい
- 普段の開発作業は問題なく回るので... (※ ライブラリ周りの闇を無視している限りは)
こういった事情でパッケージ管理ツールが入っていない事例は世の中にもあるのではないでしょうか...。
パッケージ管理ツール(Gradle, maven など)を入れましょう!
とはいえ、既存のプロジェクトにどうやって導入するのか...?
- 作業の負担が大きくなりがちな点をどうするのか?
- 入れることによる副作用や影響をどうするのか?
→ 次章へ
パッケージ管理ツール導入の戦略
〜 歴史あるシステムの改善の 実現可能性のある スコープ定義 〜
パッケージ管理ツールの導入での課題
既存のシステムへの後付け導入では、課題がいろいろ現れる:
- ツール導入前後で jar が変わってしまうことを良しとするか?
- 再入手困難な jar や central と一致しない jar をどうするか?
- ビルドプロセスにも手を入れるか?
課題に振り回されないために、ポリシーがあったほうが良い。
弊社事例で設定したポリシー
- パッケージ管理ツール導入前後で、
.jar
ファイルに差分を出さないようにする- アプリへの影響懸念・テスト負担を抑えるため
- 差分を回避不能なものは、無害な差分しかないことを確かめる (手法は後述)
- 棚卸しや ver up は次の改善のスコープにする
- ビルドプロセスも手を入れない
- まずはパッケージ管理を改善する!
パッケージ管理ツールの恩恵を得ることにスコープを絞り、
小さく・意味のあるチャレンジをする状況を作り出した。
パッケージ管理ツール導入前の状態
パッケージ管理ツール導入でこうなった
Git に .jar
を上げることの是非
メリット:
- Ant などからの移行が容易
- 常に確実に同じ jar ファイルを使える
デメリット:
- Ant が残る
- Ant で真に困り事が発生したらその時にリプレース
-
.jar
の更新を繰り返すとレポジトリが肥大化するであろう- そのうち Git submodule なりに分離する
レガシー改善の戦略
真に 困っていること
・ 変えるメリットがあること
を見極める
そのために必要な 必要十分な最小スコープ
を考える
小さいチャレンジ
で目的を達成する (コスパ高, 失敗してもダメージ小)
「レガシーだから全部抹殺するしかない!!!」ではなく、
今ある良い点や変える必要がない点は踏襲しつつ改善。
テクニック(戦術)パート
パッケージ管理ツール導入テクニック: Gradle を後付け導入する方法
〜 後付け導入でありがちな諸課題への具体的な対処方法 〜
Gradle or Maven
今回のような用途には Gradle の方がオススメ:
- 柔軟性: Groovy や Kotlin DSL で柔軟な処理を記述可能
- 歴史的なプロジェクトの構成に maven は対応できないことがある
- 調査性: Groovy Javadoc のほうが読みやすい (主観)
- 生産性: IDE のデバッガをフル活用できる
- ブラックボックス的に格闘しなくて済む
- 再現性: Gradle wrapper があるため、動作の再現性を(比較的)確保しやすい
※ 一般的には Maven の方が優れている事柄もある
Gradle によるファイルコピー
既存ビルドシステムを活かしつつ Gradle 導入するため、
以下のファイルを Gradle で所定のディレクトリにコピーしたい:
- コンパイル時・実行時に依存する jar
- 単体テスト時に依存する jar
- Source jar
また、 .classpath
ファイル(後述)も自動でメンテナンスしたい。
Gradle で jar ファイルをダウンロード
意外と簡単:
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 なども出来て便利:
copy {
// testCompile = test で依存する jar をコピー
from configurations.testCompile
into testCompileJarCopyTo
// 指定したパターンに該当する jar は無視
exclude 'jmockit-*.jar'
}
Source jar のコピーもやれば出来る
Source jar をコピーするためには、これらゴリゴリ操作するコードを書く:
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
( org.gradle.internal
も現れてしまっていますが... )
→ 先人の Gradle 2.x 向けのコードを参考に、Gradle 5.x 対応版を
Copy source jar files into directory (Qiita) へ投稿してあります
.classpath
ファイルの生成
IDE に jar やソースコードを読み込ませるための設定ファイル。
もともと Eclipse 用の形式だが、InteliJ IDEA にも対応している。
せっかくなのでこのファイルも Gradle でメンテナンスさせたい。
Gradle の eclipse
plugin を適用するだけで生成できるが...
.classpath
絶対パス問題
Plugin が生成する .classpath
中のパスは絶対パス。
環境依存のパスになってしまい、Git などで共有できず不便。
解決策:
- 各自の手元で
gradle eclipseClasspath
して.classpath
を都度生成する- 全員が毎回実行する手間が発生
- ビルドの再現性が下がる (手元では動いたのに!現象)
- 相対パス表記の
.classpath
を Gradle で生成し、jar ファイルと共に commit する- Jar ファイルのコピーなどとセットでまとめて実行するだけで OK
.classpath
を相対パス表記で生成する
eclipse
plugin の pathVariables '.': projectDir
ではなく、参考サイト などにあるように plugin のフックを使って拡張すると良い:
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 のファイル名が変わることへの対処策
- (おすすめしない) 後処理で旧来のファイル名になるようにリネームする
- リネーム元の group, artifact 名が変わった場合に事故る
- 新しい jar が増えたときの対応に困る
- 個別のライブラリを意識しないようにする
-
特定ディレクトリ以下の jar をすべて読み込むだけで OK な設計にする
- Ant, JVM
classpath
(>= 6), シェルなどの*
ワイルドカードを活用 -
*
の解釈がそれぞれ微妙に異なる点には要注意
- Ant, JVM
-
特定ディレクトリ以下の jar をすべて読み込むだけで OK な設計にする
後者は手間ではあるが、一過性の手間なので対応してしまったほうが長期的に良い。
パッケージ管理ツール導入テクニック: 諸事情ある jar の管理
〜 歴史の彼方からやってきた jar ファイルを扱うための手法 〜
古い jar ファイルにありがちなこと
- jar を再入手できたがバイナリが一致しない
-
SNAPSHOT
版 - 非
SNAPSHOT
なのに差し替えリリースされている - Maven repository 間で違うバイナリが置いてある
- 経緯不明の独自パッチ
-
- 公式ページが消えており、jar やソースを再入手できない
jar のバイナリ不一致の原因を確かめる
不一致は放置せずに、具体的な差分を確認すると良い:
- パッケージ管理ツール導入の影響範囲・リスクを限定できる
- テストすべき範囲やテスト手法も明らかになる
筆者は Gradle 化前後で jar ファイルの SHA-1 ハッシュを出し、
ハッシュが一致しない jar について具体的な差分を調べた。
jar ファイルの差分の調べ方
jar ファイルは zip であり、展開すると以下のファイルが出てくる:
-
.class
ファイル (Java バイトコード) -
META-INF
などのメタデータファイル - その他、プロパティファイルなどのリソースファイル
.class
以外はほとんどテキスト比較ツールで比較できる。
lucene-analyzers-2.4.0
の META-INF/MANIFEST.MF
で差分が出たときの例:
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 {
(final
を足した jar で差し替えられている)
再入手不能なライブラリ
当該ライブラリの利用停止以外の対策:
- ( 非推奨) Central 等の公開レポジトリにアップロードする
- ライセンスや道義的に好ましくない
- 公知の jar と一致する確証のない jar を公開することに
- ( 微妙) 対象のライブラリの
.java
ファイルを直接使う- ソースを直接使うため、独自改造や密結合が起きがち
- ライセンスによっては問題あり
- (おすすめ) 内部用のプライベートなレポジトリで管理
- 権利問題が起きにくい
- 当該ライブラリの利用状況の把握や削除がやりやすい
プライベートレポジトリ
再入手不能 jar に限らず、内製のライブラリの管理などでも
プライベートなレポジトリがあると便利。
よくある構築方法:
- ローカル上の maven レポジトリを
file://
で参照する- マシン間の同期が必要
- ビルドの再現性を損ないがち
- jar の紛失リスクやバックアップの手間もある
-
Nexus や Artifactory などのレポジトリサーバーを動かす (便利)
- 詳細なセットアップ方法については触れないが、新規でセットアップするならば Nexus の方が楽
プライベートレポジトリの利用
- 世の中に存在するライブラリは、今までどおり maven central などから取得
- プライベートなものは、プライベートレポジトリから取得
Gradle ならば以下のようにするだけで実現できる:
repositories {
maven {
url 'http://central.maven.org/maven2/'
} // mavenCentral() という短縮記法はあえて使っていない (後述)
maven {
url "http://プライベートなレポジトリのURL"
}
}
レポジトリへ jar をアップロード
メタデータを Maven の XML (pom.xml
) 形式で書いて jar と共にアップロードする。
一見とっつきにくいかもしれないが、今回の目的に絞れば簡単:
<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>
...
(Artifactory) Auto generated POM には注意
Artifactory が POM を自動生成してくれるので便利に見えるが...
<dependencies>
が空だったり間違っていたりすることが多い
アップロードした jar の依存ライブラリの情報を得られなくなる
↓パッケージ管理ツールによる依存関係管理のメリットが失われる
自動生成に丸投げせずに <dependencies>
をちゃんと書こう。
レポジトリ間で重複するライブラリ
同名だが一致しない(差分がある)ライブラリがある場合、
どちらが読み込まれるか明示的に制御すべし:
- プライベートレポジトリへ別 version でアップロード
- または、Gradle 側で制御する (下記)
repositories {
maven { // Maven central
url 'http://central.maven.org/maven2/'
content {
excludeGroup 'com.sun.jmx' // このグループをここから取得しない
excludeModule 'jdom', 'jdom' // このライブラリをここから取得しない
}
// 上記記述をするため mavenCentral() といった短縮記法は使っていない
}
最後に
Next Step
パッケージ管理ツールを入れたら特にやっておきたいこと:
- Version 衝突の検知
- classpath 上の重複の検知
- OWASP dependency check による脆弱性情報のチェック
JJUG 2018 Spring のスライドでより深く説明しています:
- タイトル:
Spring Boot と一般ライブラリの折り合いのつけかた
- Speaker Deck と Qiita 両方に同じ内容で掲載しています
※ 資料の後半は Spring Framework 関係なく使える情報です
まとめ
今回、パッケージ管理ツールを入れた際のプロセス:
- ゴールを定義
- パッケージ管理でどう良くなるかのビジョンを描く
- 戦略を構築
- 実現のために何をするか・何をあえて「しない」のか
- テクニックで課題を解決: 今回言及した各種工夫
- Gradle の活用
- jar の差分チェック
- プライベートレポジトリの活用
「闇が深くて手がつけられない」とも思える状況でも、
適切なスコープを設定し、技術で実現する ことで前に進める。