こんにちは!開発現場で日々JavaやSpring Bootのコードと格闘していると、ある日突然、こんな悪夢のようなエラーに直面することがありますよね。
java.lang.NoClassDefFoundError: org/springframework/core/env/PropertyResolver
at com.example.MyService.
「あれ?昨日まで動いていたのに、なぜ急にクラスが見つからないんだ……?」
「依存関係を追加した途端に、別のライブラリが動かなくなった……」
Javaのビルドツールである Maven を使っていると、誰もが一度はこの「JAR地獄(Class-Path衝突)」の洗礼を受けます。複数のライブラリが、それぞれ異なるバージョンの依存関係(推移的依存関係)を裏側で要求し、クラスパス上で「どちらのバージョンを採用するか」のイス取りゲームに負けた結果、必要なクラスが消えてしまう現象です。
今回は、このJAR地獄を華麗に、そしてロジカルに鎮圧するための決定版ワークフローを、優しく丁寧にお伝えします。これをマスターすれば、毎日のコーディングで依存関係のエラーに怯えることが劇的に減りますよ!
—
1. そもそもMavenと「推移的依存関係」の裏側で何が起きているのか?
Mavenは、私たちがプロジェクトの `pom.xml` に「このライブラリを使いたい」と書くだけで、そのライブラリが依存している別のライブラリ(孫やひ孫のライブラリ)まで自動でかき集めてきてくれる、非常に優秀なパッケージマネージャーです。これを推移的依存関係(Transitive Dependencies)と呼びます。
しかし、ここに落とし穴があります。
例えば、以下のような構造になっているとしましょう。
- あなたのプロジェクト
- ライブラリA(v1.0が要求する共通基盤 v2.0)
- ライブラリB(v2.0が要求する共通基盤 v1.0)
共通基盤の「v1.0」と「v2.0」でクラスの互換性がない場合、Mavenは独自の「最短パス優先(Nearest Definition)」というアルゴリズムで勝手にどちらか一方を選びます。その結果、選ばれなかった方のライブラリが必要とするクラスがランタイム(実行時)に消失し、`NoClassDefFoundError` が発生するのです。これがJAR地獄の正体です。
—
2. 基礎知識:Mavenの環境確認と「Hello World」的ビルドの作法
まずは、私たちが日々使うMavenが正しく動作しているか、そして依存関係をきれいに出力できる基本セットアップを確認しておきましょう。
動作確認:Mavenのバージョンとプロジェクトのコンパイル
ターミナルを開き、以下のコマンドを実行してみてください。
Mavenが正しくインストールされているか、バージョンを確認します
mvn -version
正常にインストールされていれば、MavenのバージョンやJavaのランタイム情報が表示されます。
次に、シンプルなMavenプロジェクトのルートディレクトリ(`pom.xml`がある場所)で、以下のコマンドを叩いてみます。
キャッシュをクリアしつつ、綺麗にコンパイルできるかテストする基本コマンド
mvn clean compile
- `clean`:過去のビルド成果物(`target`ディレクトリなど)を完全に削除し、まっさらな状態にします。
- `compile`:ソースコードをコンパイルし、クラスファイルを生成します。
ここまでは基本中の基本ですね。次からが本題です。この平穏なビルドの裏で起きている衝突を、可視化して叩き潰します。
—
3. 実践ワークフロー:`mvn dependency:analyze` で衝突の急所を見抜く
JAR地獄を解決するための黄金のステップは以下の3つです。
1. 可視化する:何が衝突し、どこが無駄になっているかを特定する
2. 除外する(exclusions):不要な古いバージョンをピンポイントで切り捨てる
3. 統制する(dependencyManagement):プロジェクト全体でバージョンのルールを強制する
ステップ1:`dependency:analyze` で使われていない依存や不正を炙り出す
Mavenのプラグインには、依存関係を解析する強力な機能が備わっています。以下のコマンドをプロジェクトのルートで実行してください。
依存関係の利用状況を深く分析し、レポートを出力するコマンド
mvn dependency:analyze
実行すると、コンソールに以下のような分析結果が表示されます。
[INFO] — maven-dependency-plugin:3.6.1:analyze (default-cli) @ my-app —
[INFO] Used undeclared dependencies found:
[INFO] com.google.guava:guava:31.1-jre
[INFO] Unused declared dependencies found:
[INFO] commons-lang:commons-lang:2.6
ここを見ることで、「本当は直接宣言すべきなのに、別のライブラリのすねをかじって使っているもの(Undeclared)」や、「誰も使っていないのに読み込まれている無駄なもの(Unused)」が丸裸になります。
さらに、特定のクラスがどのJARファイルからロードされているか(クラスパスの競合)を確認したい場合は、以下のツリー表示コマンドが絶大な効果を発揮します。
依存関係のツリー構造を表示し、バージョン違いの競合を目視できるようにする
mvn dependency:tree
—
ステップ2:`` で不要な推移的依存を外科手術的に切り捨てる
`mvn dependency:tree` で「おや、このライブラリAのせいで、古いバージョンの `jackson-databind` が勝手に紛れ込んでいるぞ」と気づいたとします。
そんな時は、`pom.xml` の該当する依存関係の中に `
このように設定することで、`library-a` は読み込みつつも、それが連れてこようとする古い `jackson-databind` だけをシャットアウトできます。
—
ステップ3:`` で全モジュールのバージョンを王様統制する
複数モジュールを持つ大規模なプロジェクトや、多くのライブラリが絡み合う複雑なシステムでは、個別の `
そこで親 `pom.xml` の `
【ここに注目!】
`
—
4. 先輩エンジニアからの実践アドバイス
最後に、実務でJAR地獄に遭遇したときに、最短で解決するためのマインドセットを伝授します。
1. まずは IDE のキャッシュを疑う
Mavenの設定を直したのにエラーが消えないときは、大抵の場合、IntelliJ IDEAやEclipseなどのIDE側が古い依存関係のインデックスをキャッシュしています。
- IntelliJなら [File] -> [Invalidate Caches…] からキャッシュをクリアして再起動。
- 命令行なら `mvn clean install -U`(`-U`オプションはリモートリポジトリからの強制アップデート)を実行。これだけで嘘のように解決することが多々あります。
2. 「なんとなくバージョンを上げる」をしない
エラーが出たからといって、適当に最新バージョンを書き連ねるのはNGです。必ず `mvn dependency:tree` で誰がどのバージョンを引っ張っているのかの相関図を頭に入れてから手を加えましょう。
JAR地獄は、ビルドツールの仕組みを深く理解する絶好のチャンスです。今回ご紹介した `mvn dependency:analyze` と `
毎日のコーディングが、もっと快適で楽しいものになりますように!