覆寫傳遞相依性版本

Gradle 會透過選取相依性圖形中找到的最新版本,來解決所有相依性版本衝突。有些專案可能需要偏離預設行為,並強制使用較早版本的相依性,例如,如果專案的原始碼相依於較舊的相依性 API,而不是某些外部函式庫。

強制使用相依性版本需要經過深思熟慮的決定。變更傳遞相依性的版本可能會導致執行時期錯誤,如果外部函式庫在沒有它們的情況下無法正常運作。可以考慮將原始碼升級為使用較新版本的函式庫,作為替代方法。

一般來說,強制相依性是為了降級相依性。降級可能有不同的使用案例

  • 在最新版本中發現錯誤

  • 您的程式碼相依於較低版本,且不相容於二進位

  • 您的程式碼不依賴於需要較高版本相依性的程式碼路徑

在所有情況下,這最能表達出你的程式碼嚴格依賴於某個傳遞依賴項的版本。使用嚴格版本,你將有效地依賴於你宣告的版本,即使傳遞依賴項另有說明。

嚴格依賴項在某種程度上類似於 Maven 的最先最近策略,但有一些細微的差異

假設一個專案使用HttpClient 函式庫來執行 HTTP 呼叫。HttpClient 會拉進Commons Codec作為傳遞依賴項,版本為 1.10。然而,專案的生產原始碼需要 Commons Codec 1.9 的 API,而這在 1.10 中已經不可用了。可以在建置指令碼中宣告依賴項版本為嚴格版本,以強制執行它

build.gradle.kts
dependencies {
    implementation("org.apache.httpcomponents:httpclient:4.5.4")
    implementation("commons-codec:commons-codec") {
        version {
            strictly("1.9")
        }
    }
}
build.gradle
dependencies {
    implementation 'org.apache.httpcomponents:httpclient:4.5.4'
    implementation('commons-codec:commons-codec') {
        version {
            strictly '1.9'
        }
    }
}

使用嚴格版本的後果

使用嚴格版本必須仔細考量,特別是函式庫作者。作為製作者,嚴格版本將有效地表現得像強制:版本宣告優先於傳遞依賴項圖中找到的任何內容。特別是,嚴格版本會覆寫傳遞找到的同一個模組上的任何其他嚴格版本

然而,對於使用者來說,嚴格版本在圖解析期間仍被視為全域性的,而且可能會觸發錯誤,如果使用者不同意。

例如,想像你的專案B嚴格依賴於C:1.0。現在,使用者A依賴於BC:1.1

然後這會觸發解析錯誤,因為A表示它需要C:1.1,但B在其子圖中)嚴格需要1.0。這表示如果你在嚴格約束中選擇單一版本,那麼該版本不能再升級,除非使用者也在同一個模組上設定嚴格版本約束。

在上面的範例中,A必須表示它嚴格依賴於 1.1

因此,一個良好的做法是,如果你使用嚴格版本,你應該用範圍和此範圍內的優先版本來表達它們。例如,B可能會說,它嚴格依賴[1.0, 2.0[範圍,但優先1.0,而不是嚴格 1.0。然後,如果使用者選擇 1.1(或範圍內的任何其他版本),建置將不再失敗(約束已解析)。

強制依賴項與嚴格依賴項

如果專案需要在組態層級強制使用特定版本的相依性,可以呼叫方法 ResolutionStrategy.force(java.lang.Object[]) 來達成。

build.gradle.kts
configurations {
    "compileClasspath" {
        resolutionStrategy.force("commons-codec:commons-codec:1.9")
    }
}

dependencies {
    implementation("org.apache.httpcomponents:httpclient:4.5.4")
}
build.gradle
configurations {
    compileClasspath {
        resolutionStrategy.force 'commons-codec:commons-codec:1.9'
    }
}

dependencies {
    implementation 'org.apache.httpcomponents:httpclient:4.5.4'
}

排除傳遞相依性

前一段落說明如何強制使用特定版本的傳遞相依性,而本段落會說明如何使用 excludes 完全移除傳遞相依性。

與強制使用相依性版本類似,完全排除相依性需要經過深思熟慮。如果外部函式庫在沒有傳遞相依性的情況下無法正常運作,排除傳遞相依性可能會導致執行時期錯誤。如果您使用 excludes,請務必透過足夠的測試範圍,確保您沒有使用任何需要排除相依性的程式碼路徑。

傳遞相依性可以在宣告相依性的層級中排除。排除事項會以 key/value 成對的方式,透過屬性 group 和/或 module 說明,如下面的範例所示。如需更多資訊,請參閱 ModuleDependency.exclude(java.util.Map)

build.gradle.kts
dependencies {
    implementation("commons-beanutils:commons-beanutils:1.9.4") {
        exclude(group = "commons-collections", module = "commons-collections")
    }
}
build.gradle
dependencies {
    implementation('commons-beanutils:commons-beanutils:1.9.4') {
        exclude group: 'commons-collections', module: 'commons-collections'
    }
}

在此範例中,我們新增對 commons-beanutils 的相依性,但排除傳遞相依性 commons-collections。在我們下方顯示的程式碼中,我們只使用 beanutils 函式庫中的其中一個方法,也就是 PropertyUtils.setSimpleProperty()。透過測試範圍驗證,使用此方法來呼叫現有的 setter 不需要任何 commons-collections 的功能。

src/main/java/Main.java
import org.apache.commons.beanutils.PropertyUtils;

public class Main {
    public static void main(String[] args) throws Exception {
        Object person = new Person();
        PropertyUtils.setSimpleProperty(person, "name", "Bart Simpson");
        PropertyUtils.setSimpleProperty(person, "age", 38);
    }
}

實際上,我們表示我們只使用函式庫的 子集,不需要 commons-collection 函式庫。這可以視為隱含地定義一個 commons-beanutils 本身並未明確宣告的 功能變異。不過,這麼做會增加中斷未測試程式碼路徑的風險。

例如,這裡我們使用 setSimpleProperty() 方法來修改 Person 類別中由設定器定義的屬性,這運作良好。如果我們嘗試設定類別中不存在的屬性,我們應該會收到類似 Person 類別中未知屬性 的錯誤。然而,由於錯誤處理路徑使用 commons-collections 中的類別,我們現在收到的錯誤是 NoClassDefFoundError: org/apache/commons/collections/FastHashMap。因此,如果我們的程式碼更動態,而我們忘記充分涵蓋錯誤案例,我們的函式庫使用者可能會遇到意外錯誤。

這只是一個說明潛在陷阱的範例。在實務上,較大的函式庫或架構可能會帶來大量的依賴關係。如果這些函式庫無法分別宣告功能,而且只能以「全有或全無」的方式使用,排除可能是將函式庫縮減到實際所需功能集的有效方法。

好處是,Gradle 的排除處理與 Maven 相反,會考量整個依賴關係圖。因此,如果有多個依賴關係依賴於某個函式庫,只有當所有依賴關係都同意時,才會執行排除。例如,如果我們將 opencsv 新增為我們上述專案的另一個依賴關係,它也依賴於 commons-beanutils,則 commons-collection 不再被排除,因為 opencsv 本身沒有排除它。

build.gradle.kts
dependencies {
    implementation("commons-beanutils:commons-beanutils:1.9.4") {
        exclude(group = "commons-collections", module = "commons-collections")
    }
    implementation("com.opencsv:opencsv:4.6") // depends on 'commons-beanutils' without exclude and brings back 'commons-collections'
}
build.gradle
dependencies {
    implementation('commons-beanutils:commons-beanutils:1.9.4') {
        exclude group: 'commons-collections', module: 'commons-collections'
    }
    implementation 'com.opencsv:opencsv:4.6' // depends on 'commons-beanutils' without exclude and brings back 'commons-collections'
}

如果我們仍然想要排除 commons-collections,因為我們結合使用 commons-beanutilsopencsv 並不需要它,我們需要從 opencsv 的傳遞依賴關係中排除它。

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

在過去,排除也被用作一種治標不治本的方法,來修復某些依賴關係管理系統不支援的其他問題。然而,Gradle 提供了各種功能,可能更適合解決特定使用案例。你可以考慮查看以下功能

  • 更新降級依賴關係版本:如果依賴關係的版本衝突,通常最好透過依賴關係約束調整版本,而不是嘗試排除具有不需要版本的依賴關係。

  • 元件資料規則:如果函式庫的資料明顯錯誤,例如包含在編譯時從不需要的編譯時相依性,一個可能的解決方案是在元件資料規則中移除相依性。透過此方式,您可以告訴 Gradle 兩個模組之間的相依性從不需要,也就是說資料錯誤,因此絕不應該考慮。如果您正在開發函式庫,您必須知道此資訊不會公開,因此有時排除會是更好的替代方案。

  • 解決相互排斥的相依性衝突:您經常看到另一個由排除解決的情況是,兩個相依性無法一起使用,因為它們表示同一事物的兩個實作(相同的功能)。一些熱門範例是衝突的記錄 API 實作(例如 log4jlog4j-over-slf4j)或在不同版本中具有不同座標的模組(例如 com.google.collectionsguava)。在這些情況下,如果 Gradle 不知道此資訊,建議透過元件資料規則新增遺失的功能資訊,如宣告元件功能區段所述。即使您正在開發函式庫,而且您的使用者必須再次處理解決衝突,讓最終函式庫使用者做出決定通常是正確的解決方案。也就是說,您作為函式庫作者不應該決定您的使用者最終使用哪個記錄實作。