概要: Gradleのビルドで発生する「duplicate class」エラーは、依存関係の競合が原因です。本記事では、原因の特定方法からexcludeやforceによる解決策、さらにビルドキャッシュや依存関係キャッシュのトラブル対処法までを解説します。
GradleのDependencyで発生するduplicate classエラーとは
duplicate classエラーの概要と発生する仕組み
duplicate classエラーは、クラスパス上に完全修飾名(FQCN)が同一のクラスが複数存在することで発生するビルドエラーです。コンパイル時や実行時ではなく、依存関係の解決段階でGradleが検出します。最も典型的な原因は、同じライブラリの異なるバージョン、または同じクラスを内包する複数の依存関係がグラフ内に混在することです。推移的依存関係の競合が根本原因となるケースが多く、直接依存しているライブラリがそれぞれ別のバージョンの同じライブラリを引き込むことで発生します。
よくあるエラーメッセージと具体例
典型的なエラーメッセージは次のような形式です。「Duplicate class kotlin.collections.CollectionsJDK8Kt found in modules kotlin-stdlib-1.8.22.jar (org.jetbrains.kotlin:kotlin-stdlib:1.8.22) and kotlin-stdlib-jdk8-1.6.21.jar (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21)」。このようにどの2つのJAR間に重複があるかがGradleによって列挙されるため、原因特定の糸口になります。特にKotlinでは1.8.0以降、-jdk7/-jdk8の分割アーティファクトが本体kotlin-stdlibに統合されたため、バージョンの新旧が混在すると同じクラスが2つのJARに存在しやすくなります。
重複クラスが発生しやすい代表的なケース
重複クラスは特定の状況で頻発します。代表的なのは、ロギング系ライブラリ(SLF4J/Log4J/commons-logging)を複数バインディングが引き込むケース、Kotlin標準ライブラリの新旧混在、SDKが自前のライブラリを内包しているケースです。また、buildSrcや生成コード(proto等)が同じクラスを複数箇所で生成する場合も発生します。さらに、古い座標と新しい座標が同一クラスを提供するケース(例:asm:asm 3.3.1以前とorg.ow2.asm:asm 4.0以降)では、capability競合としてGradleが検出します。
重複クラスの原因を特定するGradle標準の診断コマンド
dependenciesタスクで依存関係ツリー全体を確認する
重複クラスの原因特定には、まずdependenciesタスクで依存関係ツリー全体を把握することが有効です。コマンドは./gradlew dependencies --configuration runtimeClasspathを使用します。出力内で→(採用された最終版)や(*)(バージョン仲裁で差し替えられた版)の記号を確認することで、どの依存関係が競合しているかを視覚的に把握できます。追加プラグインは不要で、Gradle標準搭載のタスクです。
dependencyInsightで特定の依存関係の経路を追跡する
特定の依存関係が「なぜ・どの経路で」クラスパスに入っているかを追跡するには、dependencyInsightタスクが最も有効です。コマンドは./gradlew dependencyInsight --dependency <group>:<name> --configuration runtimeClasspathの形式で実行します。例えば重複しているクラスを含むライブラリのグループ名とモジュール名を指定すると、どの依存関係がそのライブラリを引き込んでいるのかが明確になります。複数の経路から引き込まれている場合、すべての経路を確認できるため、excludeが効かない原因の特定にも役立ちます。
診断コマンドの使い分けと情報ログの活用
状況に応じてコマンドを使い分けることが重要です。広く全体像を把握したい場合はdependenciesタスク、特定のライブラリを深掘りしたい場合はdependencyInsightタスクを選びます。さらに./gradlew <タスク> --infoを付けて実行すると、実行ログから重複クラス情報を抽出する補助になります。重複クラスの原因特定は、エラーメッセージの正確な読み取りと、これらの診断コマンドによる依存関係の可視化が基本です。
duplicate classを解決するexcludeとforceの正しい使い方
excludeによる特定の依存関係からの除外
excludeは最も安全で一般的な解決方法で、影響範囲を1つの直接依存に限定できます。Groovy DSLではimplementation('com.example:sdk:2.1.0') { exclude group: 'log4j', module: 'log4j' }、Kotlin DSLではimplementation("com.example:sdk:2.1.0") { exclude(group = "log4j", module = "log4j") }と記述します。groupとmoduleは大文字小文字・記号も含めて正確に指定する必要があります。競合が複数の経路から引き込まれる場合は、すべての経路で除外が必要です。
forceによるバージョン統一
除外すると他の依存でも使われて困る場合や、安全に更新したい場合はforceによるバージョン統一が有効です。依存を消すのではなく、すべての経路で1つのバージョンに揃えます。configurations.all { resolutionStrategy { force 'com.google.guava:guava:32.1.2-jre' } }と記述し、全経路のguavaを指定バージョンに統一します。forceは安全な固定方法としてexcludeの代替に推奨されるケースが多く、推移的依存への「制約(constraint)」を使う方法もあります。
excludeとforceの使い分けの判断基準
| 比較項目 | exclude | force |
|---|---|---|
| 影響範囲 | 特定の依存関係からのみ除外 | 全経路のバージョンを統一 |
| 適している場面 | 不要な推移的依存を排除したい場合 | 同じライブラリを複数箇所で使用する場合 |
| 主なリスク | 複数経路で除外漏れが起きやすい | 意図しないバージョン強制の可能性 |
| 推奨されるケース | 特定のライブラリだけが問題の場合 | バージョン混在を根本的に解消したい場合 |
要点:excludeは特定の経路からの除外に、forceは全経路のバージョン統一に使用する。どちらも根本解決だが、pickFirstのような応急処置は実行時エラーのリスクが残るため避ける。
Gradleのキャッシュ種類とcleanでは消えない領域のクリア手順
Gradleのキャッシュの種類と格納場所
Gradleのキャッシュには複数の種類があり、格納場所と役割が異なります。依存関係キャッシュは~/.gradle/caches/modules-2/に格納され、ダウンロードした依存(JAR・POM等)を保存します。ビルドキャッシュは~/.gradle/caches/build-cache-1/に格納され、タスク出力(前回のビルド結果)を保存します。Configuration Cacheは<プロジェクト>/.gradle/configuration-cache/に保存されます。プロジェクトキャッシュは<プロジェクト>/.gradle/にあり、増分ビルドの履歴・タスク判定に使用されます。
cleanタスクの限界と誤解されやすい点
cleanタスクはプロジェクトのbuild/ディレクトリしか消しません。依存関係キャッシュやビルドキャッシュ(Gradle User Home内)は自動的には消えないため、キャッシュクリアを目的にcleanだけを実行しても、依存関係やビルドキャッシュ由来のトラブルは解消されません。ビルドキャッシュをコマンドでクリアする場合、Gradle 8以降はcleanBuildCacheタスクが削除されているため、build-cache-1ディレクトリを手動で削除します。
キャッシュクリアの正しい手順
- Gradleデーモンを停止する(
./gradlew --stop)。特にWindowsではデーモンがキャッシュファイルにロックを掛けているため削除できないことがあります。 - 症状に応じて対象キャッシュを削除する:依存関係キャッシュは
rm -rf ~/.gradle/caches/modules-2/、ビルドキャッシュはrm -rf ~/.gradle/caches/build-cache-1/、Configuration Cacheはrm -rf .gradle/configuration-cache。 - プロジェクト固有キャッシュは
cleanでbuild/を消し、必要なら.gradle/も削除する。
要点:cleanはbuild/のみを削除する。依存関係キャッシュとビルドキャッシュはGradle User Home内にあり、手動で対象ディレクトリを削除する必要がある。~/.gradle/全体の削除はGradle本体や設定まで失うため避ける。
キャッシュトラブルの切り分けに役立つビルドオプション
ビルドキャッシュを無効化するオプション
キャッシュが原因かを切り分ける第一歩は、キャッシュを使わずにビルドが正しく動くかを確認することです。--no-build-cacheオプションを付けて実行すると、今回のビルドだけビルドキャッシュを使用しません。これはキャッシュ自体の削除にはならず、キャッシュが原因かどうかの切り分けに使用します。--refresh-dependenciesは依存関係の再チェックを強制するもので、全依存を毎回再取得するため常用すると大幅にビルドが遅くなります。一時的な対処としてのみ使用します。
詳細なログを取得するオプション
キャッシュトラブルの原因を特定するには、詳細なログの取得が有効です。--stacktraceは例外発生時のスタックトレースを表示し、--infoは情報レベルのログを、--debugはデバッグレベルの詳細ログを出力します。キャッシュのヒット・ミス状況や、どのタスクがキャッシュから復元されたかを確認できます。エラーが発生したタスク名や依存関係の競合情報を正確に把握することで、適切な対処方法を選択できます。
Configuration Cache関連のオプション
Configuration Cacheはデフォルトでは無効ですが、有効化している場合のトラブルには専用のオプションがあります。問題があるとデフォルトでビルドが失敗しますが、一時的に--configuration-cache-problems=warnで警告に緩和できます。ただしこれは恒久的な対処ではありません。org.gradle.configuration-cache.max-problems(デフォルト512)で許容する問題数を指定できます。
| オプション | 用途 | 効果の範囲 |
|---|---|---|
| –no-build-cache | ビルドキャッシュを使わない | 今回のビルドのみ |
| –refresh-dependencies | 依存関係を強制的に再チェック | 今回のビルドのみ(常用非推奨) |
| –configuration-cache-problems=warn | Configuration Cacheの問題を警告に緩和 | 一時的な対処 |
| –stacktrace / –info / –debug | 詳細なログ・スタックトレースを表示 | 診断目的 |
要点:キャッシュトラブルは、まず–no-build-cacheでビルドキャッシュを無効化して再現を確認する。キャッシュ削除は原因が切り分けられてから行うのが基本で、キャッシュが原因でない問題をキャッシュ削除で解決しようとするのは誤り。
まとめ
よくある質問
Q: Gradleで「Duplicate class found in modules」エラーが発生する原因は?
A: クラスパス上に完全修飾名が同一のクラスが複数存在することが原因です。典型的には、同じライブラリの異なるバージョンや、同じクラスを含む複数の依存関係が混在することで発生します。
Q: Gradleで重複クラスの原因となっている依存関係を特定するにはどうすればよいですか?
A: Gradle標準のdependenciesタスクとdependencyInsightタスクを使用します。dependencyInsightタスクを使うと、特定の依存関係がなぜ・どの経路でクラスパスに入っているかを追跡できるため、重複原因の特定に有効です。
Q: duplicate classエラーを解決するための「exclude」と「force」の違いは何ですか?
A: excludeは特定の依存関係からのみ、指定したライブラリを除外する方法です。forceはresolutionStrategyを使用して、すべての依存経路で特定のライブラリのバージョンを強制的に統一する方法です。
Q: Gradleの「clean」タスクでは、どのキャッシュまで削除できますか?
A: cleanタスクでは、プロジェクトのbuildディレクトリ内のビルド成果物のみが削除されます。依存関係キャッシュやビルドキャッシュはGradle User Home(~/.gradle)に格納されているため、cleanでは削除されません。
Q: Gradleのビルドキャッシュが原因で問題が起きているかどうかを確認する方法はありますか?
A: ビルド時に–no-build-cacheオプションを付けて実行し、キャッシュを使わない状態で問題が再現するかどうかを確認します。キャッシュを使わないで正しく動作する場合は、ビルドキャッシュが原因である可能性が高いです。
