Gradle 擁有豐富的 API,具備多種建立建置邏輯的方法。相關的彈性很容易導致不必要的複雜建置,而自訂程式碼通常會直接新增到建置指令碼中。在本章中,我們將介紹多項最佳實務,協助您開發易於使用的表達式且可維護的建置。

第三方 Gradle lint 外掛程式 可協助在建置指令碼中強制執行所需的程式碼樣式,如果您有興趣的話。

避免在指令碼中使用命令式邏輯

Gradle 執行時期不會強制套用特定的建置邏輯樣式。正因如此,很容易就會產生混合了宣告式 DSL 元素和命令式程序碼的建置指令碼。讓我們來談談一些具體的範例。

  • 宣告式程式碼:內建、與語言無關的 DSL 元素(例如 Project.dependencies{}Project.repositories{})或外掛程式公開的 DSL

  • 命令式程式碼:條件式邏輯或非常複雜的任務動作實作

每個建置指令碼的最終目標應該是只包含宣告式語言元素,這會讓程式碼更容易理解和維護。命令式邏輯應該存在於二進位外掛程式中,而二進位外掛程式會套用至建置指令碼。作為附帶產品,如果你將成品發布到二進位儲存庫,你會自動讓你的團隊能夠在其他專案中重複使用外掛程式邏輯

下列範例建置顯示在建置指令碼中直接使用條件式邏輯的負面範例。儘管這段程式碼很小,但很容易想像一個使用大量程序化陳述式的完整建置指令碼,以及它對可讀性和可維護性的影響。透過將程式碼移至類別中,也可以個別測試它。

build.gradle.kts
if (project.findProperty("releaseEngineer") != null) {
    tasks.register("release") {
        doLast {
            logger.quiet("Releasing to production...")

            // release the artifact to production
        }
    }
}
build.gradle
if (project.findProperty('releaseEngineer') != null) {
    tasks.register('release') {
        doLast {
            logger.quiet 'Releasing to production...'

            // release the artifact to production
        }
    }
}

讓我們將建置指令碼與實作為二進位外掛程式的相同邏輯進行比較。乍看之下,程式碼可能看起來比較複雜,但顯然更像典型的應用程式碼。這個特定的外掛程式類別存在於 buildSrc 目錄 中,這會讓建置指令碼自動使用它。

ReleasePlugin.java
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 行程式碼縮減為一行。

build.gradle.kts
plugins {
    id("com.enterprise.release")
}
build.gradle
plugins {
    id 'com.enterprise.release'
}

避免使用 Gradle 內部 API

在插件和建置指令碼中使用 Gradle 內部 API,可能會在 Gradle 或插件變更時中斷建置。

下列套件列在 Gradle 公共 API 定義Kotlin DSL API 定義 中,但名稱中含有 internal 的子套件除外。

Gradle API 套件
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.*
Kotlin DSL API 套件
org.gradle.kotlin.dsl
org.gradle.kotlin.dsl.precompile

常用內部 API 的替代方案

若要提供自訂任務的巢狀 DSL,請勿使用 org.gradle.internal.reflect.Instantiator;請改用 ObjectFactory。閱讀 惰性組態章節 也可能有所幫助。

請勿使用 org.gradle.api.internal.ConventionMapping。請使用 Provider 和/或 Property。您可以在 實作插件章節 中找到一個範例,說明如何擷取使用者輸入以組態執行時期行為。

請勿使用 org.gradle.internal.os.OperatingSystem,請改用其他方法來偵測作業系統,例如 Apache commons-lang SystemUtilsSystem.getProperty("os.name")

請使用其他集合或 I/O 架構,而非 org.gradle.util.CollectionUtilsorg.gradle.util.internal.GFileUtils,以及 org.gradle.util.* 下的其他類別。

宣告任務時遵循慣例

任務 API 提供建置作者很大的彈性,可以在建置指令碼中宣告任務。若要達到最佳的可讀性和可維護性,請遵循下列規則

build.gradle.kts
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...")
    }
}
build.gradle
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 區段下方。說明應表達其用意。

build.gradle.kts
tasks.register("generateDocs") {
    group = "Documentation"
    description = "Generates the HTML documentation for this project."

    doLast {
        // action implementation
    }
}
build.gradle
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 的相依性是在工作任務動作之外解析的。

build.gradle.kts
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())
    }
}
build.gradle
dependencies {
    implementation 'log4j:log4j:1.2.17'
}

tasks.register('printArtifactNames') {
    // always executed
    def libraryNames = configurations.compileClasspath.collect { it.name }

    doLast {
        logger.quiet libraryNames
    }
}

解析相依性的程式碼應移至工作任務動作中,以避免在實際需要相依性之前解析相依性所造成的效能影響。

build.gradle.kts
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())
    }
}
build.gradle
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 建置。通常不建議使用此類型。在某些特殊情況下,呼叫的建置不會公開與從命令列或透過 Tooling API 相同的執行時間行為,導致產生意外的結果。

通常,有更好的方法來建模需求。適當的方法取決於手邊的問題。以下是幾個選項

  • 如果目的是將不同模組的工作任務作為統一建置執行,則將建置建模為 多專案建置

  • 對於實體上分開但偶爾應作為單一單元建置的專案,請使用 複合建置

避免跨專案組態

多專案建置 中,Gradle 沒有限制建置指令碼作者從一個專案進入另一個專案的網域模型。緊密耦合的專案會損害 建置執行效能,以及程式碼的可讀性和可維護性。

應避免下列作法

將您的密碼外部分離並加密

大多數的組建都需要使用一個或多個密碼。需要這樣做的原因可能有所不同。有些組建需要密碼才能將成品發布到安全的二進位儲存庫,而其他組建則需要密碼才能下載二進位檔案。密碼應始終保持安全,以防止詐騙。在任何情況下,您都不應將密碼以純文字新增到組建指令碼中,或在專案目錄中的 gradle.properties 檔案中宣告它。這些檔案通常會儲存在版本控制儲存庫中,而且任何有權存取這些檔案的人都可以檢視這些檔案。

密碼以及任何其他敏感資料都應從版本控制的專案檔案中外部分離。Gradle 公開了一個 API,用於在 ProviderFactory 中提供憑證,以及 成品儲存庫,允許在組建需要時使用 Gradle 屬性 提供憑證值。這樣一來,憑證就可以儲存在位於使用者家目錄中的 gradle.properties 檔案中,或使用命令列引數或環境變數注入到組建中。

如果您將敏感憑證儲存在使用者的家目錄 gradle.properties 中,請考慮對其加密。目前,Gradle 沒有提供內建機制來加密、儲存和存取密碼。解決此問題的一個好方法是 Gradle 憑證外掛程式

不要預期組態建立

Gradle 將使用「視需要檢查」策略建立特定的組態,例如 defaultarchives。這表示它只會在這些組態不存在時建立這些組態。

不應自行建立這些組態。這些名稱,以及與來源組相關聯的組態名稱,應被視為隱含「保留」。保留名稱的確切清單取決於套用的外掛程式以及組建的組態方式。

此情況將透過下列不建議使用的警告公告

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.

如果您遇到此錯誤,您必須

  1. 變更組態名稱以避免衝突。

  2. 如果無法變更名稱,請確保組態允許的用法(消耗、可解析、可宣告)與 Gradle 的預期相符。

最佳實務是不要「預期」組態建立 - 讓 Gradle 先建立組態,然後再調整。或者,如果可能,請為自訂組態使用非衝突名稱,在看到此警告時重新命名。