1. 概述

在 Gradle 项目中,Source Sets(源集) 是组织源码的利器。它不只是目录结构,更是一种逻辑上的代码分组机制。

本文带你深入理解 Source Sets 的默认行为、自定义用法,以及在实际开发中如何避坑。适合已有 Gradle 基础的开发者快速查阅与进阶。


2. 默认 Source Sets

2.1 默认项目结构

Source Sets 的核心思想是:将源文件按用途逻辑分组。最常见的就是 maintest

标准 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/javasrc/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 —— maintest


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 插件)中导入项目后,可能会出现编译错误:

compilation issue eclipse

❌ 问题原因:Eclipse Buildship 不识别自定义配置(如 itestImplementation

解决方案

build.gradle 中显式将配置加入 Eclipse 类路径:

apply plugin: "eclipse"

eclipse {
    classpath {
        plusConfigurations += [configurations.itestCompileClasspath] 
    } 
}

刷新项目后,编译错误消失。

⚠️ 但有个副作用:Eclipse 不区分配置作用域,导致 test 源码也能随意引用 guava,破坏了我们“隔离依赖”的初衷。

✅ 建议:在团队中通过规范或代码审查来规避,IDE 的便利性 vs. 依赖隔离,需权衡。


4. 总结

  • maintest 是默认 Source Set,自动继承依赖与输出
  • ✅ 自定义 Source Set(如 itest)需手动配置依赖继承和类路径
  • ✅ 使用 configurations { itestImplementation.extendsFrom(...) } 实现依赖复用
  • ✅ 自定义 Test 任务来运行非标准测试
  • ⚠️ IDE(如 Eclipse)对自定义配置支持有限,需额外配置,可能破坏依赖隔离

本文完整代码示例已上传至 GitHub:https://github.com/baeldung/gradle-tutorials/tree/master/source-sets


原始标题:Gradle Source Sets