在多專案建置中,常見的模式是,一個專案會使用另一個專案的人工製品。一般來說,Java 生態系統中最簡單的消費形式是,當 A 依賴 B 時,A 會依賴專案 B 所產生的 jar。正如本章節先前所述,這會透過 A 依賴 B變異來建模,其中變異會根據 A 的需求來選擇。對於編譯,我們需要 B 的 API 依賴項,由 apiElements 變異提供。對於執行時期,我們需要 B 的執行時期依賴項,由 runtimeElements 變異提供。

但是,如果你需要一個不同於主要人工製品的人工製品怎麼辦?例如,Gradle 提供內建支援,可以依賴另一個專案的測試固定裝置,但有時你需要依賴的人工製品並未公開為變異。

為了在專案之間安全共用並允許最大效能(平行處理),此類人工製品必須透過傳出設定檔公開。

不要直接參照其他專案工作

宣告跨專案依賴項的常見反模式是

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

這種發布模式不安全,可能會導致無法重製且難以平行處理的建置。本節說明如何透過使用變異定義專案之間的「交換」來適當地建立跨專案界線

有兩種互補的選項可以在專案之間共用人工製品。簡化版本僅適用於您需要共用的是不依賴使用者的簡單人工製品。簡化解決方案也僅限於此人工製品未發佈至儲存庫的情況。這也表示使用者未發佈對此人工製品的依賴關係。在使用者在不同脈絡中解析為不同人工製品(例如,不同的目標平台)或需要發佈的情況下,您需要使用進階版本

專案之間的簡單人工製品共用

首先,生產者需要宣告一個組態,此組態將公開給使用者。如組態章節中所述,這對應於可消耗組態

假設使用者需要生產者的工具類別,但此人工製品不是主要的。生產者可以透過建立一個將「承載」此人工製品的組態來公開其工具類別。

producer/build.gradle.kts
val instrumentedJars by configurations.creating {
    isCanBeConsumed = true
    isCanBeResolved = false
    // If you want this configuration to share the same dependencies, otherwise omit this line
    extendsFrom(configurations["implementation"], configurations["runtimeOnly"])
}
producer/build.gradle
configurations {
    instrumentedJars {
        canBeConsumed = true
        canBeResolved = false
        // If you want this configuration to share the same dependencies, otherwise omit this line
        extendsFrom implementation, runtimeOnly
    }
}

此組態為可消耗,表示它是供使用者「交換」的。我們現在將人工製品新增至此組態,使用者在消耗時會取得這些人工製品。

producer/build.gradle.kts
artifacts {
    add("instrumentedJars", instrumentedJar)
}
producer/build.gradle
artifacts {
    instrumentedJars(instrumentedJar)
}

在此,我們附加的「人工製品」是一個實際上會產生 Jar 的工作。這樣一來,Gradle 可以自動追蹤此工作的依賴關係,並視需要建立它們。這是因為 Jar 工作延伸了 AbstractArchiveTask。如果不是這樣,您將需要明確宣告人工製品的產生方式。

producer/build.gradle.kts
artifacts {
    add("instrumentedJars", someTask.outputFile) {
        builtBy(someTask)
    }
}
producer/build.gradle
artifacts {
    instrumentedJars(someTask.outputFile) {
        builtBy(someTask)
    }
}

現在,使用者需要依賴此組態才能取得正確的人工製品。

consumer/build.gradle.kts
dependencies {
    instrumentedClasspath(project(mapOf(
        "path" to ":producer",
        "configuration" to "instrumentedJars")))
}
consumer/build.gradle
dependencies {
    instrumentedClasspath(project(path: ":producer", configuration: 'instrumentedJars'))
}
宣告對明確目標組態的相依性不建議使用。如果您計畫發布具有此相依性的元件,這可能會導致損毀的元資料。如果您需要在遠端存放庫上發布元件,請遵循 變異感知跨發布文件 的指示。

在這個案例中,我們將相依性新增到 instrumentedClasspath 組態,這是一個消費者特定組態。在 Gradle 術語中,這稱為 可解析組態,定義方式如下

consumer/build.gradle.kts
val instrumentedClasspath by configurations.creating {
    isCanBeConsumed = false
}
consumer/build.gradle
configurations {
    instrumentedClasspath {
        canBeConsumed = false
    }
}

專案間變異感知的成品分享

簡單的成品分享解決方案 中,我們在生產者端定義一個組態,作為生產者和消費者之間成品交換的媒介。然而,消費者必須明確告知它依賴於哪個組態,而這正是我們在變異感知解析中想要避免的。事實上,我們也 說明 消費者可以使用屬性表達需求,而生產者也應該使用屬性提供適當的傳出變異。這允許更智慧的選擇,因為使用單一相依性宣告,而沒有任何明確的目標組態,消費者可能會解析不同的內容。典型的範例是使用單一相依性宣告 project(":myLib"),我們會根據架構選擇 myLibarm64i386 版本。

為此,我們將屬性新增到消費者和生產者。

了解組態具有屬性後,它們會參與變異感知解析非常重要,這表示當使用 project(":myLib")任何表示法時,它們會被視為候選項。換句話說,在生產者上設定的屬性必須與在同一個專案上產生的其他變異相符。特別是,它們不能為現有選擇引入歧義。

實際上,表示您建立的組態上使用的屬性集可能會依賴於使用的生態系統(Java、C++、…​),因為這些生態系統相關的外掛程式通常使用不同的屬性。

讓我們增強先前的範例,它剛好是一個 Java 函式庫專案。Java 函式庫會向其使用者公開幾個變體,apiElementsruntimeElements。現在,我們要新增第 3 個變體,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

它告訴我們 Java 函式庫外掛程式會產生具有 5 個屬性的變體

  • org.gradle.category 告訴我們此變體代表一個函式庫

  • org.gradle.dependency.bundling 告訴我們此變體的相依性會以 jar 檔的形式找到(例如,它們不會重新封裝在 jar 檔內部)

  • org.gradle.jvm.version 告訴我們此函式庫支援的最低 Java 版本是 Java 11

  • org.gradle.libraryelements 告訴我們此變體包含 jar 檔中找到的所有元素(類別和資源)

  • org.gradle.usage 表示此變體是 Java 執行時期,因此適用於 Java 編譯器,也適用於執行時期

因此,如果我們希望在執行測試時,我們的工具類別用於取代此變體,我們需要將類似的屬性附加到我們的變體。事實上,我們關心的屬性是 org.gradle.libraryelements,它說明變體包含什麼,因此我們可以這樣設定變體

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"))
    }
}
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'))
        }
    }
}

選擇要設定的正確屬性是此程序中最困難的事情,因為它們會傳遞變體的語意。因此,在新增新屬性之前,您應該始終自問是否沒有傳遞您需要的語意的屬性。如果沒有,則可以新增新的屬性。在新增新屬性時,您也必須小心,因為它可能會在選擇過程中造成歧義。通常,新增屬性表示將其新增到所有現有變體。

我們在此所做的,是新增一個新的變體,它可以在執行時期使用,但包含工具類別,而不是一般類別。然而,這現在表示對於執行時期,使用者必須在兩個變體之間進行選擇

  • runtimeElementsjava-library 外掛程式提供的常規變體

  • instrumentedJars,我們建立的變體

特別是,假設我們希望在測試執行時期類別路徑上使用工具類別。現在,我們可以在使用者上宣告我們的相依性為常規專案相依性

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

如果我們就此打住,Gradle 仍會選擇 runtimeElements 變體,而非我們的 instrumentedJars 變體。這是因為 testRuntimeClasspath 組態要求組態的 libraryelements 屬性為 jar,而我們新的 instrumented-jars不相容

因此,我們需要變更所要求的屬性,以便現在尋找已編入工具的 jar

consumer/build.gradle.kts
configurations {
    testRuntimeClasspath {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, "instrumented-jar"))
        }
    }
}
consumer/build.gradle
configurations {
    testRuntimeClasspath {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, '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 變體。這是因為我們沒有說明,如果沒有可用的已編入工具版本,可以使用一般 jar。為此,我們需要撰寫相容性規則

範例 9. 相容性規則
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
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
dependencies {
    attributesSchema {
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
            compatibilityRules.add(InstrumentedJarsRule::class.java)
        }
    }
}
consumer/build.gradle
dependencies {
    attributesSchema {
        attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
            compatibilityRules.add(InstrumentedJarsRule)
        }
    }
}

這樣就完成了!現在我們有

  • 新增提供已編入工具 jar 的變體

  • 說明此變體是執行時期的替代品

  • 說明消費者僅在測試執行時期需要此變體

因此,Gradle 提供了一個強大的機制,可根據偏好和相容性選擇正確的變體。可以在文件中的變體感知外掛區段找到更多詳細資料。

透過新增值至現有屬性(如同我們所做的一樣),或定義新的屬性,我們正在擴充模型。這表示所有消費者都必須知道這個已擴充的模型。

對於本地消費者來說,這通常不是問題,因為所有專案都瞭解並共用相同的架構,但如果你必須將這個新變體發佈到外部儲存庫,這表示外部消費者必須將相同的規則新增到他們的建置中才能通過。這通常不是生態系統外掛程式(例如:Kotlin 外掛程式)的問題,因為在沒有套用外掛程式的情況下,消費在任何情況下都是不可能的,但如果你新增自訂值或屬性,這就會成為一個問題。

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

鎖定不同的平台

一個函式庫鎖定不同的平台是很常見的。在 Java 生態系統中,我們經常看到同一個函式庫有不同的成品,以不同的分類器區分。一個典型的範例是 Guava,它發佈為

  • guava-jre,適用於 JDK 8 及以上版本

  • guava-android,適用於 JDK 7

這種方法的問題在於分類器沒有關聯的語意。特別是,相依性解析引擎無法根據消費者需求自動判斷要使用哪個版本。例如,最好表達你對 Guava 有相依性,並讓引擎在相容的版本中選擇 jreandroid

Gradle 提供了一個改進的模型,它沒有分類器的缺點:屬性。

特別是在 Java 生態系統中,Gradle 提供了一個內建屬性,函式庫作者可以使用它來表達與 Java 生態系統的相容性:org.gradle.jvm.version。這個屬性表達消費者必須具備的最小版本才能正常運作

當你套用 javajava-library 外掛程式時,Gradle 會自動將這個屬性關聯到傳出的變體。這表示所有使用 Gradle 發佈的函式庫都會自動說明它們使用的目標平台。

預設情況下,org.gradle.jvm.version 會設定為來源設定檔主編譯任務的 release 屬性(或作為 targetCompatibility 值的後備)的值。

雖然此屬性會自動設定,但 Gradle 不會 預設讓您為不同的 JVM 建置專案。如果您需要執行此動作,則需要依照 關於變異感知比對的說明 來建立其他變異。

Gradle 的未來版本將提供自動為不同 Java 平台建置的方法。