本章說明 Gradle 內部的依賴性解析運作方式。在學習如何宣告 依賴性指定專案應使用的版本 後,下一步是了解如何在依賴性解析過程中合併這些宣告。

依賴性解析發生在兩個主要階段,重複執行直到建構完整的依賴性圖形

  1. 衝突解決:

    • 當引入新的依賴性時,Gradle 會解析任何衝突,以判斷應將哪個版本新增至圖形。

    • Gradle 將應用其衝突解決規則(例如,「最新版本獲勝」或自訂版本解析策略),以判斷要使用的版本。

  2. 依賴性中繼資料擷取:

    • 一旦特定依賴性(具有版本的模組)包含在圖形中,Gradle 就會擷取其中繼資料(例如 POM、Ivy 或模組中繼資料)以

      • 將其自身的依賴性(傳遞性)新增至圖形。

      • 了解該依賴性的可用變體。

此過程會持續進行,直到解析整個依賴性樹狀結構。

dependency resolution model

階段 1. 衝突解決

執行依賴性解析時,Gradle 會處理兩種型別的衝突

  1. 版本衝突:當多個依賴性請求相同的依賴性,但版本不同時發生。Gradle 必須選擇要包含在圖形中的版本。

  2. 實作 / 功能衝突:當依賴性圖形包含提供相同功能或功能的不同模組時發生。Gradle 會透過選擇一個模組來解決這些衝突,以避免重複實作。

依賴性解析過程高度可自訂,許多 API 可以影響此過程。

A. 版本衝突

當兩個元件發生版本衝突時

  • 依賴相同的模組,例如 com.google.guava:guava

  • 但在不同的版本上,例如 20.025.1-android

    • 我們的專案直接依賴 com.google.guava:guava:20.0

    • 我們的專案也依賴 com.google.inject:guice:4.2.2,而後者又依賴 com.google.guava:guava:25.1-android

Gradle 必須透過選擇一個版本來解決此衝突,以包含在依賴性圖形中。

Gradle 會考量整個依賴性圖形中所有請求的版本,並預設選擇最高版本。詳細的版本排序說明於 版本排序

Gradle 也支援 豐富版本宣告 的概念,這表示構成「最高」版本的因素取決於版本的宣告方式

  • 不含範圍:將選擇最高的非拒絕版本。

    • 如果宣告的 strictly 版本低於最高版本,解析將會失敗。

  • 包含範圍:

    • 如果非範圍版本符合範圍或高於上限,則會選取該版本。

    • 如果僅存在範圍,則選擇取決於這些範圍的交集

      • 如果範圍重疊,則會選取交集中最高的現有版本。

      • 如果不存在明確的交集,則會選取最大範圍中最高的版本。如果最高範圍中沒有任何版本存在,則解析會失敗。

    • 如果宣告的 strictly 版本低於最高版本,解析將會失敗。

對於版本範圍,Gradle 需要執行中繼資料中繼查詢,以判斷有哪些變體可用,如 階段 2. 依賴性中繼資料擷取 中所述。

具有限定詞的版本

術語「限定詞」指的是版本字串中在非點分隔符號(例如連字號或底線)之後的部分。

例如

原始版本 基礎版本 限定詞

1.2.3

1.2.3

<無>

1.2-3

1.2

3

1_alpha

1

alpha

abc

abc

<無>

1.2b3

1.2

b3

abc.1+3

abc.1

3

b1-2-3.3

b

1-2-3.3

如您所見,分隔符號可以是 .-_+ 字元中的任何一個,加上當版本的數值部分和非數值部分彼此相鄰時的空字串。

預設情況下,Gradle 在解決衝突時優先考慮不含限定詞的版本。

例如,在版本 1.0-beta 中,基礎形式為 1.0,而 beta 是限定詞。不含限定詞的版本被認為更穩定,因此 Gradle 會優先考慮它們。

以下是一些範例以釐清

  • 1.0.0(無限定詞)

  • 1.0.0-beta(限定詞:beta

  • 2.1-rc1(限定詞:rc1

即使限定詞在詞彙上較高,Gradle 通常仍會認為像 1.0.0 這樣的版本高於 1.0.0-beta

在解決版本之間的衝突時,Gradle 會應用以下邏輯

  1. 基礎版本比較: Gradle 首先選擇具有最高基礎版本的版本,忽略任何限定詞。所有其他版本都會被捨棄。

  2. 限定詞處理: 如果仍有多個版本具有相同的基礎版本,Gradle 會選擇一個版本,優先選擇不含限定詞的版本(即發行版本)。如果所有版本都包含限定詞,Gradle 會考量限定詞的順序,優先選擇更穩定的限定詞,例如「release」(發行版),而不是其他限定詞,例如「beta」或「alpha」。

B. 實作 / 功能衝突

Gradle 使用變體功能來定義模組提供的內容。

變體 本質上是依賴性的不同形式,通常基於平台(例如 JVM 或 Android)或配置(例如 compile、runtime)等因素。

功能 是一種表達依賴性互斥變體的方式。

衝突 在以下情況下產生

  • 不相容的變體:當兩個模組嘗試選擇依賴性的不同、不相容的變體時。

  • 相同功能:當多個模組宣告相同的功能時,會產生功能重疊。

如需變體選擇如何運作以及如何實現彈性依賴性管理的更多詳細資訊,請參閱下方的 variant_model.html

階段 2. 依賴性中繼資料擷取

Gradle 需要依賴性圖形中的模組中繼資料,原因有二

  1. 判斷動態依賴性的現有版本:當指定動態版本(例如 1.+latest.release)時,Gradle 必須識別可用的具體版本。

  2. 解析特定版本的模組依賴性:Gradle 會根據指定的版本擷取與模組相關聯的依賴性,確保在建置中包含正確的傳遞依賴性。

A. 判斷動態依賴性的現有版本

當面臨動態版本時,Gradle 必須透過以下步驟識別可用的具體版本

  1. 檢查儲存庫:Gradle 會依照新增順序檢查每個定義的儲存庫。它不會在第一個傳回中繼資料的儲存庫停止,而是會繼續檢查所有可用的儲存庫。

  2. Maven 儲存庫:Gradle 會從 maven-metadata.xml 檔案擷取版本資訊,其中列出可用的版本。

  3. Ivy 儲存庫:Gradle 會訴諸目錄清單來收集可用的版本。

結果是 Gradle 評估並與動態版本比對的候選版本列表。Gradle 快取 此資訊以最佳化未來的解析。此時,版本衝突解決 會繼續。

B. 解析特定版本的模組依賴性

當 Gradle 嘗試解析具有特定版本的必要依賴性時,它會遵循此過程

  1. 儲存庫檢查:Gradle 會依照定義順序檢查每個儲存庫。

    • 它會尋找描述模組的中繼資料檔案(.module.pomivy.xml),或直接尋找 Artifact 檔案。

    • 具有中繼資料檔案(.module.pomivy.xml)的模組優先於僅具有 Artifact 檔案的模組。

    • 在儲存庫中找到中繼資料後,會忽略後續的儲存庫。

  2. 擷取和剖析中繼資料:如果找到中繼資料,則會剖析中繼資料。

    • 如果 POM 檔案具有父系 POM,Gradle 會以遞迴方式解析每個父系模組。

  3. 請求 Artifact:模組的所有 Artifact 都會從提供中繼資料的相同儲存庫中擷取。

  4. 快取:所有資料(包括儲存庫來源和任何潛在的遺失)都儲存在 依賴性快取 中,以供日後使用。

以上重點突顯了整合 Maven 本機儲存庫 的潛在問題。由於 Maven 本機儲存庫充當 Maven 快取,因此有時可能會遺失模組的 Artifact。當 Gradle 從 Maven 本機儲存庫取得模組來源,但 Artifact 遺失時,它會假設這些 Artifact 完全不可用。

停用儲存庫

當 Gradle 無法從儲存庫擷取資訊時,它會停用該儲存庫以進行建置的其餘部分,並使所有依賴性解析失敗。

此行為可確保可重現性。

如果建置在忽略故障儲存庫的情況下繼續進行,則一旦儲存庫恢復連線,後續的建置可能會產生不同的結果。

HTTP 重試

Gradle 會嘗試多次連線到儲存庫,然後再停用它。如果連線失敗,Gradle 會針對可能暫時性的特定錯誤進行重試,並增加重試之間的等待時間。

當儲存庫無法連線時(無論是因為永久性錯誤還是因為已用盡最大重試次數),儲存庫會標記為無法使用。

依賴性樹狀結構

一旦流程完成,就會建立依賴性樹狀結構。

依賴性樹狀結構是專案所需的所有依賴性的階層式表示,包括直接依賴性(明確宣告)和傳遞依賴性(由這些直接依賴性自動提取)。圖形顯示依賴性彼此之間的關係,以及 Gradle 如何解析它們。

對於依賴性 org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1,圖形包含主要依賴性和其所有傳遞依賴性。以下是圖形的外觀

org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1
├── JVM variant
│   ├── org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1
│   │   ├── org.jetbrains.kotlin:kotlin-stdlib:1.8.0
│   │   ├── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
│   │   └── org.jetbrains:annotations:13.0
│   ├── org.jetbrains.kotlin:kotlin-stdlib:1.8.0
│   ├── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
│   └── org.jetbrains:annotations:13.0
│
├── Android variant
│   ├── org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1
│   │   ├── org.jetbrains.kotlin:kotlin-stdlib:1.8.0
│   │   ├── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
│   │   └── org.jetbrains:annotations:13.0
│   ├── org.jetbrains.kotlin:kotlin-stdlib:1.8.0
│   ├── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
│   ├── org.jetbrains:annotations:13.0
│   └── com.android.tools:common-library:1.0.0
│
├── Native variant
│   ├── org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1
│   │   ├── org.jetbrains.kotlin:kotlin-stdlib:1.8.0
│   │   ├── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
│   │   └── org.jetbrains:annotations:13.0
│   ├── org.jetbrains.kotlin:kotlin-stdlib:1.8.0
│   ├── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
│   ├── org.jetbrains:annotations:13.0
│   └── kotlinx.coroutines:kotlinx-coroutines-core-native:1.6.4
│
└── JavaScript variant
    ├── org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1
    │   ├── org.jetbrains.kotlin:kotlin-stdlib-js:1.8.0
    │   └── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
    ├── org.jetbrains.kotlin:kotlin-stdlib-js:1.8.0
    ├── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
    └── kotlinx.coroutines:kotlinx-coroutines-core-js:1.6.4