How to setup Gradle to improve legacy Java system

How to setup Gradle to improve legacy Java system

※ JJUG 2019 Spring のセッション "パッケージ管理していなかった既存システムに後付けで Gradle を導入した話" のスライドです (#jjug_ccc #ccc_a2)。

近代的なプロダクトでは Gradle, Maven, Ivy といったパッケージ管理ツールを既に使っていることが多いと思います。

しかし、それらのツールの登場以前からの古い Java プロジェクト等に「後から」パッケージ管理ツールを導入するための方法論や具体的手法の情報が意外と少ないように感じました。

そこで、実際に大きなプロダクトに「後から」パッケージ管理ツールを導入した事例をご紹介いたします:

- パッケージ管理ツールを入れた動機
- パッケージ管理ツールを「後から」導入する際の戦略
- パッケージ管理ツールを「後から」導入するための各種の技術的手法

この情報を活かしていただくことで、パッケージ管理ツールの利用がより広まると幸いです。

1a18bf1e50d7d2bdfe52a6c9fceec244?s=128

saiya_moebius

May 18, 2019
Tweet

Transcript

  1. 1.

    パッケージ管理していなかった既存 システムに後付けで Gradle を導入した話 〜 .jar ファイルを直接管理する世界から パッケージ管理ツールへの移行の進め方・テクニック 〜 JJUG

    CCC 2019 Spring #jjug_ccc #ccc_a2 エムスリー株式会社 矢崎 聖也 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話
  2. 2.

    スピーカー紹介 矢崎 聖也 @saiya_moebius エムスリー株式会社 CTO サーバーサイドを中心としつつ色々やっているエンジニア Java, Kotlin, Rails

    等 (言語の仕様として好きなのは C#) 最近良く触れているのは GKE (GCP の Kubernetes) や Node.js / TypeScript 新規プロジェクトや大規模リファクタリングを複数経験 今回紹介するのも、それらの足回り整備を通して得 たノウハウ #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 2
  3. 4.

    今でこそ一般化している 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
  4. 5.

    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
  5. 7.

    ライブラリのバージョンアップのつらみ 例: 新機能を使うために aws-java-sdk のバージョンを上げたい ↓ aws‑java‑sdk 系は複数のライブラリに間接依存 ↓ 182

    個の .jar をどうバージョンアップするべきか分からない ↓ AWS の新しい機能を使うのを諦める #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 7
  6. 8.

    間接依存のつらみ ある jar を使っていないので消したい ↓ もしかしたら、いずれかの jar がそれに依存しているかも... ↓ 不要

    jar の掃除ができない ↓ JVM バージョンアップや脆弱性対応などの足かせに #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 8
  7. 9.

    各種ツールを使えないつらみ 脆弱性情報をチェックしたい ↓ 定期的に目検で 182 個の .jar と脆弱性情報を照合 ↓ しんどい

    & 限界がある ↓ 放置しがちになってしまう Gradle, maven 等であれば OWASP dependency-check plugin で 自動化できるのに... #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 9
  8. 10.

    こうなってしまっていた背景 当該システム以外は maven, Gradle 導入済みであるのに対して... 当該システムは創業時からのものであり、特に歴史が長い Ant のビルドスクリプトがガッツリ組まれており、 一括リプレースはつらい 普段の開発作業は問題なく回るので...

    (※ ライブラリ周りの闇を無視している限りは) こういった事情でパッケージ管理ツールが入っていない事例は 世の中にもあるのではないでしょうか...。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 10
  9. 13.

    パッケージ管理ツールの導入での課題 既存のシステムへの後付け導入では、課題がいろいろ現れる: ツール導入前後で jar が変わってしまうことを良しとするか? 再入手困難な jar や central と一致しない

    jar をどうするか? ビルドプロセスにも手を入れるか? 課題に振り回されないために、ポリシーがあったほうが良い。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 13
  10. 14.

    弊社事例で設定したポリシー パッケージ管理ツール導入前後で、 .jar ファイルに差分を 極力出さないようにする アプリへの影響懸念・テスト負担を抑えるため 差分を回避不能なものは、無害な差分しかないことを確 かめる (手法は後述) ライブラリ更新などは次の改善のスコープにする

    ビルド・デプロイ(Ant)には手を入れない まずはパッケージ管理を改善する! パッケージ管理ツールの恩恵を得ることにスコープを絞ることで 小さく・意味のあるチャレンジをする状況を作り出した。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 14
  11. 17.

    Git に .jar を上げることの是非 メリット: Ant  などからの移行が容易 常に確実に同じ jar

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

    レガシー改善の戦略 真に 困っていること ・ 変えるメリットがあること を見極める ↓ そのために必要な 必要十分な最小スコープ を考える

    ↓ 小さいチャレンジ で目的を達成する (コスパ高, 失敗してもダメージ小) 「レガシーだから全部抹殺するしかない!!!」ではなく、 今ある良い点や変える必要がない点は踏襲しつつ改善。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 18
  13. 20.

    Gradle or Maven 今回のような用途には Gradle の方がオススメ: 柔軟性: Groovy や Kotlin

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

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

    単体テスト時に依存する jar Source jar また、 .classpath ファイル(後述)も自動でメンテナンスしたい。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 21
  15. 22.

    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
  16. 23.

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

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

    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
  18. 25.

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

    にも対応している。 せっかくなのでこのファイルも Gradle でメンテナンスさせたい。 Gradle の eclipse plugin を適用するだけで生成できるが... #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 25
  19. 26.

    .classpath 絶対パス問題 Plugin は .classpath に絶対パスを書き込む。 環境依存のパスになってしまい、Git などで共有できず不便。 解決策: 各自の手元で

    gradle eclipseClasspath して .classpath を都度生成する 全員が毎回実行する手間が発生 ビルドの再現性が下がる (手元では動いたのに!現象) 相対パス表記の .classpath を Gradle で生成し、jar ファイ ルと共に commit する (おすすめ) Jar ファイルのコピーなどとセットでまとめて実行す るだけで OK #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 26
  20. 27.

    .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
  21. 28.

    eclipse.classpath.file の whenMerged whenMerged は .classpath ファイルの内容を自由に制御できる 例えば: jar ファイルの参照先のパスを任意のロジックで書き換える

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

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

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

    jar のファイル名が変わることへの対処策 1. 後処理で旧来のファイル名になるようにリネームする リネーム元の group, artifact 名が変わった場合に事故 る 新しい

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

    古い jar ファイルにありがちなこと jar を再入手できたがバイナリが一致しない SNAPSHOT 版 非 SNAPSHOT なのに差し替えリリースされている

    Maven repository 間で違うバイナリが置いてある 経緯不明の独自パッチ 公式ページが消えており、jar やソースを再入手できない #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 32
  26. 33.

    jar のバイナリ不一致の原因を確かめる 不一致は放置せずに、具体的な差分を確認すると良い: パッケージ管理ツール導入の影響範囲・リスクを限定できる テストすべき範囲やテスト手法も明らかになる 筆者は Gradle 化前後で jar ファイルの

    SHA‑1 ハッシュを出し、 ハッシュが一致しない jar について具体的な差分を調べた。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 33
  27. 34.

    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
  28. 35.

    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
  29. 36.

    再入手不能なライブラリ 当該ライブラリの利用停止以外の対策: ( 非推奨) Central 等の公開レポジトリにアップロードする ライセンスや道義的に好ましくない 公知の jar と一致する確証のない

    jar を公開することに ( 微妙) 対象のライブラリの .java ファイルを直接使う ソースを直接使うため、独自改造や密結合が起きがち ライセンスによっては問題あり (おすすめ) 内部用のプライベートなレポジトリで管理 権利問題が起きにくい 当該ライブラリの利用状況の把握や削除がやりやすい #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 36
  30. 37.

    プライベートレポジトリ 再入手不能 jar に限らず、内製のライブラリの管理などでも プライベートなレポジトリがあると便利。 よくある構築方法: 1. ローカル上の maven レポジトリを

    file:// で参照する マシン間の同期が必要 ビルドの再現性を損ないがち jar の紛失リスクやバックアップの手間もある 2. Nexus や Artifactory などを設置する 詳細なセットアップ方法については触れないが、 新規でセットアップするならば Nexus の方が楽 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 37
  31. 38.

    プライベートレポジトリの利用 1. 世の中に存在するライブラリは、今までどおり maven central などから取得 2. プライベートなものは、プライベートレポジトリから取得 Gradle ならば以下のようにするだけで実現できる:

    repositories { maven { url 'http://central.maven.org/maven2/' } // mavenCentral() という短縮記法はあえて使っていない ( 後述) maven { url "http:// プライベートなレポジトリのURL" } } #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 38
  32. 39.

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

    (Artifactory) Auto generated POM には注意 Artifactory は POM を自動生成してくれるので便利に見えるが... <dependencies>

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

    レポジトリ間で重複するライブラリ 同名だが一致しない(差分がある)ライブラリがある場合、 どちらが読み込まれるか明示的に制御すべし: プライベートレポジトリへ別 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
  35. 42.

    Next Step パッケージ管理ツールを入れたら特にやっておきたいこと: Version 衝突の検知 classpath 上の重複の検知 OWASP dependency check

    による脆弱性情報のチェック JJUG 2018 Spring のスライドでより深く説明しています: タイトル: Spring Boot と一般ライブラリの折り合いのつけかた Speaker Deck と Qiita 両方に同じ内容で掲載しています ※ 資料の後半は Spring Framework 関係なく使える情報です #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 42
  36. 43.

    まとめ 今回、パッケージ管理ツールを入れた際のプロセス: ゴールを定義 パッケージ管理でどう良くなるかのビジョンを描く 戦略を構築 実現のために何をするか・何をあえて「しない」のか テクニックで課題を解決: 今回言及した各種工夫 Gradle の活用

    jar の差分チェック プライベートレポジトリの活用 「闇が深くて手がつけられない」とも思える状況でも、 適切なスコープを設定し 、技術で実現する ことで前に進める。 #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 43
  37. 44.

    [PR] エムスリー (株) We're Hiring! Tech Keyword: JVM 言語, 高収益サービス,

    少数精鋭チーム 医療に関する web サービスを多数展開 (20 事業以上) 全世界で約400万人の医師会員 (日本で約25万人) #jjug_ccc #ccc_a2 パッケージ管理していなかった既存システムに後付けで Gradle を導入した話 44