从 Gradle 4.0 开始,构建工具完全支持缓存纯 Java 项目。用于编译、测试、文档化和检查 Java 代码质量的内置任务开箱即用地支持构建缓存。

Java 编译

缓存 Java 编译利用了 Gradle 对编译类路径的深入理解。当依赖项以不影响其应用程序二进制接口 (ABI) 的方式更改时,该机制避免重新编译。由于缓存键仅受依赖项的 ABI(而不是其实现细节,如私有类型和方法体)的影响,因此如果任务输出是由相同的源和 ABI 等效的依赖项产生的,则任务输出缓存也可以重用已编译的类。

例如,以一个包含两个模块的项目为例:一个应用程序依赖于一个库。假设最新版本已经由 CI 构建并上传到共享缓存。如果开发人员现在修改了库中某个方法的主体,则需要在他们的计算机上重建该库。但是他们将能够从共享缓存中加载应用程序的已编译类。Gradle 可以做到这一点,因为 CI 上用于编译应用程序的库和本地可用的修改后的库共享相同的 ABI。

注解处理器

编译避免机制开箱即用。但是,有一个注意事项:当使用注解处理器时,Gradle 使用注解处理器类路径作为输入。与大多数编译依赖项(其中只有 ABI 影响编译)不同,注解处理器的实现必须被视为编译器的输入。因此,Gradle 会将注解处理器视为运行时类路径,这意味着较少的输入规范化正在发生。如果 Gradle 检测到编译类路径上有注解处理器,则当未显式设置注解处理器类路径时,注解处理器类路径默认为编译类路径,这反过来意味着整个编译类路径都被视为运行时类路径输入。

对于上面的示例,这意味着从编译类路径中提取的 ABI 将保持不变,但注解处理器类路径(因为它没有被编译避免机制处理)将是不同的。最终,开发人员将不得不重新编译应用程序。

避免这种性能损失的最简单方法是不使用注解处理器。但是,如果您需要使用它们,请确保显式设置注解处理器类路径,使其仅包含注解处理所需的库。关于 Java 编译避免的部分描述了如何做到这一点。

一些常见的 Java 依赖项(例如 Log4j 2.x)捆绑了注解处理器。如果您使用这些依赖项,但不利用捆绑的注解处理器的功能,则最好完全禁用注解处理。这可以通过将注解处理器类路径设置为空集来完成。

单元测试执行

用于 JVM 语言测试执行的 Test 任务为其类路径采用了运行时类路径规范化。这意味着测试类路径上 jar 包中顺序和时间戳的更改不会导致任务过期或更改构建缓存键。为了实现稳定的任务输入,您还可以运用过滤运行时类路径的力量。

集成测试执行

单元测试很容易缓存,因为它们通常没有外部依赖项。对于集成测试,情况可能大相径庭,因为它们可能依赖于测试和生产代码之外的各种输入。这些外部因素可以是例如:

  • 操作系统类型和版本,

  • 为测试安装的外部工具,

  • 环境变量和 Java 系统属性,

  • 正在运行的其他服务,

  • 被测软件的发行版。

您需要小心地为您的集成测试声明这些附加输入,以避免不正确的缓存命中。例如,将 Gradle 使用的操作系统声明为名为 integTestTest 任务的输入,如下所示:

build.gradle.kts
tasks.integTest {
    inputs.property("operatingSystem") {
        System.getProperty("os.name")
    }
}
build.gradle
tasks.named('integTest') {
    inputs.property("operatingSystem") {
        System.getProperty("os.name")
    }
}

将归档文件作为输入

集成测试通常依赖于您打包的应用程序。如果碰巧是 zip 或 tar 归档文件,则将其作为集成测试任务的输入添加可能会导致缓存未命中。这是因为,正如可重复的任务输出中所述,重建归档文件通常会更改归档文件中的元数据。您可以依赖于归档文件的解压缩内容。另请参阅关于处理不可重复输出的部分。

处理文件路径

您可能会通过使用系统属性将一些信息从构建环境传递到您的集成测试任务。传递绝对路径将破坏集成测试任务的可重定位性

build.gradle.kts
// Don't do this! Breaks relocatability!
tasks.integTest {
    systemProperty("distribution.location", layout.buildDirectory.dir("dist").get().asFile.absolutePath)
}
build.gradle
// Don't do this! Breaks relocatability!
tasks.named('integTest') {
    systemProperty "distribution.location", layout.buildDirectory.dir('dist').get().asFile.absolutePath
}

与其直接将绝对路径作为系统属性添加,不如将带注解的 CommandLineArgumentProvider 添加到 integTest 任务

build.gradle.kts
abstract class DistributionLocationProvider : CommandLineArgumentProvider {  (1)
    @get:InputDirectory
    @get:PathSensitive(PathSensitivity.RELATIVE)  (2)
    abstract val distribution: DirectoryProperty

    override fun asArguments(): Iterable<String> =
        listOf("-Ddistribution.location=${distribution.get().asFile.absolutePath}")  (3)
}

tasks.integTest {
    jvmArgumentProviders.add(
        objects.newInstance<DistributionLocationProvider>().apply {  (4)
            distribution = layout.buildDirectory.dir("dist")
        }
    )
}
build.gradle
abstract class DistributionLocationProvider implements CommandLineArgumentProvider {  (1)
    @InputDirectory
    @PathSensitive(PathSensitivity.RELATIVE)  (2)
    abstract DirectoryProperty getDistribution()

    @Override
    Iterable<String> asArguments() {
        ["-Ddistribution.location=${distribution.get().asFile.absolutePath}"]  (3)
    }
}

tasks.named('integTest') {
    jvmArgumentProviders.add(
        objects.newInstance(DistributionLocationProvider).tap {  (4)
            distribution = layout.buildDirectory.dir('dist')
        }
    )
}
1 创建一个实现 CommandLineArgumentProvider 的类。
2 使用相应的路径敏感性声明输入和输出。
3 asArguments 需要返回 JVM 参数,将所需的系统属性传递给测试 JVM。
4 将新创建的类的实例作为 JVM 参数提供程序添加到集成测试任务。[1]

忽略系统属性

可能需要忽略某些系统属性作为输入,因为它们不影响集成测试的结果。为了做到这一点,将 CommandLineArgumentProvider 添加到 integTest 任务

build.gradle.kts
abstract class CiEnvironmentProvider : CommandLineArgumentProvider {
    @get:Internal  (1)
    abstract val agentNumber: Property<String>

    override fun asArguments(): Iterable<String> =
        listOf("-DagentNumber=${agentNumber.get()}")  (2)
}

tasks.integTest {
    jvmArgumentProviders.add(
        objects.newInstance<CiEnvironmentProvider>().apply {  (3)
            agentNumber = providers.environmentVariable("AGENT_NUMBER").orElse("1")
        }
    )
}
build.gradle
abstract class CiEnvironmentProvider implements CommandLineArgumentProvider {
    @Internal  (1)
    abstract Property<String> getAgentNumber()

    @Override
    Iterable<String> asArguments() {
        ["-DagentNumber=${agentNumber.get()}"]  (2)
    }
}

tasks.named('integTest') {
    jvmArgumentProviders.add(
        objects.newInstance(CiEnvironmentProvider).tap {  (3)
            agentNumber = providers.environmentVariable("AGENT_NUMBER").orElse("1")
        }
    )
}
1 @Internal 意味着此属性不影响集成测试的输出。
2 实际测试执行的系统属性。
3 将新创建的类的实例作为 JVM 参数提供程序添加到集成测试任务。[1]

1. 本例中的 CommandLineArgumentProvider 被实现为托管类型