在依賴圖中,意外包含同一 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")
}
}
在這種情況下,HikariCP
和 commons-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
功能
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)
}
}
}
}
}
}
透過這個規則,如果依賴圖中同時存在 asm:asm
( < = 3.3.1
) 和 org.ow2.asm:asm
(4.0+
),建置將會失敗。
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
作為首選選項
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
日誌記錄器實作可能就足夠了,而在生產環境中,可能更適合使用像 logback
這樣更穩健的解決方案。
解析只能針對在依賴圖中找到的模組進行。select
方法僅接受來自當前候選者集合的模組。如果所需的模組不是衝突的一部分,您可以選擇不解決該特定衝突,實際上使其保持未解決狀態。圖中的另一個衝突可能具有您想要選擇的模組。
如果沒有為給定功能上的所有衝突提供解析,則建置將會失敗,因為為解析選擇的模組未在圖中找到。此外,呼叫 select(null)
將導致錯誤,應避免使用。
有關更多資訊,請參閱功能解析 API。