Contents

Gradle源集

1. 概述

源集为我们提供了一种在Gradle 项目中构建源代码的强大方法。 在本快速教程中,我们将了解如何使用它们。

2. 默认源集

在进入默认设置之前,让我们先解释一下什么是源集。顾名思义,源集代表源文件的逻辑分组

我们将介绍 Java 项目的配置,但这些概念也适用于其他 Gradle 项目类型。

2.1. 默认项目布局

让我们从一个简单的项目结构开始:

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作为默认源目录

让我们制作一个简单的实用程序任务:

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 ""
        }
    }
}

我们在这里只打印一些源集属性。我们可以随时查看完整的JavaDoc 以获取更多信息。 让我们运行它,看看我们得到了什么:

$ ./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]

请注意**,我们有两个默认源集:maintest。**

2.2. 默认配置

Java 插件还会自动为我们创建一些默认的 Gradle配置

它们遵循特殊的命名约定:<sourceSetName><configurationName>。

我们使用它们在build.gradle中声明依赖项:

dependencies { 
    implementation('org.apache.httpcomponents:httpclient:4.5.12') 
    testImplementation('junit:junit:4.12') 
}

请注意,我们指定implementation而不是mainImplementation。这是命名约定的一个例外。

默认情况下,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]
// same output as before
-->Compile classpath:
  .../httpclient-4.5.12.jar
  .../httpcore-4.4.13.jar
  .../commons-logging-1.2.jar
  .../commons-codec-1.11.jar
[test]
// same output as before
-->Compile classpath:
  .../source-sets/build/classes/java/main
  .../source-sets/build/resources/main
  .../httpclient-4.5.12.jar
  .../junit-4.12.jar
  .../httpcore-4.4.13.jar
  .../commons-logging-1.2.jar
  .../commons-codec-1.11.jar
  .../hamcrest-core-1.3.jar

test源集在其编译类路径中包含main的输出,还包括其依赖项。

接下来,让我们创建我们的单元测试:

public class SourceSetsTest {
    @Test
    public void whenRun_ThenSuccess() {
        
        SourceSetsObject underTest = new SourceSetsObject("lorem","ipsum");
        
        assertThat(underTest.getUser(), is("lorem"));
        assertThat(underTest.getPassword(), is("ipsum"));
    }
}

这里我们测试一个存储两个值的简单 POJO。我们可以直接使用它,因为main 输出在我们的test类路径中。

接下来,让我们从 Gradle 运行它:

./gradlew clean test
> Task :source-sets:test
com.blogdemo.test.SourceSetsTest > whenRunThenSuccess PASSED

3. 自定义源集

到目前为止,我们已经看到了一些合理的默认设置。然而,在实践中,我们经常需要自定义源集,尤其是对于集成测试。

那是因为我们可能只想在集成测试类路径上拥有特定的测试库。我们也可能希望独立于单元测试来执行它们。

3.1. 定义自定义源集

让我们为集成测试制作一个单独的源目录:

source-sets 
  ├── src 
  │    └── main 
  │         ├── java 
  │         │    ├── SourceSetsMain.java
  │         │    └── SourceSetsObject.java
  │         ├── test 
  │         │    └── SourceSetsTest.java
  │         └── itest 
  │              └── SourceSetsITest.java
  └── build.gradle 

接下来,让我们使用sourceSets构造在build.gradle中配置它

sourceSets {
    itest {
        java {
        }
    }
}
dependencies {
    implementation('org.apache.httpcomponents:httpclient:4.5.12')
    testImplementation('junit:junit:4.12')
}
// other declarations omitted

请注意,我们没有指定任何自定义目录。那是因为我们的文件夹与新源集 ( itest ) 的名称相匹配。

我们可以自定义srcDirs属性包含哪些目录

sourceSets{
    itest {
        java {
            srcDirs("src/itest")
        }
    }
}

还记得我们从一开始的助手任务吗?让我们重新运行它,看看它打印了什么:

$ ./gradlew printSourceSetInformation
> Task :source-sets:printSourceSetInformation
[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
[main]
 // same output as before
[test]
 // same output as before

3.2. 分配源集特定依赖项

还记得默认配置吗?我们现在也获得了itest源集的一些配置。

让我们使用itestImplementation来分配一个新的依赖

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 someStrings = ImmutableList.of("Blogdemo", "is", "cool");
        assertThat(underTest.getUser(), is("lorem"));
        assertThat(underTest.getPassword(), is("ipsum"));
        assertThat(someStrings.size(), is(3));
    }
}

为了能够运行它,我们需要定义一个使用编译输出的自定义测试任务

// source sets declarations
// dependencies declarations 
task itest(type: Test) {
    description = "Run integration tests"
    group = "verification"
    testClassesDirs = sourceSets.itest.output.classesDirs
    classpath = sourceSets.itest.runtimeClasspath
}

**这些声明在配置阶段 **进行评估。因此,它们的顺序很重要

例如,在声明 this 之前,我们不能引用任务主体中设置的itest源。

让我们看看如果我们运行测试会发生什么:

$ ./gradlew clean itest
// some compilation issues
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':source-sets:compileItestJava'.
> Compilation failed; see the compiler error output for details.

与之前的运行不同,这次我们得到了编译错误。所以发生了什么事?

这个新的源集创建了一个独立的配置。

换句话说,itestImplementation不继承JUnit依赖,也不获取main的输出。**

让我们在 Gradle 配置中解决这个问题:

sourceSets{
    itest {
        compileClasspath += sourceSets.main.output
        runtimeClasspath += sourceSets.main.output
        java {
        }
    }
}
// dependencies declaration
configurations {
    itestImplementation.extendsFrom(testImplementation)
    itestRuntimeOnly.extendsFrom(testRuntimeOnly)
}

现在让我们重新运行我们的集成测试:

$ ./gradlew clean itest
> Task :source-sets:itest
com.blogdemo.itest.SourceSetsItest > givenImmutableList_whenRun_ThenSuccess PASSED

测试通过。

3.3. Eclipse IDE 处理

到目前为止,我们已经看到了如何直接使用 Gradle 处理源集。然而,大多数时候,我们将使用 IDE(例如 Eclipse)。

当我们导入项目时,我们会遇到一些编译问题:

/uploads/gradle_source_sets/1.png

但是,如果我们从 Gradle 运行集成测试,我们不会收到任何错误:

$ ./gradlew clean itest
> Task :source-sets:itest
com.blogdemo.itest.SourceSetsItest > givenImmutableList_whenRun_ThenSuccess PASSED

所以发生了什么事?在这种情况下,guava依赖属于 itestImplementation

不幸的是,Eclipse Buildship Gradle 插件不能很好地处理这些自定义配置

让我们build.gradle中解决这个问题:

apply plugin: "eclipse"
// previous declarations
eclipse {
    classpath {
        plusConfigurations+=[configurations.itestCompileClasspath] 
    } 
}

让我们解释一下我们在这里做了什么。我们将配置附加到 Eclipse 类路径中。

如果我们刷新项目,编译问题就消失了。

但是,这种方法有一个缺点:IDE 不区分配置。

这意味着我们可以轻松地在我们的test源中导入guava(我们特别想避免这种情况)。