如 不同類型的組態中所述,相同的相依性可能有不同的變異。例如,外部 Maven 相依性有一個變異,應該在針對相依性編譯時使用 (java-api
),以及一個用於執行使用相依性的應用程式的變異 (java-runtime
)。專案相依性有更多變異,例如,用於編譯的專案類別可用作類別目錄 (org.gradle.usage=java-api, org.gradle.libraryelements=classes
) 或 JAR (org.gradle.usage=java-api, org.gradle.libraryelements=jar
)。
相依性的變異可能在其傳遞相依性或人工產物本身有所不同。例如,Maven 相依性的 java-api
和 java-runtime
變異僅在傳遞相依性上有所不同,且兩者都使用相同的人工產物 — JAR 檔案。對於專案相依性,java-api,classes
和 java-api,jars
變異具有相同的傳遞相依性,但人工產物不同 — 分別為類別目錄和 JAR 檔案。
Gradle 透過其 屬性集合來唯一識別相依性的變異。相依性的 java-api
變異是由具有值 java-api
的 org.gradle.usage
屬性所識別的變異。
當 Gradle 解析組態時,已解析組態上的 屬性 決定要求的屬性。對於組態中的所有相依性,在解析組態時會選取具有要求屬性的變異。例如,當組態在專案相依性上要求 org.gradle.usage=java-api, org.gradle.libraryelements=classes
時,類別目錄會選為人工產物。
當相依項沒有符合要求屬性的變異時,解析組態會失敗。有時可以將相依項的成品轉換成要求的變異,而不會變更傳遞相依項。例如,解壓縮 JAR 會將 java-api,jars
變異的成品轉換成 java-api,classes
變異。這種轉換稱為「成品轉換」。Gradle 可以註冊成品轉換,當相依項沒有要求的變異時,Gradle 會嘗試尋找成品轉換鏈以建立變異。
成品轉換選取和執行
如上所述,當 Gradle 解析組態,而組態中的相依項沒有符合要求屬性的變異時,Gradle 會嘗試尋找成品轉換鏈以建立變異。尋找符合成品轉換鏈的程序稱為「成品轉換選取」。每個註冊的轉換會從一組屬性轉換成另一組屬性。例如,解壓縮轉換可以從 org.gradle.usage=java-api, org.gradle.libraryelements=jars
轉換成 org.gradle.usage=java-api, org.gradle.libraryelements=classes
。
為了找到鏈,Gradle 會從要求的屬性開始,然後將修改部分要求屬性的所有轉換視為通往那裡的可能路徑。Gradle 會向後尋找,嘗試使用轉換取得現有變異的路徑。
例如,考慮一個具有兩個值的 minified
屬性:true
和 false
。minified 屬性代表移除不必要類別檔案的相依項變異。有一個已註冊的成品轉換,可以將 minified
從 false
轉換成 true
。當相依項要求 minified=true
,而只有 minified=false
的變異時,Gradle 會選取已註冊的縮小轉換。縮小轉換可以將 minified=false
的相依項成品轉換成 minified=true
的成品。
在所有找到的轉換鏈中,Gradle 會嘗試選取最佳的轉換鏈
-
如果只有一個轉換鏈,就會選取它。
-
如果兩個轉換鏈,其中一個是另一個的字尾,就會選取它。
-
如果有一個最短的轉換鏈,就會選取它。
-
在所有其他情況下,選取會失敗,並報告錯誤。
當已經有符合要求屬性的相依項變異時,Gradle 就不會嘗試選取成品轉換。 |
|
在選取所需的工件轉換後,Gradle 會解析相依項的變體,這些變體是鏈中初始轉換所必需的。當 Gradle 完成解析變體的工件後,無論是透過下載外部相依項或執行產生工件的任務,Gradle 都會開始使用選取的工件轉換鏈轉換變體的工件。Gradle 會在可能的狀況下平行執行轉換鏈。
以上述的縮小範例為例,考慮一個具有兩個相依項的組態,外部的 guava
相依項和專案相依項在 producer
專案上。組態具有屬性 org.gradle.usage=java-runtime,org.gradle.libraryelements=jar,minified=true
。外部的 guava
相依項有兩個變體
-
org.gradle.usage=java-runtime,org.gradle.libraryelements=jar,minified=false
和 -
org.gradle.usage=java-api,org.gradle.libraryelements=jar,minified=false
.
使用縮小轉換,Gradle 可以將 guava
的變體 org.gradle.usage=java-runtime,org.gradle.libraryelements=jar,minified=false
轉換成 org.gradle.usage=java-runtime,org.gradle.libraryelements=jar,minified=true
,這是所要求的屬性。專案相依項也有變體
-
org.gradle.usage=java-runtime,org.gradle.libraryelements=jar,minified=false
, -
org.gradle.usage=java-runtime,org.gradle.libraryelements=classes,minified=false
, -
org.gradle.usage=java-api,org.gradle.libraryelements=jar,minified=false
, -
org.gradle.usage=java-api,org.gradle.libraryelements=classes,minified=false
-
以及更多。
同樣地,使用縮小轉換,Gradle 可以將專案 producer
的變體 org.gradle.usage=java-runtime,org.gradle.libraryelements=jar,minified=false
轉換成 org.gradle.usage=java-runtime,org.gradle.libraryelements=jar,minified=true
,這是所要求的屬性。
當組態已解析時,Gradle 需要下載 guava
JAR 並縮小它。Gradle 也需要執行 producer:jar
任務來產生專案的 JAR 工件,然後縮小它。下載和縮小 guava.jar
會與執行 producer:jar
任務和縮小結果的 JAR 平行進行。
以下是如何設定 minified
屬性,以便上述作業正常運作。您需要在架構中註冊新的屬性,將它加到所有 JAR 工件中,並在所有可解析的組態中要求它。
val artifactType = Attribute.of("artifactType", String::class.java)
val minified = Attribute.of("minified", Boolean::class.javaObjectType)
dependencies {
attributesSchema {
attribute(minified) (1)
}
artifactTypes.getByName("jar") {
attributes.attribute(minified, false) (2)
}
}
configurations.all {
afterEvaluate {
if (isCanBeResolved) {
attributes.attribute(minified, true) (3)
}
}
}
dependencies {
registerTransform(Minify::class) {
from.attribute(minified, false).attribute(artifactType, "jar")
to.attribute(minified, true).attribute(artifactType, "jar")
}
}
dependencies { (4)
implementation("com.google.guava:guava:27.1-jre")
implementation(project(":producer"))
}
tasks.register<Copy>("resolveRuntimeClasspath") { (5)
from(configurations.runtimeClasspath)
into(layout.buildDirectory.dir("runtimeClasspath"))
}
def artifactType = Attribute.of('artifactType', String)
def minified = Attribute.of('minified', Boolean)
dependencies {
attributesSchema {
attribute(minified) (1)
}
artifactTypes.getByName("jar") {
attributes.attribute(minified, false) (2)
}
}
configurations.all {
afterEvaluate {
if (canBeResolved) {
attributes.attribute(minified, true) (3)
}
}
}
dependencies {
registerTransform(Minify) {
from.attribute(minified, false).attribute(artifactType, "jar")
to.attribute(minified, true).attribute(artifactType, "jar")
}
}
dependencies { (4)
implementation('com.google.guava:guava:27.1-jre')
implementation(project(':producer'))
}
tasks.register("resolveRuntimeClasspath", Copy) {(5)
from(configurations.runtimeClasspath)
into(layout.buildDirectory.dir("runtimeClasspath"))
}
1 | 將屬性新增至架構 |
2 | 所有 JAR 檔案都未縮小 |
3 | 要求所有可解析組態的「minified=true」 |
4 | 新增將轉換的相依性 |
5 | 新增需要轉換後的成品的任務 |
現在您可以看到執行「resolveRuntimeClasspath」任務時會發生什麼事,此任務會解析「runtimeClasspath」組態。請注意,在「resolveRuntimeClasspath」任務開始之前,Gradle 會轉換專案相依性。Gradle 會在執行「resolveRuntimeClasspath」任務時轉換二進位相依性。
> gradle resolveRuntimeClasspath > Task :producer:compileJava > Task :producer:processResources NO-SOURCE > Task :producer:classes > Task :producer:jar > Transform producer.jar (project :producer) with Minify Nothing to minify - using producer.jar unchanged > Task :resolveRuntimeClasspath Minifying guava-27.1-jre.jar Nothing to minify - using listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar unchanged Nothing to minify - using jsr305-3.0.2.jar unchanged Nothing to minify - using checker-qual-2.5.2.jar unchanged Nothing to minify - using error_prone_annotations-2.2.0.jar unchanged Nothing to minify - using j2objc-annotations-1.1.jar unchanged Nothing to minify - using animal-sniffer-annotations-1.17.jar unchanged Nothing to minify - using failureaccess-1.0.1.jar unchanged BUILD SUCCESSFUL in 0s 3 actionable tasks: 3 executed
實作成品轉換
類似於任務類型,成品轉換包含一個動作和一些參數。與自訂任務類型的主要差異在於,動作和參數是實作為兩個獨立的類別。
成品轉換動作的實作是實作 TransformAction 的類別。您需要在動作中實作「transform()」方法,此方法會將輸入成品轉換成零、一或多個輸出成品。大多數成品轉換都是一對一的,因此轉換方法會將輸入成品轉換成一個輸出成品。
成品轉換動作的實作需要透過呼叫 TransformOutputs.dir() 或 TransformOutputs.file() 來註冊每個輸出成品。
您只能提供兩種類型的路徑至「dir」或「file」方法
-
輸入成品的絕對路徑或在輸入成品中(針對輸入目錄)。
-
相對路徑。
Gradle 使用絕對路徑作為輸出成品的位置。例如,如果輸入成品是展開的 WAR,則轉換動作可以針對「WEB-INF/lib」目錄中的所有 jar 檔案呼叫「TransformOutputs.file()」。轉換的輸出將是網路應用程式的程式庫 JAR。
對於相對路徑,「dir()」或「file()」方法會傳回工作區至轉換動作。轉換動作的實作需要在提供的「工作區」的位置建立轉換後的成品。
輸出成品會以註冊的順序取代轉換變異中的輸入成品。例如,如果組態包含成品「lib1.jar」、「lib2.jar」、「lib3.jar」,而轉換動作為輸入成品註冊縮小的輸出成品「<成品名稱>-min.jar」,則轉換後的組態將包含成品「lib1-min.jar」、「lib2-min.jar」和「lib3-min.jar」。
以下是「Unzip」轉換的實作,它會透過解壓縮將 JAR 檔案轉換成類別目錄。「Unzip」轉換不需要任何參數。請注意實作如何使用「@InputArtifact」將要轉換的成品注入至動作中。它使用「TransformOutputs.dir()」要求解壓縮類別的目錄,然後將 JAR 檔案解壓縮至這個目錄中。
abstract class Unzip : TransformAction<TransformParameters.None> { (1)
@get:InputArtifact (2)
abstract val inputArtifact: Provider<FileSystemLocation>
override
fun transform(outputs: TransformOutputs) {
val input = inputArtifact.get().asFile
val unzipDir = outputs.dir(input.name) (3)
unzipTo(input, unzipDir) (4)
}
private fun unzipTo(zipFile: File, unzipDir: File) {
// implementation...
}
}
abstract class Unzip implements TransformAction<TransformParameters.None> { (1)
@InputArtifact (2)
abstract Provider<FileSystemLocation> getInputArtifact()
@Override
void transform(TransformOutputs outputs) {
def input = inputArtifact.get().asFile
def unzipDir = outputs.dir(input.name) (3)
unzipTo(input, unzipDir) (4)
}
private static void unzipTo(File zipFile, File unzipDir) {
// implementation...
}
}
1 | 如果轉換不使用參數,請使用 TransformParameters.None |
2 | 注入輸入 Artifact |
3 | 要求未壓縮檔案的輸出位置 |
4 | 執行轉換的實際工作 |
Artifact 轉換可能需要參數,例如決定某些篩選條件的 String
,或用於支援輸入 Artifact 轉換的某些檔案集合。為了將這些參數傳遞給轉換動作,您需要定義一個具有所需參數的新類型。該類型需要實作標記介面 TransformParameters。參數必須使用 受管理的屬性 來表示,而參數類型必須是 受管理的類型。您可以使用宣告 getter 的介面或抽象類別,而 Gradle 會產生實作。所有 getter 都需要有適當的輸入註解,請參閱 增量建置註解 表格。
您可以在 開發自訂 Gradle 類型 中找到更多關於實作 Artifact 轉換參數的資訊。
以下是 Minify
轉換的實作,它透過只保留 JAR 中的特定類別來縮小 JAR。Minify
轉換需要將類別保留為參數。請注意您可以在 transform()
方法中透過 TransformAction.getParameters()
取得參數。transform()
方法的實作會使用 TransformOutputs.file()
要求縮小的 JAR 的位置,然後在此位置建立縮小的 JAR。
abstract class Minify : TransformAction<Minify.Parameters> { (1)
interface Parameters : TransformParameters { (2)
@get:Input
var keepClassesByArtifact: Map<String, Set<String>>
}
@get:PathSensitive(PathSensitivity.NAME_ONLY)
@get:InputArtifact
abstract val inputArtifact: Provider<FileSystemLocation>
override
fun transform(outputs: TransformOutputs) {
val fileName = inputArtifact.get().asFile.name
for (entry in parameters.keepClassesByArtifact) { (3)
if (fileName.startsWith(entry.key)) {
val nameWithoutExtension = fileName.substring(0, fileName.length - 4)
minify(inputArtifact.get().asFile, entry.value, outputs.file("${nameWithoutExtension}-min.jar"))
return
}
}
println("Nothing to minify - using ${fileName} unchanged")
outputs.file(inputArtifact) (4)
}
private fun minify(artifact: File, keepClasses: Set<String>, jarFile: File) {
println("Minifying ${artifact.name}")
// Implementation ...
}
}
abstract class Minify implements TransformAction<Parameters> { (1)
interface Parameters extends TransformParameters { (2)
@Input
Map<String, Set<String>> getKeepClassesByArtifact()
void setKeepClassesByArtifact(Map<String, Set<String>> keepClasses)
}
@PathSensitive(PathSensitivity.NAME_ONLY)
@InputArtifact
abstract Provider<FileSystemLocation> getInputArtifact()
@Override
void transform(TransformOutputs outputs) {
def fileName = inputArtifact.get().asFile.name
for (entry in parameters.keepClassesByArtifact) { (3)
if (fileName.startsWith(entry.key)) {
def nameWithoutExtension = fileName.substring(0, fileName.length() - 4)
minify(inputArtifact.get().asFile, entry.value, outputs.file("${nameWithoutExtension}-min.jar"))
return
}
}
println "Nothing to minify - using ${fileName} unchanged"
outputs.file(inputArtifact) (4)
}
private void minify(File artifact, Set<String> keepClasses, File jarFile) {
println "Minifying ${artifact.name}"
// Implementation ...
}
}
1 | 宣告參數類型 |
2 | 轉換參數的介面 |
3 | 使用參數 |
4 | 當不需要縮小時,使用未變更的輸入 Artifact |
請記住,輸入 Artifact 是相依性,可能具有其自己的相依性。如果您的 Artifact 轉換需要存取這些傳遞相依性,它可以宣告傳回 FileCollection
的抽象 getter,並使用 @InputArtifactDependencies 註解它。當您的轉換執行時,Gradle 會透過實作 getter 將傳遞相依性注入到該 FileCollection
屬性中。請注意,在轉換中使用輸入 Artifact 相依性會對效能造成影響,只有在您真正需要時才注入它們。
此外,Artifact 轉換可以為其輸出使用 建置快取。若要為 Artifact 轉換啟用建置快取,請在動作類別上新增 @CacheableTransform
註解。對於可快取的轉換,您必須使用正規化註解(例如 @PathSensitive)註解其 @InputArtifact 屬性,以及任何標記為 @InputArtifactDependencies 的屬性。
以下範例顯示較複雜的轉換。它將 JAR 的一些選取類別移至不同的套件,並改寫已移類別和使用已移類別的所有類別的位元組碼(類別重新配置)。為了決定要重新配置的類別,它會查看輸入人工製品的套件和輸入人工製品的相依性。它也不會重新配置外部類別路徑中 JAR 檔案中包含的套件。
@CacheableTransform (1)
abstract class ClassRelocator : TransformAction<ClassRelocator.Parameters> {
interface Parameters : TransformParameters { (2)
@get:CompileClasspath (3)
val externalClasspath: ConfigurableFileCollection
@get:Input
val excludedPackage: Property<String>
}
@get:Classpath (4)
@get:InputArtifact
abstract val primaryInput: Provider<FileSystemLocation>
@get:CompileClasspath
@get:InputArtifactDependencies (5)
abstract val dependencies: FileCollection
override
fun transform(outputs: TransformOutputs) {
val primaryInputFile = primaryInput.get().asFile
if (parameters.externalClasspath.contains(primaryInputFile)) { (6)
outputs.file(primaryInput)
} else {
val baseName = primaryInputFile.name.substring(0, primaryInputFile.name.length - 4)
relocateJar(outputs.file("$baseName-relocated.jar"))
}
}
private fun relocateJar(output: File) {
// implementation...
val relocatedPackages = (dependencies.flatMap { it.readPackages() } + primaryInput.get().asFile.readPackages()).toSet()
val nonRelocatedPackages = parameters.externalClasspath.flatMap { it.readPackages() }
val relocations = (relocatedPackages - nonRelocatedPackages).map { packageName ->
val toPackage = "relocated.$packageName"
println("$packageName -> $toPackage")
Relocation(packageName, toPackage)
}
JarRelocator(primaryInput.get().asFile, output, relocations).run()
}
}
@CacheableTransform (1)
abstract class ClassRelocator implements TransformAction<Parameters> {
interface Parameters extends TransformParameters { (2)
@CompileClasspath (3)
ConfigurableFileCollection getExternalClasspath()
@Input
Property<String> getExcludedPackage()
}
@Classpath (4)
@InputArtifact
abstract Provider<FileSystemLocation> getPrimaryInput()
@CompileClasspath
@InputArtifactDependencies (5)
abstract FileCollection getDependencies()
@Override
void transform(TransformOutputs outputs) {
def primaryInputFile = primaryInput.get().asFile
if (parameters.externalClasspath.contains(primaryInput)) { (6)
outputs.file(primaryInput)
} else {
def baseName = primaryInputFile.name.substring(0, primaryInputFile.name.length - 4)
relocateJar(outputs.file("$baseName-relocated.jar"))
}
}
private relocateJar(File output) {
// implementation...
def relocatedPackages = (dependencies.collectMany { readPackages(it) } + readPackages(primaryInput.get().asFile)) as Set
def nonRelocatedPackages = parameters.externalClasspath.collectMany { readPackages(it) }
def relocations = (relocatedPackages - nonRelocatedPackages).collect { packageName ->
def toPackage = "relocated.$packageName"
println("$packageName -> $toPackage")
new Relocation(packageName, toPackage)
}
new JarRelocator(primaryInput.get().asFile, output, relocations).run()
}
}
1 | 宣告轉換可快取 |
2 | 轉換參數的介面 |
3 | 為每個參數宣告輸入類型 |
4 | 為輸入人工製品宣告正規化 |
5 | 注入輸入人工製品相依性 |
6 | 使用參數 |
註冊人工製品轉換
您需要註冊人工製品轉換動作,在必要時提供參數,以便在解析相依性時可以選取它們。
為了註冊人工製品轉換,您必須在 dependencies {}
區塊中使用 registerTransform()。
使用 registerTransform()
時有幾個重點需要考量
-
需要
from
和to
屬性。 -
轉換動作本身可以有組態選項。您可以使用
parameters {}
區塊組態它們。 -
您必須在具有將解析的組態的專案上註冊轉換。
-
您可以提供任何實作 TransformAction 的類型給
registerTransform()
方法。
例如,假設您想要解壓縮一些相依性,並將解壓縮的目錄和檔案置於類別路徑上。您可以透過註冊類型為 Unzip
的人工製品轉換動作來執行此動作,如下所示
val artifactType = Attribute.of("artifactType", String::class.java)
dependencies {
registerTransform(Unzip::class) {
from.attribute(artifactType, "jar")
to.attribute(artifactType, "java-classes-directory")
}
}
def artifactType = Attribute.of('artifactType', String)
dependencies {
registerTransform(Unzip) {
from.attribute(artifactType, 'jar')
to.attribute(artifactType, 'java-classes-directory')
}
}
另一個範例是,您想要透過只保留一些 class
檔案來縮小 JAR。請注意 parameters {}
區塊的使用,以提供要保留在縮小 JAR 中的類別給 Minify
轉換。
val artifactType = Attribute.of("artifactType", String::class.java)
val minified = Attribute.of("minified", Boolean::class.javaObjectType)
val keepPatterns = mapOf(
"guava" to setOf(
"com.google.common.base.Optional",
"com.google.common.base.AbstractIterator"
)
)
dependencies {
registerTransform(Minify::class) {
from.attribute(minified, false).attribute(artifactType, "jar")
to.attribute(minified, true).attribute(artifactType, "jar")
parameters {
keepClassesByArtifact = keepPatterns
}
}
}
def artifactType = Attribute.of('artifactType', String)
def minified = Attribute.of('minified', Boolean)
def keepPatterns = [
"guava": [
"com.google.common.base.Optional",
"com.google.common.base.AbstractIterator"
] as Set
]
dependencies {
registerTransform(Minify) {
from.attribute(minified, false).attribute(artifactType, "jar")
to.attribute(minified, true).attribute(artifactType, "jar")
parameters {
keepClassesByArtifact = keepPatterns
}
}
}
實作漸增人工製品轉換
類似於 增量任務,人工產出轉換可以透過僅處理上次執行中變更的檔案來避免工作。這是透過使用 InputChanges 介面來完成的。對於人工產出轉換,只有輸入人工產出是增量輸入,因此轉換只能在那裡查詢變更。若要在轉換動作中使用 InputChanges,請將其注入動作中。如需有關如何使用 InputChanges 的詳細資訊,請參閱 增量任務 的對應文件。
以下是一個計算 Java 原始碼檔案中程式碼行數的增量轉換範例
abstract class CountLoc : TransformAction<TransformParameters.None> {
@get:Inject (1)
abstract val inputChanges: InputChanges
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputArtifact
abstract val input: Provider<FileSystemLocation>
override
fun transform(outputs: TransformOutputs) {
val outputDir = outputs.dir("${input.get().asFile.name}.loc")
println("Running transform on ${input.get().asFile.name}, incremental: ${inputChanges.isIncremental}")
inputChanges.getFileChanges(input).forEach { change -> (2)
val changedFile = change.file
if (change.fileType != FileType.FILE) {
return@forEach
}
val outputLocation = outputDir.resolve("${change.normalizedPath}.loc")
when (change.changeType) {
ChangeType.ADDED, ChangeType.MODIFIED -> {
println("Processing file ${changedFile.name}")
outputLocation.parentFile.mkdirs()
outputLocation.writeText(changedFile.readLines().size.toString())
}
ChangeType.REMOVED -> {
println("Removing leftover output file ${outputLocation.name}")
outputLocation.delete()
}
}
}
}
}
abstract class CountLoc implements TransformAction<TransformParameters.None> {
@Inject (1)
abstract InputChanges getInputChanges()
@PathSensitive(PathSensitivity.RELATIVE)
@InputArtifact
abstract Provider<FileSystemLocation> getInput()
@Override
void transform(TransformOutputs outputs) {
def outputDir = outputs.dir("${input.get().asFile.name}.loc")
println("Running transform on ${input.get().asFile.name}, incremental: ${inputChanges.incremental}")
inputChanges.getFileChanges(input).forEach { change -> (2)
def changedFile = change.file
if (change.fileType != FileType.FILE) {
return
}
def outputLocation = new File(outputDir, "${change.normalizedPath}.loc")
switch (change.changeType) {
case ADDED:
case MODIFIED:
println("Processing file ${changedFile.name}")
outputLocation.parentFile.mkdirs()
outputLocation.text = changedFile.readLines().size()
case REMOVED:
println("Removing leftover output file ${outputLocation.name}")
outputLocation.delete()
}
}
}
}
1 | 注入 InputChanges |
2 | 查詢輸入人工產出中的變更 |