55
50

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-05-17

この資料について

JJUG CCC 2019 Spring の表題セッションの発表資料を Qiita にて公開したものです (twitter: #ccc_a2)。

image.png

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

はじめに

今でこそ一般化している 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 を直接管理するオペレーション

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

適当なところから手作業で jar をダウンロードして Git に入れるという闇のオペレーションの図

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

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

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

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

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

間接依存のつらみ

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

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

不要 jar の掃除ができない

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

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

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

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

しんどい & 限界がある

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

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

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

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

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

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

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

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

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

→ 次章へ

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

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

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

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

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

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

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

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

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

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

適当なところから手作業で jar をダウンロードして Git に入れるという闇のオペレーションの図

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

jar のダウンロードだけを Gradle 化した図

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

:thumbsup: メリット:

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

:thumbsdown: デメリット:

  • 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 を都度生成する
    • :thumbsdown: 全員が毎回実行する手間が発生
    • :thumbsdown: ビルドの再現性が下がる (手元では動いたのに!現象)
  • 相対パス表記の .classpath を Gradle で生成し、jar ファイルと共に commit する
    • :thumbsup: 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.filewhenMerged

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

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

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

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

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

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

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

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

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

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

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

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

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

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

lucene-analyzers-2.4.0META-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 で差し替えられている)

再入手不能なライブラリ

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

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

プライベートレポジトリ

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

よくある構築方法:

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

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

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

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 と一般ライブラリの折り合いのつけかた

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

まとめ

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

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

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

55
50
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
50

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?