在多專案建置中,常見的模式是,一個專案會使用另一個專案的人工製品。一般來說,Java 生態系統中最簡單的消費形式是,當 A
依賴 B
時,A
會依賴專案 B
所產生的 jar
。正如本章節先前所述,這會透過 A
依賴 B
的變異來建模,其中變異會根據 A
的需求來選擇。對於編譯,我們需要 B
的 API 依賴項,由 apiElements
變異提供。對於執行時期,我們需要 B
的執行時期依賴項,由 runtimeElements
變異提供。
但是,如果你需要一個不同於主要人工製品的人工製品怎麼辦?例如,Gradle 提供內建支援,可以依賴另一個專案的測試固定裝置,但有時你需要依賴的人工製品並未公開為變異。
為了在專案之間安全共用並允許最大效能(平行處理),此類人工製品必須透過傳出設定檔公開。
宣告跨專案依賴項的常見反模式是
dependencies {
// this is unsafe!
implementation project(":other").tasks.someOtherJar
}
這種發布模式不安全,可能會導致無法重製且難以平行處理的建置。本節說明如何透過使用變異定義專案之間的「交換」來適當地建立跨專案界線。
專案之間的簡單人工製品共用
首先,生產者需要宣告一個組態,此組態將公開給使用者。如組態章節中所述,這對應於可消耗組態。
假設使用者需要生產者的工具類別,但此人工製品不是主要的。生產者可以透過建立一個將「承載」此人工製品的組態來公開其工具類別。
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"])
}
configurations {
instrumentedJars {
canBeConsumed = true
canBeResolved = false
// If you want this configuration to share the same dependencies, otherwise omit this line
extendsFrom implementation, runtimeOnly
}
}
此組態為可消耗,表示它是供使用者「交換」的。我們現在將人工製品新增至此組態,使用者在消耗時會取得這些人工製品。
artifacts {
add("instrumentedJars", instrumentedJar)
}
artifacts {
instrumentedJars(instrumentedJar)
}
在此,我們附加的「人工製品」是一個實際上會產生 Jar 的工作。這樣一來,Gradle 可以自動追蹤此工作的依賴關係,並視需要建立它們。這是因為 Jar
工作延伸了 AbstractArchiveTask
。如果不是這樣,您將需要明確宣告人工製品的產生方式。
artifacts {
add("instrumentedJars", someTask.outputFile) {
builtBy(someTask)
}
}
artifacts {
instrumentedJars(someTask.outputFile) {
builtBy(someTask)
}
}
現在,使用者需要依賴此組態才能取得正確的人工製品。
dependencies {
instrumentedClasspath(project(mapOf(
"path" to ":producer",
"configuration" to "instrumentedJars")))
}
dependencies {
instrumentedClasspath(project(path: ":producer", configuration: 'instrumentedJars'))
}
宣告對明確目標組態的相依性不建議使用。如果您計畫發布具有此相依性的元件,這可能會導致損毀的元資料。如果您需要在遠端存放庫上發布元件,請遵循 變異感知跨發布文件 的指示。 |
在這個案例中,我們將相依性新增到 instrumentedClasspath 組態,這是一個消費者特定組態。在 Gradle 術語中,這稱為 可解析組態,定義方式如下
val instrumentedClasspath by configurations.creating {
isCanBeConsumed = false
}
configurations {
instrumentedClasspath {
canBeConsumed = false
}
}
專案間變異感知的成品分享
在 簡單的成品分享解決方案 中,我們在生產者端定義一個組態,作為生產者和消費者之間成品交換的媒介。然而,消費者必須明確告知它依賴於哪個組態,而這正是我們在變異感知解析中想要避免的。事實上,我們也 說明 消費者可以使用屬性表達需求,而生產者也應該使用屬性提供適當的傳出變異。這允許更智慧的選擇,因為使用單一相依性宣告,而沒有任何明確的目標組態,消費者可能會解析不同的內容。典型的範例是使用單一相依性宣告 project(":myLib")
,我們會根據架構選擇 myLib
的 arm64
或 i386
版本。
為此,我們將屬性新增到消費者和生產者。
了解組態具有屬性後,它們會參與變異感知解析非常重要,這表示當使用 project(":myLib")
等任何表示法時,它們會被視為候選項。換句話說,在生產者上設定的屬性必須與在同一個專案上產生的其他變異相符。特別是,它們不能為現有選擇引入歧義。
實際上,表示您建立的組態上使用的屬性集可能會依賴於使用的生態系統(Java、C++、…),因為這些生態系統相關的外掛程式通常使用不同的屬性。
讓我們增強先前的範例,它剛好是一個 Java 函式庫專案。Java 函式庫會向其使用者公開幾個變體,apiElements
和 runtimeElements
。現在,我們要新增第 3 個變體,instrumentedJars
。
因此,我們需要了解新的變體是用於什麼,才能設定其上適當的屬性。讓我們看看我們在生產者的 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
,它說明變體包含什麼,因此我們可以這樣設定變體
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"))
}
}
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'))
}
}
}
選擇要設定的正確屬性是此程序中最困難的事情,因為它們會傳遞變體的語意。因此,在新增新屬性之前,您應該始終自問是否沒有傳遞您需要的語意的屬性。如果沒有,則可以新增新的屬性。在新增新屬性時,您也必須小心,因為它可能會在選擇過程中造成歧義。通常,新增屬性表示將其新增到所有現有變體。 |
我們在此所做的,是新增一個新的變體,它可以在執行時期使用,但包含工具類別,而不是一般類別。然而,這現在表示對於執行時期,使用者必須在兩個變體之間進行選擇
-
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::class.java, "instrumented-jar"))
}
}
}
configurations {
testRuntimeClasspath {
attributes {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'instrumented-jar'))
}
}
}
我們可以在消費者端查看另一份報告,以查看將要求每個相依性的哪些屬性
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。為此,我們需要撰寫相容性規則
abstract class InstrumentedJarsRule: AttributeCompatibilityRule<LibraryElements> {
override fun execute(details: CompatibilityCheckDetails<LibraryElements>) = details.run {
if (consumerValue?.name == "instrumented-jar" && producerValue?.name == "jar") {
compatible()
}
}
}
abstract class InstrumentedJarsRule implements AttributeCompatibilityRule<LibraryElements> {
@Override
void execute(CompatibilityCheckDetails<LibraryElements> details) {
if (details.consumerValue.name == 'instrumented-jar' && details.producerValue.name == 'jar') {
details.compatible()
}
}
}
我們需要在屬性架構中宣告
dependencies {
attributesSchema {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
compatibilityRules.add(InstrumentedJarsRule::class.java)
}
}
}
dependencies {
attributesSchema {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
compatibilityRules.add(InstrumentedJarsRule)
}
}
}
這樣就完成了!現在我們有
-
新增提供已編入工具 jar 的變體
-
說明此變體是執行時期的替代品
-
說明消費者僅在測試執行時期需要此變體
因此,Gradle 提供了一個強大的機制,可根據偏好和相容性選擇正確的變體。可以在文件中的變體感知外掛區段找到更多詳細資料。
透過新增值至現有屬性(如同我們所做的一樣),或定義新的屬性,我們正在擴充模型。這表示所有消費者都必須知道這個已擴充的模型。 對於本地消費者來說,這通常不是問題,因為所有專案都瞭解並共用相同的架構,但如果你必須將這個新變體發佈到外部儲存庫,這表示外部消費者必須將相同的規則新增到他們的建置中才能通過。這通常不是生態系統外掛程式(例如:Kotlin 外掛程式)的問題,因為在沒有套用外掛程式的情況下,消費在任何情況下都是不可能的,但如果你新增自訂值或屬性,這就會成為一個問題。 因此,避免發佈僅供內部使用的自訂變體。 |
鎖定不同的平台
一個函式庫鎖定不同的平台是很常見的。在 Java 生態系統中,我們經常看到同一個函式庫有不同的成品,以不同的分類器區分。一個典型的範例是 Guava,它發佈為
-
guava-jre
,適用於 JDK 8 及以上版本 -
guava-android
,適用於 JDK 7
這種方法的問題在於分類器沒有關聯的語意。特別是,相依性解析引擎無法根據消費者需求自動判斷要使用哪個版本。例如,最好表達你對 Guava 有相依性,並讓引擎在相容的版本中選擇 jre
和 android
。
Gradle 提供了一個改進的模型,它沒有分類器的缺點:屬性。
特別是在 Java 生態系統中,Gradle 提供了一個內建屬性,函式庫作者可以使用它來表達與 Java 生態系統的相容性:org.gradle.jvm.version
。這個屬性表達消費者必須具備的最小版本才能正常運作。
當你套用 java
或 java-library
外掛程式時,Gradle 會自動將這個屬性關聯到傳出的變體。這表示所有使用 Gradle 發佈的函式庫都會自動說明它們使用的目標平台。
預設情況下,org.gradle.jvm.version
會設定為來源設定檔主編譯任務的 release
屬性(或作為 targetCompatibility
值的後備)的值。
雖然此屬性會自動設定,但 Gradle 不會 預設讓您為不同的 JVM 建置專案。如果您需要執行此動作,則需要依照 關於變異感知比對的說明 來建立其他變異。
Gradle 的未來版本將提供自動為不同 Java 平台建置的方法。 |