元件功能簡介

依賴圖通常會意外包含多個相同 API 的實作。這在記錄架構中特別常見,其中有多個繫結可用,且一個函式庫在另一個傳遞依賴項選擇另一個繫結時選擇一個繫結。由於這些實作位於不同的 GAV 座標,因此建置工具通常無法找出這些函式庫之間存在衝突。為了解決此問題,Gradle 提供了功能的概念。

在單一依賴圖中找到兩個元件提供相同功能是非法的。直覺上,這表示如果 Gradle 在類別路徑上找到兩個提供相同功能的元件,它將會傳回錯誤,指出哪些模組有衝突。在我們的範例中,這表示記錄架構的不同繫結提供相同的功能。

功能座標

功能(group, module, version) 三元組定義。每個元件定義一個隱含功能,對應其 GAV 座標(群組、成品、版本)。例如,org.apache.commons:commons-lang3:3.8 模組具有隱含功能,群組為 org.apache.commons、名稱為 commons-lang3、版本為 3.8。了解功能是版本化的非常重要。

宣告元件功能

預設情況下,如果依賴圖中的兩個元件提供相同功能,Gradle 會傳回錯誤。由於目前大多數模組都是在沒有 Gradle 模組中繼資料的情況下發布,因此 Gradle 並非總是自動偵測到功能。然而,使用規則來宣告元件功能很有趣,以便在建置期間而非執行期間盡快發現衝突。

典型的範例是,當元件在新的版本中重新定位到不同的座標時。例如,ASM 函式庫在版本 3.3.1 之前位於 asm:asm 座標,然後自 4.0 起變更為 org.ow2.asm:asm。在類別路徑中同時擁有 ASM <= 3.3.1 和 4.0+ 是不合法的,因為它們提供相同的功能,只是元件已重新定位。由於每個元件都有與其 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)
                    }
                }
            }
        }
    }
}

現在,只要在同一個依賴圖中找到這兩個元件,建置就會失敗

在此階段,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。

在這種情況下,我們可以透過明確選擇 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 可能就夠了,但對於製作,slf4-over-log4j 可能會更好。

解析只能針對圖表中找到的模組進行。

select 方法只接受在目前候選項中找到的模組。如果您要選擇的模組並非衝突的一部分,您可以選擇不執行選擇,實際上並未解決衝突。圖表中可能存在針對相同功能的另一個衝突,且將包含您要選擇的模組。

如果未針對特定功能的所有衝突提供解析,則會因為選擇要解析的模組根本不是圖表的一部分而導致建置失敗。

此外,select(null) 會導致錯誤,因此應避免使用。

如需更多資訊,請查看功能解析 API