はじめに
Gradle に implementation が導入されて随分と経つのですが、お恥ずかしながら、これまでずっと仕様を勘違いしていました。ここに自身で調べたことを掲載し、私と同じように理解が曖昧な方の一助になれば幸いです。
勘違いしていた内容
implementation は「依存関係が伝播しない」が勘違いしていたものでした。
ネットの情報を斜め読みしていると implementation は「依存関係が伝播しない」と説明されているものが多くあります。きちんと読めば正しく説明されているものもありますが、あまり詳しく解説されていないものも見受けられました。私はこの「依存関係が伝播しない」という言葉をそのまま受け取ってしまい、「じゃあ、利用側はわざわざ自分で実行に必要な依存関係を追加しないといけないのか?」と考え、「だったら api でいいじゃないか」と勘違いしていた訳です。
正しい理解
implementation は 「compileClasspath には伝播しないが、runtimeClasspath には伝播する」が正しい。
implementation は「依存関係が伝播しない」というのは嘘ではないが、正確ではありません。実際に確認してみると分かりやすいので、ここでは以下のようなプロジェクトを使って確認します。producer がライブラリを提供するプロジェクト、consumer はそれを利用するプロジェクトという想定です。
% tree implementation-scope-test
implementation-scope-test
├── build.gradle.kts
├── consumer
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── consumer
│ └── Main.java
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── producer
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── producer
│ └── Library.java
└── settings.gradle.kts
17 directories, 10 files
それぞれの build.gradle.kts には以下のように記述しておきます。
plugins {
`java-library`
`maven-publish`
}
dependencies {
implementation("org.apache.commons:commons-lang3:3.17.0")
}
publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
}
}
}
dependencies {
implementation(project(":producer"))
}
ここで注目すべきは producer/build.gradle.kts の commons-lang3 の依存関係が consumer に対してどのように伝播するかです。
% ./gradlew :consumer:dependencies --configuration compileClasspath
> Task :consumer:dependencies
------------------------------------------------------------
Project ':consumer'
------------------------------------------------------------
compileClasspath - Compile classpath for source set 'main'.
\--- project :producer
A web-based, searchable dependency report is available by adding the --scan option.
BUILD SUCCESSFUL in 337ms
1 actionable task: 1 executed
consumer の compileClasspath には commons-lang3 は出てきません。
% ./gradlew :consumer:dependencies --configuration runtimeClasspath
> Task :consumer:dependencies
------------------------------------------------------------
Project ':consumer'
------------------------------------------------------------
runtimeClasspath - Runtime classpath of source set 'main'.
\--- project :producer
\--- org.apache.commons:commons-lang3:3.17.0
A web-based, searchable dependency report is available by adding the --scan option.
BUILD SUCCESSFUL in 406ms
1 actionable task: 1 executed
一方で runtimeClasspath には commons-lang3 が出てきます。では producer の pom はどうなっているのでしょうか。
% ./gradlew :producer:publishToMavenLocal
BUILD SUCCESSFUL in 350ms
% cat ~/.m2/repository/org/example/producer/1.0-SNAPSHOT/producer-1.0-SNAPSHOT.pom
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>producer</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.17.0</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
scope は runtime になっていました。だから runtimeClasspath には伝播している訳です。
つまり、implementation を利用すると producer が内部的に利用している commons-lang3 は consumer の compileClasspath には含まれないため、consumer のコードからは直接利用できない状態となります。一方で consumer の実行時には producer の実行に commons-lang3 が必要なので、runtimeClasspath に含められます。implementation が推奨される理由はここにある訳ですね。
では producer で runtime と指定した場合とどう違うのか?と思うかもしれませんが、 producer で runtime と指定すると producer の compileClasspath にも commons-lang3 が含まれなくなるので、producer のコードで commons-lang3 が使えなくなります。
気をつけないといけないところ
producer が次のような実装を提供している場合は implementation で良い。
package org.example.producer;
import org.apache.commons.lang3.StringUtils;
public class Library {
private Library () {
}
public static String concat(String a, String b) {
return StringUtils.join(a, b);
}
}
以下のような実装の場合は commons-lang3 のクラスが外部に露出しているので implementation は不適切です。
package org.example.producer;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
public class Library {
private Library () {
}
public static String concat(Pair<String, String> pair) {
return StringUtils.join(pair.getLeft(), pair.getRight());
}
}
このような場合は api として、consumer の compileClasspath に commons-lang3 を伝播させないと、Pair
のインスタンスを作るコードを consumer で書けない訳です。
まとめ
説明はきちんと読んで実際に試すのが正義。