Slide 1

Slide 1 text

パッケージ管理していなかった既存 システムに後付けで Gradle を導入した話 〜 .jar ファイルを直接管理する世界から パッケージ管理ツールへの移行の進め方・テクニック 〜 JJUG CCC 2019 Spring #jjug_ccc #ccc_a2 エムスリー株式会社 矢崎 聖也 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話

Slide 2

Slide 2 text

スピーカー紹介 矢崎 聖也 @saiya_moebius エムスリー株式会社 CTO サーバーサイドを中心としつつ色々やっているエンジニア Java, Kotlin, Rails 等 (言語の仕様として好きなのは C#) 最近良く触れているのは GKE (GCP の Kubernetes) や Node.js / TypeScript 新規プロジェクトや大規模リファクタリングを複数経験 今回紹介するのも、それらの足回り整備を通して得 たノウハウ #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 2

Slide 3

Slide 3 text

このスライドについて Speakers Desk および Qiita で公開しております。 時間の都合でコードの詳細など詳述しない部分があるため、 必要に応じて参照していただければと思います。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 3

Slide 4

Slide 4 text

今でこそ一般化している 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 あたりの時代)。 Maven, Gradle, Ivy がまだ無い時代から Java を使っていました。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 4

Slide 5

Slide 5 text

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 が... #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 5

Slide 6

Slide 6 text

.jar を直接管理するオペレーション このようなプロセスでメンテナンスしている状態だった: #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 6

Slide 7

Slide 7 text

ライブラリのバージョンアップのつらみ 例: 新機能を使うために aws-java-sdk のバージョンを上げたい ↓ aws‑java‑sdk 系は複数のライブラリに間接依存 ↓ 182 個の .jar をどうバージョンアップするべきか分からない ↓ AWS の新しい機能を使うのを諦める #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 7

Slide 8

Slide 8 text

間接依存のつらみ ある jar を使っていないので消したい ↓ もしかしたら、いずれかの jar がそれに依存しているかも... ↓ 不要 jar の掃除ができない ↓ JVM バージョンアップや脆弱性対応などの足かせに #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 8

Slide 9

Slide 9 text

各種ツールを使えないつらみ 脆弱性情報をチェックしたい ↓ 定期的に目検で 182 個の .jar と脆弱性情報を照合 ↓ しんどい & 限界がある ↓ 放置しがちになってしまう Gradle, maven 等であれば OWASP dependency-check plugin で 自動化できるのに... #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 9

Slide 10

Slide 10 text

こうなってしまっていた背景 当該システム以外は maven, Gradle 導入済みであるのに対して... 当該システムは創業時からのものであり、特に歴史が長い Ant のビルドスクリプトがガッツリ組まれており、 一括リプレースはつらい 普段の開発作業は問題なく回るので... (※ ライブラリ周りの闇を無視している限りは) こういった事情でパッケージ管理ツールが入っていない事例は 世の中にもあるのではないでしょうか...。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 10

Slide 11

Slide 11 text

パッケージ管理ツール(Gradle, maven 等) を入れましょう! とはいえ、既存のプロジェクトにどうやって導入するのか...? 作業の負担が大きくなりがちな点をどうするのか? 入れることによる副作用や影響をどうするのか? → 次章へ #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 11

Slide 12

Slide 12 text

パッケージ管理ツール導入の戦略 〜 歴史あるシステムの改善の 実現可能性のある スコープ定義 〜 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 12

Slide 13

Slide 13 text

パッケージ管理ツールの導入での課題 既存のシステムへの後付け導入では、課題がいろいろ現れる: ツール導入前後で jar が変わってしまうことを良しとするか? 再入手困難な jar や central と一致しない jar をどうするか? ビルドプロセスにも手を入れるか? 課題に振り回されないために、ポリシーがあったほうが良い。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 13

Slide 14

Slide 14 text

弊社事例で設定したポリシー パッケージ管理ツール導入前後で、 .jar ファイルに差分を 極力出さないようにする アプリへの影響懸念・テスト負担を抑えるため 差分を回避不能なものは、無害な差分しかないことを確 かめる (手法は後述) ライブラリ更新などは次の改善のスコープにする ビルド・デプロイ(Ant)には手を入れない まずはパッケージ管理を改善する! パッケージ管理ツールの恩恵を得ることにスコープを絞ることで 小さく・意味のあるチャレンジをする状況を作り出した。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 14

Slide 15

Slide 15 text

パッケージ管理ツール導入前の状態 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 15

Slide 16

Slide 16 text

パッケージ管理ツール導入でこうなった #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 16

Slide 17

Slide 17 text

Git に .jar を上げることの是非 メリット: Ant などからの移行が容易 常に確実に同じ jar ファイルを使える デメリット: Ant が残る Ant で真に困り事が発生したらその時にリプレース .jar の更新を繰り返すとレポジトリが肥大化するであろう そのうち Git submodule なりに分離する #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 17

Slide 18

Slide 18 text

レガシー改善の戦略 真に 困っていること ・ 変えるメリットがあること を見極める ↓ そのために必要な 必要十分な最小スコープ を考える ↓ 小さいチャレンジ で目的を達成する (コスパ高, 失敗してもダメージ小) 「レガシーだから全部抹殺するしかない!!!」ではなく、 今ある良い点や変える必要がない点は踏襲しつつ改善。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 18

Slide 19

Slide 19 text

パッケージ管理ツール導入テクニック: Gradle を後付け導入する方法 〜 後付け導入でありがちな諸課題への具体的な対処方法 〜 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 19

Slide 20

Slide 20 text

Gradle or Maven 今回のような用途には Gradle の方がオススメ: 柔軟性: Groovy や Kotlin DSL で柔軟な処理を記述可能 歴史的なプロジェクトの構成に maven は対応できないこ とがある 調査性: Groovy Javadoc のほうが読みやすい (主観) 生産性: IDE のデバッガをフル活用できる ブラックボックス的に格闘しなくて済む 再現性: Gradle wrapper があるため、動作の再現性を(比較的) 確保しやすい ※ 一般的には Maven の方が優れている事柄もある #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 20

Slide 21

Slide 21 text

Gradle によるファイルコピー 既存ビルドシステムを活かしつつ Gradle 導入するため、 以下のファイルを Gradle で所定のディレクトリにコピーしたい: コンパイル時・実行時に依存する jar 単体テスト時に依存する jar Source jar また、 .classpath ファイル(後述)も自動でメンテナンスしたい。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 21

Slide 22

Slide 22 text

Gradle で jar ファイルをダウンロード 意外と簡単: def compileJarCopyTo = 'development/lib/app' task downloadJars() { doLast { // コピー先にある *.jar を消す delete fileTree(dir: compileJarCopyTo, include: '*.jar') // コピーする copy { from configurations.compile into compileJarCopyTo } } } #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 22

Slide 23

Slide 23 text

copy task コピー対象 jar の指定や include, exclude なども出来て便利: copy { // testCompile = test で依存する jar をコピー from configurations.testCompile into testCompileJarCopyTo // 指定したパターンに該当する jar は無視 exclude 'jmockit-*.jar' } 大抵のディレクトリ構成に対応できるはず。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 23

Slide 24

Slide 24 text

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) へ投稿してあります #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 24

Slide 25

Slide 25 text

.classpath ファイルの生成 IDE に jar やソースコードを読み込ませるための設定ファイル。 もともと Eclipse 用の形式だが、InteliJ IDEA にも対応している。 せっかくなのでこのファイルも Gradle でメンテナンスさせたい。 Gradle の eclipse plugin を適用するだけで生成できるが... #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 25

Slide 26

Slide 26 text

.classpath 絶対パス問題 Plugin は .classpath に絶対パスを書き込む。 環境依存のパスになってしまい、Git などで共有できず不便。 解決策: 各自の手元で gradle eclipseClasspath して .classpath を都度生成する 全員が毎回実行する手間が発生 ビルドの再現性が下がる (手元では動いたのに!現象) 相対パス表記の .classpath を Gradle で生成し、jar ファイ ルと共に commit する (おすすめ) Jar ファイルのコピーなどとセットでまとめて実行す るだけで OK #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 26

Slide 27

Slide 27 text

.classpath を相対パス表記で生成する 参考サイト などにあるように whenMerged フックを使うと良い: eclipse.classpath.file { whenMerged { classpath -> classpath.entries.findAll { entry -> entry.kind == 'lib' }.each { it.path = it.path.replaceFirst("${projectDir.absolutePath}/", '') it.exported = false } } } #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 27

Slide 28

Slide 28 text

eclipse.classpath.file の whenMerged whenMerged は .classpath ファイルの内容を自由に制御できる 例えば: jar ファイルの参照先のパスを任意のロジックで書き換える 弊社事例では、一部ファイルだけ異なるディレクトリに 置いている classpath.entries.sort で .classpath の並び順を制御 JMockit のようにロード順に依存するライブラリで有用 classpath.getEntries().removeAll { return ( 条件式) } IDE に読み込ませたくない jar の除外が可能 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 28

Slide 29

Slide 29 text

jar のファイル名の変化 手作業管理の jar ファイル名と、Gradle 由来の jar は ファイル名が一致しないことが多い。例えば: 手動管理のファイル名: crimson.jar Gradle でのダウンロード結果: crimson-1.1.3.jar ファイル名決め打ちで参照している場合に問題が出る。 特に以下がありがち: Ant などのビルドツールの classpath におけるファイル名の 決め打ち プログラムの起動時の JVM の classpath オプションでの ファイル名の決め打ち #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 29

Slide 30

Slide 30 text

jar のファイル名が変わることへの対処策 1. 後処理で旧来のファイル名になるようにリネームする リネーム元の group, artifact 名が変わった場合に事故 る 新しい jar が増えたときの対応に困る 2. 個別のライブラリを意識しないようにする 特定ディレクトリ以下の jar をすべて読み込むだけで OK な設計にする Ant, JVM classpath (>= 6), シェルなどの * * の解釈がそれぞれ微妙に異なる点には要注意 一過性の手間はかかるが、後者のほうが長期的におすすめ。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 30

Slide 31

Slide 31 text

パッケージ管理ツール導入テクニック: 諸事情ある jar の管理 〜 歴史の彼方からやってきた jar ファイルを扱うための手法 〜 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 31

Slide 32

Slide 32 text

古い jar ファイルにありがちなこと jar を再入手できたがバイナリが一致しない SNAPSHOT 版 非 SNAPSHOT なのに差し替えリリースされている Maven repository 間で違うバイナリが置いてある 経緯不明の独自パッチ 公式ページが消えており、jar やソースを再入手できない #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 32

Slide 33

Slide 33 text

jar のバイナリ不一致の原因を確かめる 不一致は放置せずに、具体的な差分を確認すると良い: パッケージ管理ツール導入の影響範囲・リスクを限定できる テストすべき範囲やテスト手法も明らかになる 筆者は Gradle 化前後で jar ファイルの SHA‑1 ハッシュを出し、 ハッシュが一致しない jar について具体的な差分を調べた。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 33

Slide 34

Slide 34 text

jar ファイルの差分の調べ方 jar ファイルは zip であり、展開すると以下のファイルが出てくる: 1. .class ファイル (Java バイトコード) 2. META-INF などのメタデータファイル 3. その他、プロパティファイルなどのリソースファイル .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 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 34

Slide 35

Slide 35 text

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 で差し替えられている) #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 35

Slide 36

Slide 36 text

再入手不能なライブラリ 当該ライブラリの利用停止以外の対策: ( 非推奨) Central 等の公開レポジトリにアップロードする ライセンスや道義的に好ましくない 公知の jar と一致する確証のない jar を公開することに ( 微妙) 対象のライブラリの .java ファイルを直接使う ソースを直接使うため、独自改造や密結合が起きがち ライセンスによっては問題あり (おすすめ) 内部用のプライベートなレポジトリで管理 権利問題が起きにくい 当該ライブラリの利用状況の把握や削除がやりやすい #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 36

Slide 37

Slide 37 text

プライベートレポジトリ 再入手不能 jar に限らず、内製のライブラリの管理などでも プライベートなレポジトリがあると便利。 よくある構築方法: 1. ローカル上の maven レポジトリを file:// で参照する マシン間の同期が必要 ビルドの再現性を損ないがち jar の紛失リスクやバックアップの手間もある 2. Nexus や Artifactory などを設置する 詳細なセットアップ方法については触れないが、 新規でセットアップするならば Nexus の方が楽 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 37

Slide 38

Slide 38 text

プライベートレポジトリの利用 1. 世の中に存在するライブラリは、今までどおり maven central などから取得 2. プライベートなものは、プライベートレポジトリから取得 Gradle ならば以下のようにするだけで実現できる: repositories { maven { url 'http://central.maven.org/maven2/' } // mavenCentral() という短縮記法はあえて使っていない ( 後述) maven { url "http:// プライベートなレポジトリのURL" } } #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 38

Slide 39

Slide 39 text

レポジトリへ jar をアップロード メタデータを Maven の XML ( pom.xml ) 形式で書いて jar と共にアップロード・配置する。 一見とっつきにくいかもしれないが、今回の目的に絞れば簡単: 4.0.0 com.m3 m3generator 1.0.0 log4j log4j 1.2.17 ... #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 39

Slide 40

Slide 40 text

(Artifactory) Auto generated POM には注意 Artifactory は POM を自動生成してくれるので便利に見えるが... が空だったり間違っていたりすることが多い ↓ アップロードした jar の依存ライブラリの情報を得られなくなる ↓ パッケージ管理ツールによる依存関係管理のメリットが失われる 自動生成に丸投げせずに をちゃんと書こう。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 40

Slide 41

Slide 41 text

レポジトリ間で重複するライブラリ 同名だが一致しない(差分がある)ライブラリがある場合、 どちらが読み込まれるか明示的に制御すべし: プライベートレポジトリへ別 version 名でアップロード または、Gradle 側で制御する (下記) repositories { maven { // Maven central url 'http://central.maven.org/maven2/' content { excludeGroup 'com.sun.jmx' // このグループをここから取得しない excludeModule 'jdom', 'jdom' // このライブラリをここから取得しない } // 上記記述をするため mavenCentral() といった短縮記法は使っていない } #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 41

Slide 42

Slide 42 text

Next Step パッケージ管理ツールを入れたら特にやっておきたいこと: Version 衝突の検知 classpath 上の重複の検知 OWASP dependency check による脆弱性情報のチェック JJUG 2018 Spring のスライドでより深く説明しています: タイトル: Spring Boot と一般ライブラリの折り合いのつけかた Speaker Deck と Qiita 両方に同じ内容で掲載しています ※ 資料の後半は Spring Framework 関係なく使える情報です #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 42

Slide 43

Slide 43 text

まとめ 今回、パッケージ管理ツールを入れた際のプロセス: ゴールを定義 パッケージ管理でどう良くなるかのビジョンを描く 戦略を構築 実現のために何をするか・何をあえて「しない」のか テクニックで課題を解決: 今回言及した各種工夫 Gradle の活用 jar の差分チェック プライベートレポジトリの活用 「闇が深くて手がつけられない」とも思える状況でも、 適切なスコープを設定し 、技術で実現する ことで前に進める。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 43

Slide 44

Slide 44 text

[PR] エムスリー (株) We're Hiring! Tech Keyword: JVM 言語, 高収益サービス, 少数精鋭チーム 医療に関する web サービスを多数展開 (20 事業以上) 全世界で約400万人の医師会員 (日本で約25万人) #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 44