编写可维护构建的最佳实践
Gradle 拥有丰富的 API,提供多种创建构建逻辑的方法。相关的灵活性很容易导致不必要的复杂构建,而自定义代码通常直接添加到构建脚本中。在本章中,我们介绍了几种最佳实践,将帮助您开发易于使用的、富有表现力和可维护的构建。
如果您感兴趣的话, 第三方Gradle lint 插件有助于在构建脚本中强制执行所需的代码样式。 |
避免在脚本中使用命令式逻辑
Gradle 运行时不强制构建逻辑的特定样式。正是出于这个原因,很容易最终得到一个将声明性 DSL 元素与命令式过程代码混合在一起的构建脚本。我们来谈谈一些具体的例子。
-
声明性代码:内置的、与语言无关的 DSL 元素(例如Project.dependency{}或Project.repositories{})或插件公开的 DSL
-
命令式代码:条件逻辑或非常复杂的任务操作实现
每个构建脚本的最终目标应该是仅包含声明性语言元素,这使得代码更易于理解和维护。命令式逻辑应该存在于二进制插件中,然后应用于构建脚本。作为副产品,如果您将工件发布到二进制存储库,您将自动使您的团队能够在其他项目中重用插件逻辑。
以下示例构建显示了直接在构建脚本中使用条件逻辑的反面示例。虽然此代码片段很小,但很容易想象一个使用大量过程语句的完整构建脚本及其对可读性和可维护性的影响。通过将代码移动到一个类中,也可以单独进行测试。
if (project.findProperty("releaseEngineer") != null) {
tasks.register("release") {
doLast {
logger.quiet("Releasing to production...")
// release the artifact to production
}
}
}
if (project.findProperty('releaseEngineer') != null) {
tasks.register('release') {
doLast {
logger.quiet 'Releasing to production...'
// release the artifact to production
}
}
}
让我们将构建脚本与作为二进制插件实现的相同逻辑进行比较。该代码乍一看可能看起来更复杂,但显然看起来更像典型的应用程序代码。这个特定的插件类位于buildSrc
目录中,这使得构建脚本可以自动使用它。
package com.enterprise;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.tasks.TaskProvider;
public class ReleasePlugin implements Plugin<Project> {
private static final String RELEASE_ENG_ROLE_PROP = "releaseEngineer";
private static final String RELEASE_TASK_NAME = "release";
@Override
public void apply(Project project) {
if (project.findProperty(RELEASE_ENG_ROLE_PROP) != null) {
Task task = project.getTasks().create(RELEASE_TASK_NAME);
task.doLast(new Action<Task>() {
@Override
public void execute(Task task) {
task.getLogger().quiet("Releasing to production...");
// release the artifact to production
}
});
}
}
}
现在构建逻辑已转换为插件,您可以将其应用到构建脚本中。构建脚本已从 8 行代码缩减为一行。
plugins {
id("com.enterprise.release")
}
plugins {
id 'com.enterprise.release'
}
避免使用内部 Gradle API
当 Gradle 或插件发生更改时,在插件和构建脚本中使用 Gradle 内部 API 可能会破坏构建。
Gradle 公共 API 定义
和
Kotlin DSL API 定义中列出了以下包
,internal
名称中带有 的任何子包除外。
org.gradle org.gradle.api.* org.gradle.authentication.* org.gradle.build.* org.gradle.buildinit.* org.gradle.caching.* org.gradle.concurrent.* org.gradle.deployment.* org.gradle.external.javadoc.* org.gradle.ide.* org.gradle.ivy.* org.gradle.jvm.* org.gradle.language.* org.gradle.maven.* org.gradle.nativeplatform.* org.gradle.normalization.* org.gradle.platform.* org.gradle.plugin.devel.* org.gradle.plugin.use org.gradle.plugin.management org.gradle.plugins.* org.gradle.process.* org.gradle.testfixtures.* org.gradle.testing.jacoco.* org.gradle.tooling.* org.gradle.swiftpm.* org.gradle.model.* org.gradle.testkit.* org.gradle.testing.* org.gradle.vcs.* org.gradle.work.* org.gradle.workers.* org.gradle.util.*
org.gradle.kotlin.dsl org.gradle.kotlin.dsl.precompile
常用内部 API 的替代方案
要为您的自定义任务提供嵌套 DSL,请勿使用org.gradle.internal.reflect.Instantiator
;请改用ObjectFactory。阅读有关延迟配置的章节也可能会有所帮助。
代替org.gradle.internal.os.OperatingSystem
,使用另一种方法来检测操作系统,例如Apache commons-lang SystemUtils或System.getProperty("os.name")
.
使用其他集合或 I/O 框架代替org.gradle.util.CollectionUtils
、org.gradle.util.internal.GFileUtils
和 下的其他类org.gradle.util.*
。
声明任务时遵循约定
任务 API 为构建作者在构建脚本中声明任务提供了很大的灵活性。为了获得最佳的可读性和可维护性,请遵循以下规则:
-
任务类型应该是任务名称后面括号内的唯一键值对。
-
其他配置应在任务的配置块内完成。
-
声明任务时添加的任务操作只能使用方法Task.doFirst{}或Task.doLast{}声明。
-
声明临时任务(没有显式类型的任务)时,如果您仅声明单个操作,则应使用Task.doLast{} 。
-
任务应该定义组和描述。
import com.enterprise.DocsGenerate
tasks.register<DocsGenerate>("generateHtmlDocs") {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = "Generates the HTML documentation for this project."
title = "Project docs"
outputDir = layout.buildDirectory.dir("docs")
}
tasks.register("allDocs") {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = "Generates all documentation for this project."
dependsOn("generateHtmlDocs")
doLast {
logger.quiet("Generating all documentation...")
}
}
import com.enterprise.DocsGenerate
def generateHtmlDocs = tasks.register('generateHtmlDocs', DocsGenerate) {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = 'Generates the HTML documentation for this project.'
title = 'Project docs'
outputDir = layout.buildDirectory.dir('docs')
}
tasks.register('allDocs') {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = 'Generates all documentation for this project.'
dependsOn generateHtmlDocs
doLast {
logger.quiet('Generating all documentation...')
}
}
提高任务的可发现性
即使是构建的新用户也应该能够快速、轻松地找到关键信息。在 Gradle 中,您可以为构建的任何任务声明一个组和描述。任务报告使用分配的值来组织和呈现任务,以便于发现。分配组和描述对于您希望构建用户调用的任何任务最有帮助。
该示例任务generateDocs
以 HTML 页面的形式生成项目文档。任务应该组织在桶下面Documentation
。描述应表达其意图。
tasks.register("generateDocs") {
group = "Documentation"
description = "Generates the HTML documentation for this project."
doLast {
// action implementation
}
}
tasks.register('generateDocs') {
group = 'Documentation'
description = 'Generates the HTML documentation for this project.'
doLast {
// action implementation
}
}
任务报告的输出反映了分配的值。
> gradle tasks > Task :tasks Documentation tasks ------------------- generateDocs - Generates the HTML documentation for this project.
最小化配置阶段执行的逻辑
对于每个构建脚本开发人员来说,了解构建生命周期的不同阶段及其对构建逻辑的性能和评估顺序的影响非常重要。在配置阶段,应该配置项目及其域对象,而执行阶段仅执行命令行上请求的任务的操作及其依赖项。请注意,任何不属于任务操作的代码都将在每次构建运行时执行。构建扫描可以帮助您确定每个生命周期阶段所花费的时间。它是诊断常见性能问题的宝贵工具。
让我们考虑一下上述反模式的以下咒语。在构建脚本中,您可以看到分配给配置的依赖项printArtifactNames
是在任务操作之外解析的。
dependencies {
implementation("log4j:log4j:1.2.17")
}
tasks.register("printArtifactNames") {
// always executed
val libraryNames = configurations.compileClasspath.get().map { it.name }
doLast {
logger.quiet(libraryNames.joinToString())
}
}
dependencies {
implementation 'log4j:log4j:1.2.17'
}
tasks.register('printArtifactNames') {
// always executed
def libraryNames = configurations.compileClasspath.collect { it.name }
doLast {
logger.quiet libraryNames
}
}
用于解决依赖关系的代码应移至任务操作中,以避免在实际需要依赖关系之前解决依赖关系对性能造成影响。
dependencies {
implementation("log4j:log4j:1.2.17")
}
tasks.register("printArtifactNames") {
val compileClasspath: FileCollection = configurations.compileClasspath.get()
doLast {
val libraryNames = compileClasspath.map { it.name }
logger.quiet(libraryNames.joinToString())
}
}
dependencies {
implementation 'log4j:log4j:1.2.17'
}
tasks.register('printArtifactNames') {
FileCollection compileClasspath = configurations.compileClasspath
doLast {
def libraryNames = compileClasspath.collect { it.name }
logger.quiet libraryNames
}
}
避免使用GradleBuild
任务类型
GradleBuild任务类型允许构建脚本定义调用另一个 Gradle 构建的任务。通常不鼓励使用这种类型。在某些特殊情况下,调用的构建不会公开与命令行或工具 API 相同的运行时行为,从而导致意外结果。
通常,有更好的方法来对需求进行建模。适当的方法取决于当前的问题。这里有一些选项:
避免项目间配置
Gradle 不限制构建脚本作者在多项目构建中从一个项目进入另一个项目的域模型。强耦合的项目会损害构建执行性能以及代码的可读性和可维护性。
应避免以下做法:
-
通过Task.dependsOn(java.lang.Object...)显式依赖于另一个项目的任务。
-
从另一个项目设置属性值或调用域对象的方法。
-
使用GradleBuild执行构建的另一部分。
-
声明不必要的项目依赖关系。
外部化并加密您的密码
大多数构建需要使用一个或多个密码。这种需要的原因可能有所不同。某些构建需要密码才能将工件发布到安全的二进制存储库,其他构建需要密码才能下载二进制文件。密码应始终安全,以防止欺诈。在任何情况下都不应将密码以纯文本形式添加到构建脚本中或在gradle.properties
项目目录的文件中声明它。这些文件通常位于版本控制存储库中,任何有权访问它的人都可以查看。
密码和任何其他敏感数据应保存在版本控制项目文件的外部。 Gradle 公开了一个 API,用于在ProviderFactory
和Artifact Repositories中提供凭证
,允许
在构建需要时使用Gradle 属性提供凭证值。这样,凭据可以存储在gradle.properties
驻留在用户主目录中的文件中,或者使用命令行参数或环境变量注入到构建中。
如果您将敏感凭据存储在用户 home 中gradle.properties
,请考虑对其进行加密。目前 Gradle 不提供用于加密、存储和访问密码的内置机制。解决这个问题的一个很好的解决方案是Gradle Credentials 插件。
不要预期配置创建
Gradle 将使用“如果需要则检查”策略创建某些配置,例如default
或。archives
这意味着它只会创建这些配置(如果它们尚不存在)。
您不应该自己创建这些配置。诸如此类的名称以及与源集关联的配置的名称应被视为隐式“保留”。保留名称的确切列表取决于应用的插件以及构建的配置方式。
这种情况将通过以下弃用警告来宣布:
Configuration customCompileClasspath already exists with permitted usage(s):
Consumable - this configuration can be selected by another project as a dependency
Resolvable - this configuration can be resolved by this project to a set of files
Declarable - this configuration can have dependencies added to it
Yet Gradle expected to create it with the usage(s):
Resolvable - this configuration can be resolved by this project to a set of files
然后,Gradle 将尝试改变允许的用法以匹配预期的用法,并发出第二个警告:
Gradle will mutate the usage of this configuration to match the expected usage. This may cause unexpected behavior. Creating configurations with reserved names has been deprecated. This is scheduled to be removed in Gradle 9.0. Create source sets prior to creating or accessing the configurations associated with them.
某些配置可能会锁定其用途以防止突变。在这种情况下,您的构建将失败,并且此警告之后将立即出现带有以下消息的异常:
Gradle cannot mutate the usage of configuration 'customCompileClasspath' because it is locked.
如果您遇到此错误,您必须:
-
更改配置的名称以避免冲突。
-
如果无法更改名称,请确保配置允许的用途(可消耗的、可解析的、可声明的)与 Gradle 的期望一致。
作为最佳实践,您不应该“预期”配置创建 - 让 Gradle 首先创建配置,然后调整它。或者,如果可能,请在看到此警告时重命名自定义配置,使用不冲突的名称。