本節涵蓋 Gradle 提供的機制,可直接影響相依性解析引擎的行為。與本章中涵蓋的其他概念不同,例如 相依性約束元件資料規則,這些都是解析的輸入,下列機制讓您可以撰寫直接注入解析引擎的規則。因此,它們可以視為強制解決方案,可能會隱藏未來的問題(例如,如果新增新的相依性)。因此,一般建議僅在其他方式不足夠時才使用下列機制。如果您正在撰寫 函式庫,您應該始終優先考慮 相依性約束,因為它們已發布給您的使用者。

使用相依性解析規則

相依性解析規則會針對每個已解析的相依性執行,並提供強大的 API,用於在解析相依性之前操作請求的相依性。此功能目前提供變更請求的相依性的群組、名稱和/或版本的可能性,允許在解析期間以完全不同的模組替換相依性。

相依性解析規則提供一個非常強大的方式來控制相依性解析程序,且可用於實作相依性管理中的各種進階模式。以下概述其中一些模式。如需更多資訊和程式碼範例,請參閱 API 文件中的 ResolutionStrategy 類別。

實作自訂版本編號配置

在某些企業環境中,可在 Gradle 建置中宣告的模組版本清單會在外部維護和稽核。相依性解析規則提供此模式的簡潔實作

  • 在建置指令碼中,開發人員會使用模組群組和名稱宣告相依性,但使用 placeholder 版本,例如:default

  • default 版本會透過相依性解析規則解析為特定版本,此規則會在企業核准模組的目錄中查詢版本。

此規則實作可簡潔地封裝在企業外掛程式中,並在組織中的所有建置中共用。

build.gradle.kts
configurations.all {
    resolutionStrategy.eachDependency {
        if (requested.version == "default") {
            val version = findDefaultVersionInCatalog(requested.group, requested.name)
            useVersion(version.version)
            because(version.because)
        }
    }
}

data class DefaultVersion(val version: String, val because: String)

fun findDefaultVersionInCatalog(group: String, name: String): DefaultVersion {
    //some custom logic that resolves the default version into a specific version
    return DefaultVersion(version = "1.0", because = "tested by QA")
}
build.gradle
configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        if (details.requested.version == 'default') {
            def version = findDefaultVersionInCatalog(details.requested.group, details.requested.name)
            details.useVersion version.version
            details.because version.because
        }
    }
}

def findDefaultVersionInCatalog(String group, String name) {
    //some custom logic that resolves the default version into a specific version
    [version: "1.0", because: 'tested by QA']
}

拒絕特定版本並提供替代版本

相依性解析規則提供一種機制,可拒絕特定版本的相依性並提供替代版本。如果某個相依性版本已損毀且不應使用,這會很有用,其中相依性解析規則會導致此版本被已知的良好版本取代。已損毀模組的一個範例是宣告對在任何公開儲存庫中都找不到的函式庫的相依性,但還有許多其他原因會導致某個特定模組版本不受歡迎,而偏好不同的版本。

在以下範例中,假設版本 1.2.1 包含重要的修正,且應優先於 1.2 使用。提供的規則將強制執行此規則:任何時候遇到版本 1.2,它都會被 1.2.1 取代。請注意,這與上述強制版本不同,因為此模組的任何其他版本都不會受到影響。這表示,如果此版本也經過傳遞式擷取,則「最新」衝突解決策略仍會選取版本 1.3

build.gradle.kts
configurations.all {
    resolutionStrategy.eachDependency {
        if (requested.group == "org.software" && requested.name == "some-library" && requested.version == "1.2") {
            useVersion("1.2.1")
            because("fixes critical bug in 1.2")
        }
    }
}
build.gradle
configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        if (details.requested.group == 'org.software' && details.requested.name == 'some-library' && details.requested.version == '1.2') {
            details.useVersion '1.2.1'
            details.because 'fixes critical bug in 1.2'
        }
    }
}

使用豐富版本約束reject 指令時,會有所不同:如果在圖形中找到被拒絕的版本,豐富版本會導致建置失敗,或在使用動態相依項時選擇非被拒絕的版本。在此,我們操作所要求的版本,以便在找到被拒絕的版本時選擇不同的版本。換句話說,這是被拒絕版本的一種解決方案,而豐富版本約束允許宣告意圖(您不應該使用此版本)。

使用模組替換規則

最好以功能衝突來表達模組衝突。但是,如果沒有宣告此類規則,或者您正在使用不支援功能的 Gradle 版本,Gradle 會提供工具來解決這些問題。

模組替換規則允許建置宣告舊版程式庫已被新程式庫取代。新程式庫取代舊版程式庫的一個好範例是 google-collections -> guava 遷移。建立 google-collections 的團隊決定將模組名稱從 com.google.collections:google-collections 變更為 com.google.guava:guava。這是業界的合法情境:團隊需要能夠變更他們維護的產品名稱,包括模組座標。模組座標的重新命名會影響衝突解決。

為了說明對衝突解決的影響,我們來考慮 google-collections -> guava 情境。可能會發生兩個程式庫都被拉進同一個相依項圖形中。例如,我們的專案相依於 guava,但我們的某些相依項會拉進舊版 google-collections。這可能會導致執行時期錯誤,例如在測試或應用程式執行期間。Gradle 沒有自動解決 google-collections -> guava 衝突,因為它不被視為版本衝突。這是因為兩個程式庫的模組座標完全不同,而且只有當 groupmodule 座標相同,但相依項圖形中有不同的版本可用時,才會啟動衝突解決(如需更多資訊,請參閱衝突解決部分)。解決此問題的傳統方法是

  • 宣告排除規則以避免將 google-collections 拉進圖形中。這可能是最熱門的方法。

  • 避免拉進舊版程式庫的相依項。

  • 如果新版本不再拉進舊版程式庫,請升級相依項版本。

  • 降級到 google-collections。不建議這樣做,僅為完整性而提及。

傳統方法有效,但不夠通用。例如,組織希望在所有專案中解決 google-collections -> guava 衝突解決問題。可以宣告某個模組已被其他模組取代。這使組織能夠在公司外掛程式套件中包含有關模組替換的資訊,並為企業中所有由 Gradle 提供支援的專案整體解決問題。

build.gradle.kts
dependencies {
    modules {
        module("com.google.collections:google-collections") {
            replacedBy("com.google.guava:guava", "google-collections is now part of Guava")
        }
    }
}
build.gradle
dependencies {
    modules {
        module("com.google.collections:google-collections") {
            replacedBy("com.google.guava:guava", "google-collections is now part of Guava")
        }
    }
}

如需更多範例和詳細的 API,請參閱 ComponentMetadataHandler 的 DSL 參考。

當我們宣告 google-collections 已被 guava 取代時,會發生什麼事?Gradle 可以使用此資訊來解決衝突。Gradle 會將 guava 的每個版本視為比 google-collections 的任何版本更新/更好。此外,Gradle 會確保只有 guava jar 存在於類別路徑/已解析的檔案清單中。請注意,如果只有 google-collections 出現在相依性圖表中(例如沒有 guava),Gradle 就不會急於將它替換為 guava。模組替換是 Gradle 用來解決衝突的資訊。如果沒有衝突(例如圖表中只有 google-collections 或只有 guava),則不會使用替換資訊。

目前無法宣告某個模組已被一組模組取代。但是,可以宣告多個模組已被單一模組取代。

使用相依性替換規則

相依性替換規則的工作方式類似於相依性解析規則。事實上,相依性解析規則的許多功能都可以透過相依性替換規則來實作。它們允許專案和模組相依性以透明的方式替換為指定的替換項。與相依性解析規則不同,相依性替換規則允許專案和模組相依性可以互換地替換。

將相依性替換規則新增到組態會變更該組態的解析時機。組態並非在第一次使用時解析,而是在建構工作圖表時解析。如果組態在工作執行期間進一步修改,或者組態依賴於在執行另一個工作時發佈的模組,則可能會產生意外的後果。

說明

  • Configuration 可以宣告為任何工作輸入,而該組態在解析時可以包含專案相依性。

  • 如果專案相依性是工作輸入(透過組態),則必須將建構專案成品的工作新增到工作相依性。

  • 為了確定輸入工作的專案相依性,Gradle 需要解析 Configuration 輸入。

  • 由於 Gradle 工作圖表在工作執行開始後便會固定,因此 Gradle 需要在執行任何工作之前執行此解析。

在沒有相依性替換規則的情況下,Gradle 知道外部模組相依性永遠不會遞迴參考專案相依性。這使得透過簡單的圖形遍歷,可以輕鬆地確定組態的專案相依性完整集合。有了此功能,Gradle 便無法再做出此假設,並且必須執行完整解析才能確定專案相依性。

用專案相依性取代外部模組相依性

相依性取代的一個使用案例是使用模組的本地開發版本,取代從外部儲存庫下載的版本。這對於測試相依性的本地修補版本很有用。

要取代的模組可以宣告為有或沒有指定版本。

build.gradle.kts
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute(module("org.utils:api"))
            .using(project(":api")).because("we work with the unreleased development version")
        substitute(module("org.utils:util:2.5")).using(project(":util"))
    }
}
build.gradle
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute module("org.utils:api") using project(":api") because "we work with the unreleased development version"
        substitute module("org.utils:util:2.5") using project(":util")
    }
}

請注意,被取代的專案必須包含在多專案建置中(透過 settings.gradle)。相依性取代規則會負責用專案相依性取代模組相依性,並連結任何工作相依性,但不會隱含地將專案包含在建置中。

用模組取代取代專案相依性

使用取代規則的另一種方式是用多專案建置中的模組取代專案相依性。這對於透過允許從儲存庫下載專案相依性的子集,而不是建置它們,來加速大型多專案建置的開發很有用。

要作為取代使用的模組必須宣告為有指定版本。

build.gradle.kts
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute(project(":api"))
            .using(module("org.utils:api:1.3")).because("we use a stable version of org.utils:api")
    }
}
build.gradle
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute project(":api") using module("org.utils:api:1.3") because "we use a stable version of org.utils:api"
    }
}

當專案相依性已被模組相依性取代時,該專案仍會包含在整體多專案建置中。但是,建置取代相依性的工作不會執行,以解決相依的 Configuration

有條件取代相依性

相依性取代的常見使用案例是允許在多專案建置中更靈活地組裝子專案。這對於開發外部相依性的本地修補版本,或在大型多專案建置中建置模組的子集很有用。

以下範例使用相依性取代規則來取代群組為 org.example 的任何模組相依性,但前提是能找到與相依性名稱相符的本地專案。

build.gradle.kts
configurations.all {
    resolutionStrategy.dependencySubstitution.all {
        requested.let {
            if (it is ModuleComponentSelector && it.group == "org.example") {
                val targetProject = findProject(":${it.module}")
                if (targetProject != null) {
                    useTarget(targetProject)
                }
            }
        }
    }
}
build.gradle
configurations.all {
    resolutionStrategy.dependencySubstitution.all { DependencySubstitution dependency ->
        if (dependency.requested instanceof ModuleComponentSelector && dependency.requested.group == "org.example") {
            def targetProject = findProject(":${dependency.requested.module}")
            if (targetProject != null) {
                dependency.useTarget targetProject
            }
        }
    }
}

請注意,被取代的專案必須包含在多專案建置中(透過 settings.gradle)。相依性取代規則會負責用專案相依性取代模組相依性,但不會隱含地將專案包含在建置中。

用另一種變體取代相依性

Gradle 的相依性管理引擎是 變異感知 的,這表示對於單一元件,引擎可能會選取不同的成品和傳遞相依性。

選取的內容是由消費者組態的屬性和在生產者端找到的變異屬性所決定。不過,有些特定相依性可能會覆寫組態本身的屬性。這通常是使用 Java Platform 外掛程式 時的情況:此外掛程式會建置一種稱為「平台」的特殊元件類型,而且可以透過將元件類別屬性設定為 platform 來處理,與目標為函式庫的典型相依性相反。

因此,您可能會遇到想要將平台相依性替換為一般相依性,或反之亦然的情況。

使用屬性替換相依性

假設您想要將平台相依性替換為一般相依性。這表示您正在使用的函式庫宣告類似這樣的內容

lib/build.gradle.kts
dependencies {
    // This is a platform dependency but you want the library
    implementation(platform("com.google.guava:guava:28.2-jre"))
}
lib/build.gradle
dependencies {
    // This is a platform dependency but you want the library
    implementation platform('com.google.guava:guava:28.2-jre')
}

platform 關鍵字實際上是 具有屬性的相依性 的簡寫符號。如果我們想要將此相依性替換為一般相依性,那麼我們需要精確選取具有 platform 屬性的相依性。

這可以使用替換規則來完成

consumer/build.gradle.kts
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute(platform(module("com.google.guava:guava:28.2-jre")))
            .using(module("com.google.guava:guava:28.2-jre"))
    }
}
consumer/build.gradle
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute(platform(module('com.google.guava:guava:28.2-jre'))).
            using module('com.google.guava:guava:28.2-jre')
    }
}

沒有 platform 關鍵字的相同規則會嘗試將 一般相依性 替換為一般相依性,這不是您想要的,因此了解替換規則套用於 相依性規格 非常重要:它會將請求的相依性 (substitute XXX) 與替代品 (using YYY) 相符。

您可以在請求的相依性 替代品上設定屬性,而且替換不限於 platform:您實際上可以使用 variant 符號指定相依性屬性的完整組。下列規則與上述規則 完全等效

consumer/build.gradle.kts
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute(variant(module("com.google.guava:guava:28.2-jre")) {
            attributes {
                attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.REGULAR_PLATFORM))
            }
        }).using(module("com.google.guava:guava:28.2-jre"))
    }
}
consumer/build.gradle
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute variant(module('com.google.guava:guava:28.2-jre')) {
            attributes {
                attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.REGULAR_PLATFORM))
            }
        } using module('com.google.guava:guava:28.2-jre')
    }
}

請參閱 替代 DSL API 文件,以取得變數替代 API 的完整參考。

複合建置 中,您必須符合確切要求的相依屬性規則並不適用:使用複合時,Gradle 會自動符合要求的屬性。換句話說,如果您包含其他建置,表示您正在以包含建置中的等效變數替代替代模組的所有變數

以具備功能的相依項替代相依項

類似於 屬性替代,Gradle 讓您可以以具備或不具備功能的相依項替代具備或不具備功能的相依項。

例如,假設您需要以其 測試固定裝置 替代常規相依項。您可以透過使用下列相依項替代規則來達成此目的

build.gradle.kts
configurations.testCompileClasspath {
    resolutionStrategy.dependencySubstitution {
        substitute(module("com.acme:lib:1.0")).using(variant(module("com.acme:lib:1.0")) {
            capabilities {
                requireCapability("com.acme:lib-test-fixtures")
            }
        })
    }
}
build.gradle
configurations.testCompileClasspath {
    resolutionStrategy.dependencySubstitution {
        substitute(module('com.acme:lib:1.0'))
            .using variant(module('com.acme:lib:1.0')) {
            capabilities {
                requireCapability('com.acme:lib-test-fixtures')
            }
        }
    }
}

在要求的相依項上替代規則中宣告的功能構成相依項符合規格的一部分,因此需要功能的相依項將不會符合。

請參閱 替代 DSL API 文件,以取得變數替代 API 的完整參考。

以分類器或人工製品替代相依項

雖然外部模組通常透過其群組/人工製品/版本座標來處理,但這些模組通常會發布額外的您可能想要用來取代主人工製品的人工製品。這通常是分類人工製品的情況,但您可能也需要選取具有不同檔案類型或副檔名的的人工製品。Gradle 不鼓勵在相依項中使用分類器,並偏好將此類人工製品建模為 模組的額外變數。與分類人工製品相比,使用變數有許多優點,包括但不限於這些人工製品的不同相依項集。

不過,為了協助橋接這兩個模型,Gradle 提供變更或移除替代規則中分類器的方法。

consumer/build.gradle.kts
dependencies {
    implementation("com.google.guava:guava:28.2-jre")
    implementation("co.paralleluniverse:quasar-core:0.8.0")
    implementation(project(":lib"))
}
consumer/build.gradle
dependencies {
    implementation 'com.google.guava:guava:28.2-jre'
    implementation 'co.paralleluniverse:quasar-core:0.8.0'
    implementation project(':lib')
}

在上述範例中,quasar 上的第一層相依項讓我們認為 Gradle 會解析 quasar-core-0.8.0.jar,但事實並非如此:建置會失敗並顯示此訊息

Execution failed for task ':resolve'.
> Could not resolve all files for configuration ':runtimeClasspath'.
   > Could not find quasar-core-0.8.0-jdk8.jar (co.paralleluniverse:quasar-core:0.8.0).
     Searched in the following locations:
         https://repo1.maven.org/maven2/co/paralleluniverse/quasar-core/0.8.0/quasar-core-0.8.0-jdk8.jar

這是因為另一個專案 lib 上有一個相依項,而 lib 本身又相依於不同版本的 quasar-core

lib/build.gradle.kts
dependencies {
    implementation("co.paralleluniverse:quasar-core:0.7.10:jdk8")
}
lib/build.gradle
dependencies {
    implementation "co.paralleluniverse:quasar-core:0.7.10:jdk8"
}

發生的事情是 Gradle 會在 quasar-core 0.8.0 和 quasar-core 0.7.10 之間執行衝突解析。由於 0.8.0 較高,我們選取這個版本,但 lib 中的相依項有一個分類器 jdk8,而這個分類器不再存在於版本 0.8.0 中。

若要修正此問題,您可以要求 Gradle 在沒有分類器的情況下解析兩個相依項

consumer/build.gradle.kts
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute(module("co.paralleluniverse:quasar-core"))
            .using(module("co.paralleluniverse:quasar-core:0.8.0"))
            .withoutClassifier()
    }
}
consumer/build.gradle
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute module('co.paralleluniverse:quasar-core') using module('co.paralleluniverse:quasar-core:0.8.0') withoutClassifier()
    }
}

此規則有效地以不帶分類項的相依項取代圖形中找到的任何對 quasar-core 的相依項。

或者,可以選擇帶有特定分類項的相依項,或針對更具體的使用案例,以非常具體的成品 (類型、副檔名和分類項) 替換。

如需更多資訊,請參閱下列 API 文件

停用傳遞式解析

預設情況下,Gradle 會解析相依項元資料中指定的所有傳遞式相依項。有時此行為可能不理想,例如如果元資料不正確或定義了大量的傳遞式相依項圖形。您可以透過將 ModuleDependency.setTransitive(boolean) 設定為 false,來指示 Gradle 為相依項停用傳遞式相依項管理。因此,只會為宣告的相依項解析主要成品。

build.gradle.kts
dependencies {
    implementation("com.google.guava:guava:23.0") {
        isTransitive = false
    }
}
build.gradle
dependencies {
    implementation('com.google.guava:guava:23.0') {
        transitive = false
    }
}
停用傳遞式相依項解析可能會要求您在建置指令碼中宣告必要的執行時期相依項,否則這些相依項會自動解析。如果不這麼做,可能會導致執行時期類別路徑問題。

專案可以決定完全停用傳遞式相依項解析。您不希望依賴發布到所使用儲存庫的元資料,或者您希望完全控制圖形中的相依項。如需更多資訊,請參閱 Configuration.setTransitive(boolean)

build.gradle.kts
configurations.all {
    isTransitive = false
}

dependencies {
    implementation("com.google.guava:guava:23.0")
}
build.gradle
configurations.all {
    transitive = false
}

dependencies {
    implementation 'com.google.guava:guava:23.0'
}

在解析之前變更組態相依項

有時,外掛可能希望在解析組態之前修改其相依項。withDependencies 方法允許以程式化方式新增、移除或修改相依項。

build.gradle.kts
configurations {
    create("implementation") {
        withDependencies {
            val dep = this.find { it.name == "to-modify" } as ExternalModuleDependency
            dep.version {
                strictly("1.2")
            }
        }
    }
}
build.gradle
configurations {
    implementation {
        withDependencies { DependencySet dependencies ->
            ExternalModuleDependency dep = dependencies.find { it.name == 'to-modify' } as ExternalModuleDependency
            dep.version {
                strictly "1.2"
            }
        }
    }
}

設定預設組態相依性

如果沒有明確設定組態的相依性,則可以設定組態的預設相依性。此功能的主要用例是開發使用使用者可能覆寫的版本化工具的外掛程式。透過指定預設相依性,外掛程式只能在使用者未指定要使用的特定版本時,使用該工具的預設版本。

build.gradle.kts
configurations {
    create("pluginTool") {
        defaultDependencies {
            add(project.dependencies.create("org.gradle:my-util:1.0"))
        }
    }
}
build.gradle
configurations {
    pluginTool {
        defaultDependencies { dependencies ->
            dependencies.add(project.dependencies.create("org.gradle:my-util:1.0"))
        }
    }
}

完全排除組態中的相依性

類似於 在相依性宣告中排除相依性,您可以使用 Configuration.exclude(java.util.Map) 完全排除特定組態的傳遞相依性。這會自動排除組態中宣告的所有相依性的傳遞相依性。

build.gradle.kts
configurations {
    "implementation" {
        exclude(group = "commons-collections", module = "commons-collections")
    }
}

dependencies {
    implementation("commons-beanutils:commons-beanutils:1.9.4")
    implementation("com.opencsv:opencsv:4.6")
}
build.gradle
configurations {
    implementation {
        exclude group: 'commons-collections', module: 'commons-collections'
    }
}

dependencies {
    implementation 'commons-beanutils:commons-beanutils:1.9.4'
    implementation 'com.opencsv:opencsv:4.6'
}

將相依性與儲存庫配對

Gradle 公開一個 API 來宣告儲存庫可能包含或不包含的內容。此功能提供細緻的控制,以決定哪個儲存庫提供哪些人工製品,這可以是控制相依性來源的方法之一。

前往 儲存庫內容篩選區段,以進一步了解此功能。

啟用 Ivy 動態解析模式

Gradle 的 Ivy 儲存庫實作支援等同於 Ivy 的動態解析模式。通常,Gradle 會使用 ivy.xml 檔案中包含的每個相依性定義的 rev 屬性。在動態解析模式中,Gradle 會優先使用給定相依性定義的 revConstraint 屬性,而不是 rev 屬性。如果沒有 revConstraint 屬性,則會使用 rev 屬性。

若要啟用動態解析模式,您需要在儲存庫定義中設定適當的選項。以下顯示幾個範例。請注意,動態解析模式僅適用於 Gradle 的 Ivy 儲存庫。它不適用於 Maven 儲存庫,或自訂 Ivy DependencyResolver 實作。

build.gradle.kts
// Can enable dynamic resolve mode when you define the repository
repositories {
    ivy {
        url = uri("http://repo.mycompany.com/repo")
        resolve.isDynamicMode = true
    }
}

// Can use a rule instead to enable (or disable) dynamic resolve mode for all repositories
repositories.withType<IvyArtifactRepository> {
    resolve.isDynamicMode = true
}
build.gradle
// Can enable dynamic resolve mode when you define the repository
repositories {
    ivy {
        url "http://repo.mycompany.com/repo"
        resolve.dynamicMode = true
    }
}

// Can use a rule instead to enable (or disable) dynamic resolve mode for all repositories
repositories.withType(IvyArtifactRepository) {
    resolve.dynamicMode = true
}