在依賴圖中,意外包含同一 API 的多個實作是很常見的,尤其是在像日誌框架這樣的函式庫中,不同的綁定會被各種傳遞依賴關係選中。

由於這些實作通常位於不同的組、成品和版本 (GAV) 座標,建置工具通常無法偵測到衝突。

為了應對這個問題,Gradle 引入了功能的概念。

理解功能

功能本質上是一種宣告不同組件(依賴關係)提供相同功能的方式。

Gradle 在單個依賴圖中包含多個提供相同功能的組件是不允許的。如果 Gradle 偵測到兩個組件提供相同的功能(例如,日誌框架的不同綁定),它將使建置失敗並出現錯誤,指出衝突的模組。這確保了衝突的實作得到解決,避免了類路徑上的問題。

例如,假設您依賴於兩個不同的函式庫來進行資料庫連線池

dependencies {
    implementation("com.zaxxer:HikariCP:4.0.3")  // A popular connection pool
    implementation("org.apache.commons:commons-dbcp2:2.8.0")  // Another connection pool
}

configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("database:connection-pool") {
        select("com.zaxxer:HikariCP")
    }
}

在這種情況下,HikariCPcommons-dbcp2 都提供相同的功能(連線池)。如果兩者都在類路徑上,Gradle 將會失敗。

由於應該只使用一個,Gradle 的解析策略允許您選擇 HikariCP,從而解決衝突。

理解功能座標

功能(組, 模組, 版本) 三元組識別。

每個組件都根據其 GAV 座標定義一個隱含功能:組、成品和版本。

例如,org.apache.commons:commons-lang3:3.8 模組具有一個隱含功能,其組為 org.apache.commons,名稱為 commons-lang3,版本為 3.8

dependencies {
    implementation("org.apache.commons:commons-lang3:3.8")
}

重要的是要注意,功能是版本化的

宣告組件功能

為了儘早偵測到衝突,透過規則宣告組件功能很有用,這樣可以在建置期間而不是在執行時捕獲衝突。

一個常見的場景是組件在新版本中重新定位到不同的座標。

例如,ASM 函式庫在 3.3.1 版本之前以 asm:asm 發布,然後從 4.0 版本開始重新定位到 org.ow2.asm:asm。在類路徑上同時包含這兩個版本是不允許的,因為它們在不同的座標下提供相同的功能。

由於每個組件都有一個基於其 GAV 座標的隱含功能,我們可以透過使用一個規則來解決這個衝突,該規則宣告 asm:asm 模組提供 org.ow2.asm:asm 功能

build.gradle.kts
class AsmCapability : ComponentMetadataRule {
    override
    fun execute(context: ComponentMetadataContext) = context.details.run {
        if (id.group == "asm" && id.name == "asm") {
            allVariants {
                withCapabilities {
                    // Declare that ASM provides the org.ow2.asm:asm capability, but with an older version
                    addCapability("org.ow2.asm", "asm", id.version)
                }
            }
        }
    }
}
build.gradle
@CompileStatic
class AsmCapability implements ComponentMetadataRule {
    void execute(ComponentMetadataContext context) {
        context.details.with {
            if (id.group == "asm" && id.name == "asm") {
                allVariants {
                    it.withCapabilities {
                        // Declare that ASM provides the org.ow2.asm:asm capability, but with an older version
                        it.addCapability("org.ow2.asm", "asm", id.version)
                    }
                }
            }
        }
    }
}

透過這個規則,如果依賴圖中同時存在 asm:asm ( < = 3.3.1) 和 org.ow2.asm:asm (4.0+),建置將會失敗。

Gradle 不會自動解決衝突,但這有助於您意識到問題的存在。建議將這些規則打包到外掛中以在建置中使用,讓使用者決定使用哪個版本或修復類路徑衝突。

在候選者之間選擇

在某些時候,依賴圖將包含不相容的模組,或互斥的模組。

例如,您可能有不同的日誌記錄器實作,您需要選擇一個綁定。功能有助於理解衝突,然後 Gradle 為您提供解決衝突的工具。

在不同的功能候選者之間選擇

在上面的重新定位範例中,Gradle 能夠告訴您,您在類路徑上有同一個 API 的兩個版本:「舊」模組和「重新定位」的模組。我們可以透過自動選擇具有最高功能版本的組件來解決衝突

build.gradle.kts
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("org.ow2.asm:asm") {
        selectHighestVersion()
    }
}
build.gradle
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability('org.ow2.asm:asm') {
        selectHighestVersion()
    }
}

但是,選擇最高功能版本衝突解決方案並不總是合適的。

例如,對於日誌框架,我們使用哪個版本的日誌框架並不重要。在這種情況下,我們明確選擇 slf4j 作為首選選項

build.gradle.kts
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("log4j:log4j") {
        val toBeSelected = candidates.firstOrNull { it.id.let { id -> id is ModuleComponentIdentifier && id.module == "log4j-over-slf4j" } }
        if (toBeSelected != null) {
            select(toBeSelected)
        }
        because("use slf4j in place of log4j")
    }
}
build.gradle
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("log4j:log4j") {
        def toBeSelected = candidates.find { it.id instanceof ModuleComponentIdentifier && it.id.module == 'log4j-over-slf4j' }
        if (toBeSelected != null) {
            select(toBeSelected)
        }
        because 'use slf4j in place of log4j'
    }
}

如果您在類路徑上有多個 slf4j 綁定,這種方法也適用;綁定基本上是不同的日誌記錄器實作,您只需要一個。但是,選擇的實作可能取決於正在解析的配置。

例如,在測試環境中,輕量級的 slf4j-simple 日誌記錄器實作可能就足夠了,而在生產環境中,可能更適合使用像 logback 這樣更穩健的解決方案。

解析只能針對在依賴圖中找到的模組進行。select 方法僅接受來自當前候選者集合的模組。如果所需的模組不是衝突的一部分,您可以選擇不解決該特定衝突,實際上使其保持未解決狀態。圖中的另一個衝突可能具有您想要選擇的模組。

如果沒有為給定功能上的所有衝突提供解析,則建置將會失敗,因為為解析選擇的模組未在圖中找到。此外,呼叫 select(null) 將導致錯誤,應避免使用。

有關更多資訊,請參閱功能解析 API