元件功能簡介
依賴圖通常會意外包含多個相同 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
功能
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)
}
}
}
}
}
@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 的版本:一個「舊」模組和一個「重新定位」的模組。現在,我們可以透過自動選擇具有最高功能版本之元件來解決衝突
configurations.all {
resolutionStrategy.capabilitiesResolution.withCapability("org.ow2.asm:asm") {
selectHighestVersion()
}
}
configurations.all {
resolutionStrategy.capabilitiesResolution.withCapability('org.ow2.asm:asm') {
selectHighestVersion()
}
}
不過,透過選擇最高功能版本衝突解決方案來修正並不總是合適。例如,對於記錄架構,我們使用哪個版本的記錄架構並不重要,我們應該總是選擇 Slf4j。
在這種情況下,我們可以透過明確選擇 slf4j 作為獲勝者來修正它
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")
}
}
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。