在多專案建置中,常見的模式是一個專案使用另一個專案的成品。
一般而言,在 Java 生態系統中最簡單的使用形式是,當 A
相依於 B
時,A
將相依於專案 B
產生的 jar
。
考量與可能的解決方案
宣告跨專案相依性的常見反模式是
dependencies {
// this is unsafe!
implementation project(":other").tasks.someOtherJar
}
此發佈模型是不安全的,並可能導致無法重現且難以平行化的建置。
不要直接參考其他專案工作! |
您可以在生產者端定義一個配置,作為生產者和消費者之間交換成品的用途。
dependencies {
instrumentedClasspath(project(path: ":producer", configuration: 'instrumentedJars'))
}
dependencies {
instrumentedClasspath(project(mapOf(
"path" to ":producer",
"configuration" to "instrumentedJars")))
}
但是,消費者必須明確告知它相依於哪個配置,而這不建議。如果您計劃發佈具有此相依性的組件,則可能會導致中繼資料損壞。
本節說明如何透過使用變體定義專案之間的「交換」來正確建立跨專案邊界。
變體感知成品共用
Gradle 的 變體模型 允許消費者使用屬性指定需求,而生產者也使用屬性提供適當的傳出變體。
例如,像 project(":myLib")
這樣的單一相依性宣告可以根據架構選擇 myLib
的 arm64
或 i386
版本。
為了實現這一點,必須在消費者和生產者配置上都定義屬性。
當配置具有屬性時,它們會參與變體感知解析。這表示每當使用任何相依性宣告(例如 生產者配置上的屬性必須與同一專案提供的其他變體一致。引入不一致或不明確的屬性可能會導致解析失敗。 實際上,您定義的屬性通常取決於生態系統(例如,Java、C++),因為特定於生態系統的外掛程式通常會應用不同的屬性慣例。 |
考慮 Java 函式庫專案的範例。Java 函式庫通常向消費者公開兩個變體:apiElements
和 runtimeElements
。在本例中,我們新增了第三個變體 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 個屬性
-
org.gradle.category
指出此變體代表函式庫。 -
org.gradle.dependency.bundling
指定相依性是外部 jar(未重新封裝在 jar 內)。 -
org.gradle.jvm.version
表示支援的最低 Java 版本,即 Java 11。 -
org.gradle.libraryelements
顯示此變體包含通常在 jar 中找到的所有元素(類別和資源)。 -
org.gradle.usage
將變體定義為 Java 執行階段,適用於編譯和執行階段。
為了確保在執行測試時使用 instrumentedJars
變體來代替 runtimeElements
,我們必須將類似的屬性附加到這個新變體。
此配置的關鍵屬性是 org.gradle.libraryelements
,因為它描述了變體包含的內容。我們可以相應地設定 instrumentedJars
變體
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'))
}
}
}
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 的元素,使其能夠被適當地選擇。
選擇正確的屬性是此過程中最具挑戰性的部分,因為它們定義了變體的語意。在引入新屬性之前,請務必考慮現有屬性是否已傳達所需的語意。如果沒有合適的屬性存在,您可以建立一個新的屬性。但是,請務必謹慎 — 新增屬性可能會在變體選擇期間引入歧義。在許多情況下,新增屬性需要將其一致地應用於所有現有變體。 |
我們為執行階段引入了一個新的變體,它提供檢測過的類別而不是普通的類別。因此,消費者現在面臨兩個執行階段變體之間的選擇
-
runtimeElements
-java-library
外掛程式提供的預設執行階段變體。 -
instrumentedJars
- 我們新增的自訂變體。
如果我們希望將檢測過的類別包含在測試執行階段類別路徑中,我們現在可以將消費者端的相依性宣告為常規專案相依性
dependencies {
testImplementation 'junit:junit:4.13'
testImplementation project(':producer')
}
dependencies {
testImplementation("junit:junit:4.13")
testImplementation(project(":producer"))
}
如果我們就此停止,Gradle 仍然會解析 runtimeElements
變體,而不是 instrumentedJars
變體。
發生這種情況是因為 testRuntimeClasspath
配置請求將 libraryelements
屬性設定為 jar
的變體,而我們的 instrumented-jars
值不匹配。
為了修正此問題,我們需要更新請求的屬性,以專門針對檢測過的 jar
configurations {
testRuntimeClasspath {
attributes {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'instrumented-jar'))
}
}
}
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。為了解決這個問題,我們需要定義一個相容性規則
abstract class InstrumentedJarsRule implements AttributeCompatibilityRule<LibraryElements> {
@Override
void execute(CompatibilityCheckDetails<LibraryElements> details) {
if (details.consumerValue.name == 'instrumented-jar' && details.producerValue.name == 'jar') {
details.compatible()
}
}
}
abstract class InstrumentedJarsRule: AttributeCompatibilityRule<LibraryElements> {
override fun execute(details: CompatibilityCheckDetails<LibraryElements>) = details.run {
if (consumerValue?.name == "instrumented-jar" && producerValue?.name == "jar") {
compatible()
}
}
}
然後,我們在屬性架構上宣告此規則
dependencies {
attributesSchema {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
compatibilityRules.add(InstrumentedJarsRule)
}
}
}
dependencies {
attributesSchema {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
compatibilityRules.add(InstrumentedJarsRule::class.java)
}
}
}
就這樣!現在我們有了
-
新增了一個提供檢測過的 jar 的變體。
-
指定此變體是執行階段的替代品。
-
定義消費者僅在測試執行階段需要此變體。
Gradle 提供了一個強大的機制,用於根據偏好和相容性選擇正確的變體。如需更多詳細資訊,請查看文件中的 變體感知外掛程式章節。
透過將值新增到現有屬性或定義新屬性,我們正在擴展模型。這表示所有消費者都必須了解這個擴展的模型。 對於本機消費者,這通常不是問題,因為所有專案都共用相同的架構。但是,如果您需要將這個新變體發佈到外部儲存庫,外部消費者也必須在其建置中新增相同的規則才能使其運作。 對於生態系統外掛程式(例如,Kotlin 外掛程式)而言,這通常不是問題,因為不套用外掛程式就無法使用。但是,如果您新增自訂值或屬性,則會變得有問題。 因此,如果僅供內部使用,請避免發佈自訂變體。 |