从 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)与注释处理器捆绑在一起。如果您使用这些依赖项,但不利用捆绑注释处理器的功能,则最好完全禁用注释处理。这可以通过将注释处理器类路径设置为空集来完成。 |
单元测试执行
Test
用于 JVM 语言测试执行的任务对其类路径采用运行时类路径规范化。这意味着测试类路径上 jar 中的顺序和时间戳的更改不会导致任务过期或更改构建缓存键。为了实现稳定的任务输入,您还可以利用过滤运行时类路径的功能。
集成测试执行
单元测试很容易缓存,因为它们通常没有外部依赖项。对于集成测试,情况可能完全不同,因为它们可能依赖于测试和生产代码之外的各种输入。这些外部因素可以是例如:
-
操作系统类型和版本,
-
正在安装用于测试的外部工具,
-
环境变量和Java系统属性,
-
其他服务正在启动并运行,
-
被测软件的发行版。
您需要小心地为集成测试声明这些附加输入,以避免错误的缓存命中。例如,将 Gradle 使用的操作系统声明为调用的Test
任务的输入integTest
将按如下方式工作:
tasks.integTest {
inputs.property("operatingSystem") {
System.getProperty("os.name")
}
}
tasks.named('integTest') {
inputs.property("operatingSystem") {
System.getProperty("os.name")
}
}
处理文件路径
您可能会使用系统属性将一些信息从构建环境传递到集成测试任务。传递绝对路径将破坏集成测试任务的可重定位性。
// Don't do this! Breaks relocatability!
tasks.integTest {
systemProperty("distribution.location", layout.buildDirectory.dir("dist").get().asFile.absolutePath)
}
// Don't do this! Breaks relocatability!
tasks.named('integTest') {
systemProperty "distribution.location", layout.buildDirectory.dir('dist').get().asFile.absolutePath
}
可以将带注释的CommandLineArgumentProvider添加到任务中,而不是直接将绝对路径添加为系统属性integTest
:
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")
}
)
}
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
:
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")
}
)
}
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 ] |