1. 概述
在 Gradle 项目中,Source Sets(源集) 是组织源码的利器。它不只是目录结构,更是一种逻辑上的代码分组机制。
本文带你深入理解 Source Sets 的默认行为、自定义用法,以及在实际开发中如何避坑。适合已有 Gradle 基础的开发者快速查阅与进阶。
2. 默认 Source Sets
2.1 默认项目结构
Source Sets 的核心思想是:将源文件按用途逻辑分组。最常见的就是 main
和 test
。
标准 Java 项目结构如下:
source-sets
├── src
│ ├── main
│ │ └── java
│ │ ├── SourceSetsMain.java
│ │ └── SourceSetsObject.java
│ └── test
│ └── java
│ └── SourceSetsTest.java
└── build.gradle
对应的 build.gradle
配置:
apply plugin: "java"
description = "Source Sets example"
test {
testLogging {
events "passed", "skipped", "failed"
}
}
dependencies {
implementation('org.apache.httpcomponents:httpclient:4.5.12')
testImplementation('junit:junit:4.12')
}
✅ 关键点:Java 插件默认识别 src/main/java
和 src/test/java
作为源码目录。
我们写个辅助任务查看 Source Set 信息:
task printSourceSetInformation() {
doLast {
sourceSets.each { srcSet ->
println "[" + srcSet.name + "]"
print "-->Source directories: " + srcSet.allJava.srcDirs + "\n"
print "-->Output directories: " + srcSet.output.classesDirs.files + "\n"
println ""
}
}
}
执行结果:
$ ./gradlew printSourceSetInformation
> Task :source-sets:printSourceSetInformation
[main]
-->Source directories: [.../source-sets/src/main/java]
-->Output directories: [.../source-sets/build/classes/java/main]
[test]
-->Source directories: [.../source-sets/src/test/java]
-->Output directories: [.../source-sets/build/classes/java/test]
⚠️ 注意:Gradle 自动生成了两个默认 Source Set —— main
和 test
。
2.2 默认依赖配置
Java 插件不仅创建了源集,还自动创建了对应的 依赖配置(configurations),命名规则为:<sourceSetName><configurationName>
。
例如:
implementation
→ 对应main
的编译依赖testImplementation
→ 对应test
的编译依赖
dependencies {
implementation('org.apache.httpcomponents:httpclient:4.5.12')
testImplementation('junit:junit:4.12')
}
✅ 特例:implementation
实际上是 mainImplementation
的别名,这是 Gradle 的简化写法。
更关键的是:**testImplementation
会自动继承 implementation
的依赖和输出**。
我们升级一下任务,查看编译类路径:
task printSourceSetInformation() {
doLast {
sourceSets.each { srcSet ->
println "[" + srcSet.name + "]"
print "-->Source directories: " + srcSet.allJava.srcDirs + "\n"
print "-->Output directories: " + srcSet.output.classesDirs.files + "\n"
print "-->Compile classpath:\n"
srcSet.compileClasspath.files.each {
print " " + it.path + "\n"
}
println ""
}
}
}
输出节选:
[main]
-->Compile classpath:
.../httpclient-4.5.12.jar
.../httpcore-4.4.13.jar
.../commons-logging-1.2.jar
.../commons-codec-1.11.jar
[test]
-->Compile classpath:
.../source-sets/build/classes/java/main // main 的输出
.../source-sets/build/resources/main // main 的资源
.../httpclient-4.5.12.jar // 继承的依赖
.../junit-4.12.jar
.../hamcrest-core-1.3.jar
✅ 结论:test
能直接使用 main
中的类,正是因为 main
的输出被包含在 test
的编译类路径中。
测试代码示例:
public class SourceSetsTest {
@Test
public void whenRun_ThenSuccess() {
SourceSetsObject underTest = new SourceSetsObject("lorem","ipsum");
assertThat(underTest.getUser(), is("lorem"));
assertThat(underTest.getPassword(), is("ipsum"));
}
}
执行测试:
./gradlew clean test
> Task :source-sets:test
com.baeldung.test.SourceSetsTest > whenRunThenSuccess PASSED
一切正常,验证了依赖和输出的继承机制。
3. 自定义 Source Sets
虽然默认配置够用,但实际项目中经常需要自定义 Source Set,比如:
- ✅ 集成测试(Integration Test)
- ✅ 功能测试(Functional Test)
- ✅ 示例代码(samples)
- ✅ 特定环境的代码(如 dev-only 工具类)
最常见的场景是 分离单元测试和集成测试,避免测试污染,也能独立运行。
3.1 定义自定义 Source Set
我们创建一个 itest
目录用于集成测试:
source-sets
├── src
│ └── main
│ ├── java
│ │ ├── SourceSetsMain.java
│ │ └── SourceSetsObject.java
│ ├── test
│ │ └── SourceSetsTest.java
│ └── itest
│ └── SourceSetsITest.java
└── build.gradle
在 build.gradle
中声明:
sourceSets {
itest {
java {
}
}
}
✅ 默认约定:src/itest/java
会被自动识别,无需显式指定。
当然也可以手动指定目录:
sourceSets {
itest {
java {
srcDirs("src/itest/java")
}
}
}
再次运行 printSourceSetInformation
,输出中多出了 itest
:
[itest]
-->Source directories: [.../source-sets/src/itest/java]
-->Output directories: [.../source-sets/build/classes/java/itest]
-->Compile classpath:
.../source-sets/build/classes/java/main
.../source-sets/build/resources/main
⚠️ 注意:此时 itest
的类路径中虽然包含了 main
的输出,但 没有自动继承 testImplementation
的依赖。
3.2 为自定义 Source Set 添加依赖
Gradle 会为 itest
自动生成以下配置:
itestImplementation
itestRuntimeOnly
itestCompileClasspath
itestRuntimeClasspath
我们添加 Guava 作为集成测试专用依赖:
dependencies {
implementation('org.apache.httpcomponents:httpclient:4.5.12')
testImplementation('junit:junit:4.12')
itestImplementation('com.google.guava:guava:29.0-jre')
}
测试类示例:
public class SourceSetsItest {
@Test
public void givenImmutableList_whenRun_ThenSuccess() {
SourceSetsObject underTest = new SourceSetsObject("lorem", "ipsum");
List<String> someStrings = ImmutableList.of("Baeldung", "is", "cool");
assertThat(underTest.getUser(), is("lorem"));
assertThat(underTest.getPassword(), is("ipsum"));
assertThat(someStrings.size(), is(3));
}
}
要运行这个测试,必须定义一个 Test
类型的任务:
task itest(type: Test) {
description = "Run integration tests"
group = "verification"
testClassesDirs = sourceSets.itest.output.classesDirs
classpath = sourceSets.itest.runtimeClasspath
}
⚠️ 重要:这个任务必须在 sourceSets.itest
声明之后定义,否则会报错(配置阶段执行)。
首次运行:
$ ./gradlew clean itest
FAILURE: Build failed with an exception.
> Compilation failed; see the compiler error output for details.
❌ 踩坑:itest
不会自动继承 JUnit
,也不会包含 testImplementation
的依赖!
解决方案
我们需要手动让 itest
继承 test
的依赖和输出:
sourceSets {
itest {
compileClasspath += sourceSets.main.output
runtimeClasspath += sourceSets.main.output
java {
}
}
}
configurations {
itestImplementation.extendsFrom(testImplementation)
itestRuntimeOnly.extendsFrom(testRuntimeOnly)
}
✅ 现在 itest
能访问:
main
的类和资源testImplementation
的所有依赖(如 JUnit)- 自己独有的依赖(如 Guava)
重新运行:
$ ./gradlew clean itest
> Task :source-sets:itest
com.baeldung.itest.SourceSetsItest > givenImmutableList_whenRun_ThenSuccess PASSED
✅ 成功通过!
3.3 在 Eclipse 中的兼容性问题
虽然 Gradle 跑得通,但在 Eclipse(使用 Buildship 插件)中导入项目后,可能会出现编译错误:
❌ 问题原因:Eclipse Buildship 不识别自定义配置(如 itestImplementation
)。
解决方案
在 build.gradle
中显式将配置加入 Eclipse 类路径:
apply plugin: "eclipse"
eclipse {
classpath {
plusConfigurations += [configurations.itestCompileClasspath]
}
}
刷新项目后,编译错误消失。
⚠️ 但有个副作用:Eclipse 不区分配置作用域,导致 test
源码也能随意引用 guava
,破坏了我们“隔离依赖”的初衷。
✅ 建议:在团队中通过规范或代码审查来规避,IDE 的便利性 vs. 依赖隔离,需权衡。
4. 总结
- ✅
main
和test
是默认 Source Set,自动继承依赖与输出 - ✅ 自定义 Source Set(如
itest
)需手动配置依赖继承和类路径 - ✅ 使用
configurations { itestImplementation.extendsFrom(...) }
实现依赖复用 - ✅ 自定义
Test
任务来运行非标准测试 - ⚠️ IDE(如 Eclipse)对自定义配置支持有限,需额外配置,可能破坏依赖隔离
本文完整代码示例已上传至 GitHub:https://github.com/baeldung/gradle-tutorials/tree/master/source-sets