在多專案建置中,常見的模式是一個專案使用另一個專案的成品。

一般而言,在 Java 生態系統中最簡單的使用形式是,當 A 相依於 B 時,A 將相依於專案 B 產生的 jar

考量與可能的解決方案

宣告跨專案相依性的常見反模式是

dependencies {
   // this is unsafe!
   implementation project(":other").tasks.someOtherJar
}

此發佈模型是不安全的,並可能導致無法重現且難以平行化的建置。

不要直接參考其他專案工作!

您可以在生產者端定義一個配置,作為生產者和消費者之間交換成品的用途。

consumer/build.gradle
dependencies {
    instrumentedClasspath(project(path: ":producer", configuration: 'instrumentedJars'))
}
consumer/build.gradle.kts
dependencies {
    instrumentedClasspath(project(mapOf(
        "path" to ":producer",
        "configuration" to "instrumentedJars")))
}

但是,消費者必須明確告知它相依於哪個配置,而這不建議。如果您計劃發佈具有此相依性的組件,則可能會導致中繼資料損壞。

本節說明如何透過使用變體定義專案之間的「交換」來正確建立跨專案邊界

變體感知成品共用

Gradle 的 變體模型 允許消費者使用屬性指定需求,而生產者也使用屬性提供適當的傳出變體。

例如,像 project(":myLib") 這樣的單一相依性宣告可以根據架構選擇 myLibarm64i386 版本。

為了實現這一點,必須在消費者和生產者配置上都定義屬性。

當配置具有屬性時,它們會參與變體感知解析。這表示每當使用任何相依性宣告(例如 project(":myLib"))時,它們都會成為解析的候選項。

生產者配置上的屬性必須與同一專案提供的其他變體一致。引入不一致或不明確的屬性可能會導致解析失敗。

實際上,您定義的屬性通常取決於生態系統(例如,Java、C++),因為特定於生態系統的外掛程式通常會應用不同的屬性慣例。

考慮 Java 函式庫專案的範例。Java 函式庫通常向消費者公開兩個變體:apiElementsruntimeElements。在本例中,我們新增了第三個變體 instrumentedJars

為了正確配置這個新變體,我們需要了解其用途並設定適當的屬性。以下是生產者的 runtimeElements 配置上的屬性

$ .gradle outgoingVariants --variant runtimeElements

Attributes
    - org.gradle.category            = library
    - org.gradle.dependency.bundling = external
    - org.gradle.jvm.version         = 11
    - org.gradle.libraryelements     = jar
    - org.gradle.usage               = java-runtime

這告訴我們 runtimeElements 配置包含 5 個屬性

  1. org.gradle.category 指出此變體代表函式庫

  2. org.gradle.dependency.bundling 指定相依性是外部 jar(未重新封裝在 jar 內)。

  3. org.gradle.jvm.version 表示支援的最低 Java 版本,即 Java 11。

  4. org.gradle.libraryelements 顯示此變體包含通常在 jar 中找到的所有元素(類別和資源)。

  5. org.gradle.usage 將變體定義為 Java 執行階段,適用於編譯和執行階段。

為了確保在執行測試時使用 instrumentedJars 變體來代替 runtimeElements,我們必須將類似的屬性附加到這個新變體。

此配置的關鍵屬性是 org.gradle.libraryelements,因為它描述了變體包含的內容。我們可以相應地設定 instrumentedJars 變體

producer/build.gradle
configurations {
    instrumentedJars {
        canBeConsumed = true
        canBeResolved = false
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
            attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
            attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
            attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, JavaVersion.current().majorVersion.toInteger())
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'instrumented-jar'))
        }
    }
}
producer/build.gradle.kts
val instrumentedJars by configurations.creating {
    isCanBeConsumed = true
    isCanBeResolved = false
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
        attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
        attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
        attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, JavaVersion.current().majorVersion.toInt())
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("instrumented-jar"))
    }
}

這確保 instrumentedJars 變體被正確識別為包含類似於 jar 的元素,使其能夠被適當地選擇。

選擇正確的屬性是此過程中最具挑戰性的部分,因為它們定義了變體的語意。在引入新屬性之前,請務必考慮現有屬性是否已傳達所需的語意。如果沒有合適的屬性存在,您可以建立一個新的屬性。但是,請務必謹慎 — 新增屬性可能會在變體選擇期間引入歧義。在許多情況下,新增屬性需要將其一致地應用於所有現有變體。

我們為執行階段引入了一個新的變體,它提供檢測過的類別而不是普通的類別。因此,消費者現在面臨兩個執行階段變體之間的選擇

  1. runtimeElements - java-library 外掛程式提供的預設執行階段變體。

  2. instrumentedJars - 我們新增的自訂變體。

如果我們希望將檢測過的類別包含在測試執行階段類別路徑中,我們現在可以將消費者端的相依性宣告為常規專案相依性

consumer/build.gradle
dependencies {
    testImplementation 'junit:junit:4.13'
    testImplementation project(':producer')
}
consumer/build.gradle.kts
dependencies {
    testImplementation("junit:junit:4.13")
    testImplementation(project(":producer"))
}

如果我們就此停止,Gradle 仍然會解析 runtimeElements 變體,而不是 instrumentedJars 變體。

發生這種情況是因為 testRuntimeClasspath 配置請求將 libraryelements 屬性設定為 jar 的變體,而我們的 instrumented-jars 值不匹配。

為了修正此問題,我們需要更新請求的屬性,以專門針對檢測過的 jar

consumer/build.gradle
configurations {
    testRuntimeClasspath {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'instrumented-jar'))
        }
    }
}
consumer/build.gradle.kts
configurations {
    testRuntimeClasspath {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, "instrumented-jar"))
        }
    }
}

我們可以查看消費者端的另一個報告,以準確查看將請求每個相依性的哪些屬性

$ .gradle resolvableConfigurations --configuration testRuntimeClasspath

Attributes
    - org.gradle.category            = library
    - org.gradle.dependency.bundling = external
    - org.gradle.jvm.version         = 11
    - org.gradle.libraryelements     = instrumented-jar
    - org.gradle.usage               = java-runtime

resolvableConfigurations 報告是我們先前執行的 outgoingVariants 報告的補充。

透過分別在關係的消費者端和生產者端執行這兩個報告,您可以確切地看到在相依性解析期間涉及匹配的哪些屬性,並更好地預測配置解析時的結果。

此時,我們指定測試執行階段類別路徑應解析具有檢測過的類別的變體。

但是,有一個問題:某些相依性(例如 JUnit)不提供檢測過的類別。如果我們就此停止,Gradle 將會失敗,並指出不存在 JUnit 的相容變體。

發生這種情況是因為我們沒有告訴 Gradle,當檢測過的變體不可用時,可以退回到常規 jar。為了解決這個問題,我們需要定義一個相容性規則

consumer/build.gradle
abstract class InstrumentedJarsRule implements AttributeCompatibilityRule<LibraryElements> {

    @Override
    void execute(CompatibilityCheckDetails<LibraryElements> details) {
        if (details.consumerValue.name == 'instrumented-jar' && details.producerValue.name == 'jar') {
            details.compatible()
        }
    }
}
consumer/build.gradle.kts
abstract class InstrumentedJarsRule: AttributeCompatibilityRule<LibraryElements> {
    override fun execute(details: CompatibilityCheckDetails<LibraryElements>) = details.run {
        if (consumerValue?.name == "instrumented-jar" && producerValue?.name == "jar") {
            compatible()
        }
    }
}

然後,我們在屬性架構上宣告此規則

consumer/build.gradle
dependencies {
    attributesSchema {
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
            compatibilityRules.add(InstrumentedJarsRule)
        }
    }
}
consumer/build.gradle.kts
dependencies {
    attributesSchema {
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
            compatibilityRules.add(InstrumentedJarsRule::class.java)
        }
    }
}

就這樣!現在我們有了

  • 新增了一個提供檢測過的 jar 的變體。

  • 指定此變體是執行階段的替代品。

  • 定義消費者僅在測試執行階段需要此變體。

Gradle 提供了一個強大的機制,用於根據偏好和相容性選擇正確的變體。如需更多詳細資訊,請查看文件中的 變體感知外掛程式章節

透過將值新增到現有屬性或定義新屬性,我們正在擴展模型。這表示所有消費者都必須了解這個擴展的模型。

對於本機消費者,這通常不是問題,因為所有專案都共用相同的架構。但是,如果您需要將這個新變體發佈到外部儲存庫,外部消費者也必須在其建置中新增相同的規則才能使其運作。

對於生態系統外掛程式(例如,Kotlin 外掛程式)而言,這通常不是問題,因為不套用外掛程式就無法使用。但是,如果您新增自訂值或屬性,則會變得有問題。

因此,如果僅供內部使用,請避免發佈自訂變體