在 Gradle 中排除傳遞依賴
1. 概述
Gradle 是一個建置自動化工具,用於管理和自動化建置、測試和部署應用程式的流程。
使用基於 Groovy 或 Kotlin 的領域特定語言 (DSL) 來定義建置任務,可以輕鬆自動定義和管理專案中所需的庫相依性。
在本教程中,我們將具體討論在 Gradle 中排除傳遞依賴的幾種方法。
2.什麼是傳遞依賴?
假設我們使用的庫 A依賴另一個庫 B。預設情況下,當我們包含 A 時,Gradle 會自動將 B 新增到專案的類別路徑中,以便 B 中的程式碼也可以在我們的專案中使用,即使我們沒有明確地將其新增為依賴項。
為了說得更清楚,讓我們用一個真實的例子,我們在專案中定義了 Google Guava:
dependencies {
// ...
implementation 'com.google.guava:guava:31.1-jre'
}
如果 Google Guava 與其他函式庫有依賴關係,那麼 Gradle 就會自動包含這些其他函式庫。
要查看我們在專案中使用的依賴項,我們可以列印它們:
./gradlew <module-name>:dependencies
在這種情況下,我們使用一個名為excluding-transitive-dependencies
的模組:
./gradlew excluding-transitive-dependencies:dependencies
讓我們看看輸出:
testRuntimeClasspath - Runtime classpath of source set 'test'.
\--- com.google.guava:guava:31.1-jre
+--- com.google.guava:failureaccess:1.0.1
+--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
+--- com.google.code.findbugs:jsr305:3.0.2
+--- org.checkerframework:checker-qual:3.12.0
+--- com.google.errorprone:error_prone_annotations:2.11.0
\--- com.google.j2objc:j2objc-annotations:1.3
我們可以看到一些我們沒有明確定義的程式庫,但 Gradle 自動添加了它們,因為 Google Guava 需要它們。
然而,有時我們可能有充分的理由排除傳遞依賴。
3. 為什麼要排除傳遞依賴?
讓我們回顧為什麼我們可能想要排除傳遞依賴的幾個充分理由:
- 避免安全性問題:例如,Firestore Firebase SDK 24.4.0 或 Dagger 2.44 對 Google Guava 31.1-jre 具有傳遞依賴關係,該依賴關係存在安全漏洞問題。
- 避免不必要的依賴項:某些程式庫可能會帶來與我們的應用程式無關的依賴項。然而,應該明智而仔細地考慮——例如,當我們需要完全排除傳遞依賴項時,無論版本號如何。
- 減少應用程式大小:透過排除未使用的傳遞依賴項,我們可以減少打包到應用程式中的庫的數量,從而減少輸出檔案(JAR、WAR、APK)的大小。我們還可以使用 ProGuard 等工具,透過刪除未使用的程式碼、優化字節碼、混淆類別和方法名稱以及刪除不必要的資源來顯著減小應用程式的大小。此過程會在不犧牲功能的情況下產生更小、更快且更有效率的應用程式。
所以,Gradle也提供了一個排除依賴的機制。
3.1 解決版本衝突
我們不建議排除傳遞依賴來解決版本衝突,因為Gradle 已經有一個很好的機制來處理這個問題。
當有兩個或多個相同的依賴項時,Gradle 只會選擇一個。如果版本不同, 預設會選擇最新版本。如果我們仔細觀察,此行為會顯示在日誌中:
+--- org.hibernate.orm:hibernate-core:7.0.0.Beta1
| +--- jakarta.persistence:jakarta.persistence-api:3.2.0-M2
| +--- jakarta.transaction:jakarta.transaction-api:2.0.1
| +--- org.jboss.logging:jboss-logging:3.5.0.Final <-------------------+ same version
| +--- org.hibernate.models:hibernate-models:0.8.6 |
| | +--- io.smallrye:jandex:3.1.2 -> 3.2.0 <------------------+ |
| | \--- org.jboss.logging:jboss-logging:3.5.0.Final +-----------|--+
| +--- io.smallrye:jandex:3.2.0 +-------------------------------+ latest version
| +--- com.fasterxml:classmate:1.5.1
| | \--- jakarta.activation:jakarta.activation-api:2.1.0 -> 2.1.1 <---+
| +--- org.glassfish.jaxb:jaxb-runtime:4.0.2 |
| | \--- org.glassfish.jaxb:jaxb-core:4.0.2 |
| | +--- jakarta.xml.bind:jakarta.xml.bind-api:4.0.0 (*) |
| | +--- jakarta.activation:jakarta.activation-api:2.1.1 +-----+ latest version
| | +--- org.eclipse.angus:angus-activation:2.0.0
我們可以看到一些已識別的依賴關係是相同的。例如, org.jboss.logging:jboss-logging:3.5.0.Final
出現兩次,但由於它們是相同的版本,Gradle 將只包含一份副本。
同時,對於jakarta.activation:jakarta.activation-api
,找到兩個版本 - 2.1.0 和 2.1.1。 Gradle 將選擇最新版本,分別為 2.1.1。 io.smallrye:jandex
也是如此,它將選擇 3.2.0。
但有時我們不想使用最新版本。我們可以強制 Gradle 選擇我們嚴格需要的版本:
implementation("io.smallrye:jandex") {
version {
strictly '3.1.2'
}
}
在這裡,即使找到另一個甚至更新的版本,Gradle 仍然會選擇版本 3.1.2。
我們可以聲明具有特定版本或版本範圍的依賴項,以定義我們的專案可以使用的依賴項的可接受版本。
4. 排除傳遞依賴
我們可以排除各種場景中的傳遞依賴。好吧,為了讓它更清晰、更容易理解,我們將使用我們可能熟悉的庫的真實範例。
4.1.排除組
當我們定義一個依賴項時,例如 Google Guava,如果我們看一下,依賴項的格式如下:
com.google.guava : guava : 31.1-jre
---------------- ----- --------
^ ^ ^
| | |
group module version
如果我們查看第 2 節中的輸出,我們將看到 Google Guava 依賴的五個模組。它們是com.google.code.findbugs
、 com.google.errorprone
、 com.google.guava
、 com.google.j2objc
和org.checkerframework
。
我們將排除com.google.guava group
,其中包含guava
、 failureaccess
和listenablefuture
模組:
dependencies {
// ...
implementation ('com.google.guava:guava:31.1-jre') {
exclude group: 'com.google.guava'
}
}
這將排除com.google.guava
群組中除guava
之外的所有模組,因為它是主模組。
4.2.排除特定模組
要排除特定模組依賴項,我們可以使用目標路徑。例如,當我們使用Hibernate函式庫時,我們只需要排除org.glassfish.jaxb:txw2
模組:
dependencies {
// ...
implementation ('org.hibernate.orm:hibernate-core:7.0.0.Beta1') {
exclude group: 'org.glassfish.jaxb', module : 'txw2'
}
}
這意味著即使 Hibernate 依賴txw2
模組,我們也不會在專案中包含該模組。
4.3.排除多個模組
Gradle 也允許我們在單一依賴關係語句中排除多個模組:
dependencies {
// ...
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation ('org.junit.jupiter:junit-jupiter') {
exclude group: 'org.junit.jupiter', module : 'junit-jupiter-api'
exclude group: 'org.junit.jupiter', module : 'junit-jupiter-params'
exclude group: 'org.junit.jupiter', module : 'junit-jupiter-engine'
}
}
在此範例中,我們從org.junit-jupiter
依賴項中排除junit-jupiter-api
、 junit-jupiter-params
和junit-jupiter-engine
模組。
透過這個機制,我們可以對更多的模組排除情況做同樣的事情:
dependencies {
// ...
implementation('com.google.android.gms:play-services-mlkit-face-detection:17.1.0') {
exclude group: 'androidx.annotation', module: 'annotation'
exclude group: 'android.support.v4', module: 'core'
exclude group: 'androidx.arch.core', module: 'core'
exclude group: 'androidx.collection', module: 'collection'
exclude group: 'androidx.coordinatorlayout', module: 'coordinatorlayout'
exclude group: 'androidx.core', module: 'core'
exclude group: 'androidx.viewpager', module: 'viewpager'
exclude group: 'androidx.print', module: 'print'
exclude group: 'androidx.localbroadcastmanager', module: 'localbroadcastmanager'
exclude group: 'androidx.loader', module: 'loader'
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel'
exclude group: 'androidx.lifecycle', module: 'lifecycle-livedata'
exclude group: 'androidx.lifecycle', module: 'lifecycle-common'
exclude group: 'androidx.fragment', module: 'fragment'
exclude group: 'androidx.drawerlayout', module: 'drawerlayout'
exclude group: 'androidx.legacy.content', module: 'legacy-support-core-utils'
exclude group: 'androidx.cursoradapter', module: 'cursoradapter'
exclude group: 'androidx.customview', module: 'customview'
exclude group: 'androidx.documentfile.provider', module: 'documentfile'
exclude group: 'androidx.interpolator', module: 'interpolator'
exclude group: 'androidx.exifinterface', module: 'exifinterface'
}
}
此範例從 Google ML Kit 依賴項中排除各種模組,以避免包含預設已包含在專案中的某些模組。
4.4.排除所有傳遞模組
有時我們可能只需要使用主模組而不需要任何其他依賴項。或者也許當我們需要明確指定所使用的每個依賴項的版本時。
transitive = false
語句將告訴 Gradle 不要自動包含我們使用的函式庫中的傳遞依賴項:
dependencies {
// ...
implementation('org.hibernate.orm:hibernate-core:7.0.0.Beta1') {
transitive = false
}
}
這意味著只有 Hibernate Core 本身會被加入到專案中,而沒有任何其他依賴項。
4.5.從每個配置中排除
除了在依賴聲明中排除傳遞依賴之外,我們還可以在configuration
層級這樣做。
我們可以使用configurations.configureEach { }
,它使用給定的操作配置集合中的每個元素。
此方法在 Gradle 4.9 及更高版本中可用,作為all()
的建議替代方法。
讓我們立即嘗試一下:
dependencies {
// ...
testImplementation 'org.mockito:mockito-core:3.+'
}
configurations.configureEach {
exclude group: 'net.bytebuddy', module: 'byte-buddy-agent'
}
這意味著我們從使用該依賴項的所有配置中排除net.bytebuddy
群組的byte-buddy-agent
模組。
4.6.在特定配置中排除
有時我們可能需要透過針對特定配置來排除依賴項。當然,Gradle 也允許這樣做:
configurations.testImplementation {
exclude group: 'org.junit.jupiter', module : 'junit-jupiter-engine'
}
configurations.testCompileClasspath {
exclude group : 'com.google.j2objc', module : 'j2objc-annotations'
}
configurations.annotationProcessor {
exclude group: 'com.google.guava'
}
是的,Gradle 允許以這種方式排除依賴項。我們可以在類別路徑中使用exclude
進行特定configurations
例如testImplementation
、 testCompileClasspath
、 annotationProcessor
等。
5. 結論
排除傳遞依賴有三個主要原因:避免安全問題、避免不必要的程式庫、減少應用程式大小。
在本文中,我們討論了在 Gradle 中排除傳遞依賴的方法,從按組、特定模組或多個模組排除,到排除所有傳遞模組。我們也可以在配置層級進行排除。然而,必須明智且仔細地考慮排除情況。
與往常一樣,完整的源代碼可以在 GitHub 上取得。