如果您想在使用依賴項中的檔案之前對其進行更改,該怎麼辦?

例如,您可能想要解壓縮壓縮檔、調整 JAR 的內容,或從包含多個檔案的依賴項中刪除不必要的檔案,然後再在任務中使用結果。

Gradle 具有一項稱為構件轉換的內建功能。透過構件轉換,您可以修改、新增、移除依賴項中包含的檔案集(或構件)- 如 JAR 檔案。這是在解析構件時的最後一步完成的,在任務或 IDE 等工具可以使用構件之前。

構件轉換總覽

每個組件都公開一組變體,其中每個變體都由一組 屬性(即,鍵值對,例如 debug=true)識別。

當 Gradle 解析配置時,它會查看每個依賴項,將其解析為組件,並從該組件中選擇與請求的屬性匹配的對應變體。如果組件沒有匹配的變體,則解析失敗,除非 Gradle 可以建構一系列轉換,將現有構件修改為建立有效的匹配(而不更改其傳遞依賴關係)。

構件轉換是一種在建置過程中將一種構件類型轉換為另一種類型的機制。它們為消費者提供了一種高效且靈活的機制,用於將給定生產者的構件轉換為所需的格式,而無需生產者以該格式公開變體。

artifact transform 2

構件轉換很像任務。它們是具有一些輸入和輸出的工作單元。諸如 UP-TO-DATE 和快取之類的機制也適用於轉換。

artifact transform 1

任務和轉換之間的主要區別在於它們在 Gradle 配置和執行建置時執行的動作鏈中如何排程和放置。在高層次上,轉換始終在任務之前運行,因為它們在依賴關係解析期間執行。轉換在構件成為任務的輸入之前修改構件。

以下是關於如何建立和使用構件轉換的簡要概述

artifact transform 3
  1. 實作轉換:您可以透過建立一個實作 TransformAction 介面的類別來定義構件轉換。此類別指定應如何將輸入構件轉換為輸出構件。

  2. 宣告請求屬性:屬性(用於描述組件不同變體的鍵值對),例如 org.gradle.usage=java-apiorg.gradle.usage=java-runtime,用於指定所需的構件格式或類型。

  3. 註冊轉換:您可以使用 registerTransform() 方法在 dependencies 區塊中註冊轉換。此方法告訴 Gradle,轉換可用於修改具有給定 "from" 屬性的任何變體的構件。它還告訴 Gradle,新的 "to" 屬性集將描述結果構件的格式或類型。

  4. 使用轉換:當解析需要組件中尚不存在的構件時(因為沒有實際構件具有與請求屬性相容的屬性),Gradle 不會直接放棄!相反,Gradle 首先自動搜尋所有已註冊的轉換,以查看是否可以建構一系列轉換,最終產生匹配項。如果 Gradle 找到這樣的鏈,它會依序執行每個轉換,並交付轉換後的構件作為結果。

1. 實作轉換

轉換通常編寫為實作 TransformAction 介面的抽象類別。它可以選擇性地在單獨的介面中定義參數。

每個轉換都恰好有一個輸入構件。它必須使用 @InputArtifact 註釋進行註釋。

然後,您實作 transform(TransformOutputs) 方法,該方法來自 TransformAction 介面。此方法的實作定義了觸發轉換時應執行的操作。該方法具有一個 TransformOutputs 參數,您可以使用它來告訴 Gradle 轉換產生哪些構件。

在此,MyTransform 是自訂轉換動作,將 jar 構件轉換為 transformed-jar 構件

build.gradle.kts
abstract class MyTransform : TransformAction<TransformParameters.None> {
    @get:InputArtifact
    abstract val inputArtifact: Provider<FileSystemLocation>

    override fun transform(outputs: TransformOutputs) {
        val inputFile = inputArtifact.get().asFile
        val outputFile = outputs.file(inputFile.name.replace(".jar", "-transformed.jar"))
        // Perform transformation logic here
        inputFile.copyTo(outputFile, overwrite = true)
    }
}
build.gradle
abstract class MyTransform implements TransformAction<TransformParameters.None> {
    @InputArtifact
    abstract Provider<FileSystemLocation> getInputArtifact()

    @Override
    void transform(TransformOutputs outputs) {
        def inputFile = inputArtifact.get().asFile
        def outputFile = outputs.file(inputFile.name.replace(".jar", "-transformed.jar"))
        // Perform transformation logic here
        inputFile.withInputStream { input ->
            outputFile.withOutputStream { output ->
                output << input
            }
        }
    }
}

2. 宣告請求屬性

屬性指定依賴項的所需屬性。

在此,我們指定 runtimeClasspath 配置需要 transformed-jar 格式

build.gradle.kts
configurations.named("runtimeClasspath") {
    attributes {
        attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "transformed-jar")
    }
}
build.gradle
configurations.named("runtimeClasspath") {
    attributes {
        attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "transformed-jar")
    }
}

3. 註冊轉換

必須使用 dependencies.registerTransform() 方法註冊轉換。

在此,我們的轉換在 dependencies 區塊中註冊

build.gradle.kts
dependencies {
    registerTransform(MyTransform::class) {
        from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "jar")
        to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "transformed-jar")
    }
}
build.gradle
dependencies {
    registerTransform(MyTransform) {
        from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "jar")
        to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "transformed-jar")
    }
}

"To" 屬性用於描述此轉換可用作輸入的構件的格式或類型,而 "from" 屬性用於描述其作為輸出產生的構件的格式或類型。

4. 使用轉換

在建置期間,如果沒有直接可用的匹配項,Gradle 會自動執行已註冊的轉換以滿足解析請求。

由於不存在提供請求格式構件的變體(因為沒有一個包含 artifactType 屬性且值為 "transformed-jar"),Gradle 會嘗試建構一系列轉換,以提供與請求屬性匹配的構件。

Gradle 的搜尋找到了 MyTransform,它已註冊為產生請求的格式,因此它將自動運行。執行此轉換動作會修改現有來源變體的構件,以產生新的構件,這些構件以請求的格式交付給消費者。

Gradle 在此過程中產生組件的「虛擬構件集」。

了解構件轉換

依賴項可以具有不同的變體,本質上是同一依賴項的不同版本或形式。這些變體可以各自提供不同的構件集,旨在滿足不同的用例,例如編譯程式碼、瀏覽文件或執行應用程式。

每個變體都由一組 屬性 識別。屬性是鍵值對,用於描述變體的特定特性。

artifact transform 4

讓我們使用以下範例,其中外部 Maven 依賴項具有兩個變體

表 1. Maven 依賴項
變體 描述

org.gradle.usage=java-api

用於針對依賴項進行編譯。

org.gradle.usage=java-runtime

用於執行使用依賴項的應用程式。

專案依賴項具有更多變體

表 2. 專案依賴項
變體 描述

org.gradle.usage=java-api org.gradle.libraryelements=classes

代表類別目錄。

org.gradle.usage=java-api org.gradle.libraryelements=jar

代表封裝的 JAR 檔案,包含類別和資源。

依賴項的變體可能在其傳遞依賴關係或它們包含的構件集中有所不同,或兩者兼而有之。

例如,Maven 依賴項的 java-apijava-runtime 變體僅在其傳遞依賴關係中有所不同,並且都使用相同的構件 — JAR 檔案。對於專案依賴項,java-api,classesjava-api,jars 變體具有相同的傳遞依賴關係,但不同的構件 — 分別是 classes 目錄和 JAR 檔案。

當 Gradle 解析配置時,它會使用定義的屬性來選擇每個依賴項的適當變體。Gradle 用於確定要選擇哪個變體的屬性稱為請求的屬性

例如,如果配置請求 org.gradle.usage=java-apiorg.gradle.libraryelements=classes,Gradle 將選擇每個依賴項中與這些屬性匹配的變體(在本例中,用於在編譯期間用作 API 的類別目錄)。匹配不必完全相同,因為某些屬性值可以被 Gradle 識別為與其他值相容,並在 匹配 期間互換使用。

有時,依賴項可能沒有具有與請求屬性匹配的屬性的變體。在這種情況下,Gradle 可以透過修改一個變體的構件而不更改其傳遞依賴關係,將其構件轉換為另一個「虛擬構件集」。

當依賴項的變體已經存在且與請求的屬性匹配時,Gradle 將不會嘗試選擇或運行構件轉換。

例如,如果請求的變體是 java-api,classes,但依賴項僅具有 java-api,jar,Gradle 可以透過使用已使用這些屬性註冊的構件轉換來解壓縮 JAR 檔案,從而將其潛在地轉換為 classes 目錄。

Gradle 將轉換應用於構件,而不是變體。

執行構件轉換

Gradle 會根據需要自動選擇構件轉換,以滿足解析請求。

若要執行構件轉換,您可以配置自訂 構件視圖,以請求目標組件的任何變體都未公開的構件集。

當解析 ArtifactView 時,Gradle 將根據視圖中請求的屬性搜尋適當的構件轉換。Gradle 將對目標組件的變體中找到的原始構件執行這些轉換,以產生與視圖中的屬性相容的結果。

在以下範例中,TestTransform 類別定義了一個轉換,該轉換已註冊以將類型為 "jar" 的構件處理為類型為 "stub" 的構件

build.gradle.kts
// The TestTransform class implements TransformAction,
// transforming input JAR files into text files with specific content
abstract class TestTransform : TransformAction<TransformParameters.None> {
    @get:InputArtifact
    abstract val inputArtifact: Provider<FileSystemLocation>

    override fun transform(outputs: TransformOutputs) {
        val outputFile = outputs.file("transformed-stub.txt")
        outputFile.writeText("Transformed from ${inputArtifact.get().asFile.name}")
    }
}

// The transform is registered to convert artifacts from the type "jar" to "stub"
dependencies {
    registerTransform(TestTransform::class.java) {
        from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "jar")
        to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "stub")
    }
}

dependencies {
    runtimeOnly("com.github.javafaker:javafaker:1.0.2")
}

// The testArtifact task queries and prints the attributes of resolved artifacts,
// showing the type conversion in action.
tasks.register("testArtifact") {
    val resolvedArtifacts = configurations.runtimeClasspath.get().incoming.artifactView {
        attributes {
            attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "stub")
        }
    }.artifacts.resolvedArtifacts

    resolvedArtifacts.get().forEach {
        println("Resolved artifact variant:")
        println("- ${it.variant}")
        println("Resolved artifact attributes:")
        println("- ${it.variant.attributes}")
        println("Resolved artifact type:")
        println("- ${it.variant.attributes.getAttribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE)}")
    }
}
build.gradle
// The TestTransform class implements TransformAction,
// transforming input JAR files into text files with specific content
abstract class TestTransform implements TransformAction<TransformParameters.None> {
    @InputArtifact
    abstract Provider<FileSystemLocation> getInputArtifact()

    @Override
    void transform(TransformOutputs outputs) {
        def outputFile = outputs.file("transformed-stub.txt")
        outputFile.text = "Transformed from ${getInputArtifact().get().asFile.name}"
    }
}

// The transform is registered to convert artifacts from the type "jar" to "stub"
dependencies {
    registerTransform(TestTransform) {
        from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "jar")
        to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "stub")
    }
}

dependencies {
    runtimeOnly("com.github.javafaker:javafaker:1.0.2")
}

// The testArtifact task queries and prints the attributes of resolved artifacts,
// showing the type conversion in action.
tasks.register("testArtifact") {
    def resolvedArtifacts = configurations.runtimeClasspath.incoming.artifactView {
        attributes {
            attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "stub")
        }
    }.artifacts.resolvedArtifacts

    resolvedArtifacts.get().each {
        println "Resolved artifact variant:"
        println "- ${it.variant}"
        println "Resolved artifact attributes:"
        println "- ${it.variant.attributes}"
        println "Resolved artifact type:"
        println "- ${it.variant.attributes.getAttribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE)}"
    }
}

testArtifact 任務使用 runtimeClasspath 配置解析類型為 "stub" 的構件。這是透過建立一個 ArtifactView 來實現的,該視圖篩選具有 ARTIFACT_TYPE_ATTRIBUTE = "stub" 的構件。

了解構件轉換鏈

當 Gradle 解析配置且圖形中的變體不具有具有請求屬性的構件集時,它會嘗試尋找一個或多個可以循序運行的構件轉換鏈,以建立所需的構件集。此過程稱為構件轉換選擇

artifact transform 5

構件轉換選擇過程:

  1. 從請求的屬性開始:

    • Gradle 從解析的配置上指定的屬性開始,附加在 ArtifactView 上指定的任何屬性,最後附加直接在依賴項上宣告的任何屬性。

    • 它會考量所有修改這些屬性的已註冊轉換。

  2. 尋找通往現有變體的路徑:

    • Gradle 向後工作,嘗試尋找從請求的屬性到現有變體的路徑。

例如,如果 minified 屬性的值為 truefalse,並且轉換可以將 minified=false 更改為 minified=true,則如果僅 minified=false 變體可用但請求了 minified=true,Gradle 將使用此轉換。

Gradle 使用以下過程選擇轉換鏈

  • 如果只有一個可能的鏈產生請求的屬性,則選擇該鏈。

  • 如果有多个这样的链,则只考虑最短的链。

  • 如果仍然有多个链保持同等适用但产生不同的结果,则选择失败,并报告错误。

  • 如果所有剩余的链都产生相同的结果属性集,则 Gradle 任意选择一个。

多个链如何产生不同的适用结果?转换可以一次更改多个属性。转换链的适用结果是具有与请求属性兼容的属性的结果。但是结果可能还包含其他属性,这些属性未被请求,并且与结果无关。

例如:如果请求属性 A=aB=b,并且变体 V1 包含属性 A=aB=bC=c,而变体 V2 包含属性 A=aB=bD=d,那么由于 AB 的所有值都相同(或兼容),因此 V1V2 都将满足请求。

完整範例

讓我們繼續探索上面開始的 minified 範例:配置請求 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

  • 專案 producer 依賴項,具有變體

    • 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 使用 minify 轉換將 minified=false 變體轉換為 minified=true

  • 對於 guava,Gradle 轉換

    • org.gradle.usage=java-runtime, org.gradle.libraryelements=jar, minified=false

    • org.gradle.usage=java-runtime, org.gradle.libraryelements=jar, minified=true.

  • 對於 producer,Gradle 轉換

    • 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,然後運行轉換以縮小它。

  • 這些任務和轉換在可能的情況下並行執行。

若要設定 minified 屬性以使上述工作正常運作,您必須將屬性新增至正在產生的所有 JAR 變體,並將其新增至正在請求的所有可解析配置。您還應在屬性架構中註冊該屬性。

build.gradle.kts
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.runtimeClasspath.configure {
    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"))
}
build.gradle
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.runtimeClasspath {
    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 請求縮小運行時類別路徑
4 新增將被轉換的依賴項
5 新增需要轉換後構件的任務

您現在可以看到當我們運行 resolveRuntimeClasspath 任務(解析 runtimeClasspath 配置)時會發生什麼。Gradle 在 resolveRuntimeClasspath 任務開始之前轉換專案依賴項。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() 方法,該方法將輸入構件轉換為零個、一個或多個輸出構件。

大多數構件轉換都是一對一的,因此 transform 方法將用於將 from 變體中包含的每個輸入構件轉換為恰好一個輸出構件。

構件轉換動作的實作需要透過呼叫 TransformOutputs.dir()TransformOutputs.file() 來註冊每個輸出構件。

您可以向 dirfile 方法提供兩種路徑類型

  • 輸入構件的絕對路徑或輸入構件內的路徑(對於輸入目錄)。

  • 相對路徑。

Gradle 使用絕對路徑作為輸出構件的位置。例如,如果輸入構件是展開的 WAR,則轉換動作可以為 WEB-INF/lib 目錄中的所有 JAR 檔案呼叫 TransformOutputs.file()。轉換的輸出將是 Web 應用程式的函式庫 JAR。

對於相對路徑,dir()file() 方法會傳回轉換動作的工作區。轉換動作需要在提供的工作區的位置建立轉換後的構件。

輸出構件會取代轉換後變體中的輸入構件,順序與它們註冊的順序相同。例如,如果選定的輸入變體包含構件 lib1.jarlib2.jarlib3.jar,並且轉換動作為每個輸入構件註冊一個縮小的輸出構件 <構件名稱>-min.jar,則轉換後的配置將由構件 lib1-min.jarlib2-min.jarlib3-min.jar 組成。

以下是 Unzip 轉換的實作,它將 JAR 檔案解壓縮到 classes 目錄中。Unzip 轉換不需要任何參數

build.gradle.kts
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 + "-unzipped")                (3)
        unzipTo(input, unzipDir)                                            (4)
    }

    private fun unzipTo(zipFile: File, unzipDir: File) {
        // implementation...
    }
}
build.gradle
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 + "-unzipped")                (3)
        unzipTo(input, unzipDir)                                            (4)
    }

    private static void unzipTo(File zipFile, File unzipDir) {
        // implementation...
    }
}
1 如果轉換不使用參數,請使用 TransformParameters.None
2 注入輸入構件
3 請求解壓縮檔案的輸出位置
4 執行轉換的實際工作

請注意實作如何使用 @InputArtifact 將要轉換的構件注入到動作類別中,以便可以在 transform 方法中存取它。此方法透過使用 TransformOutputs.dir() 請求解壓縮類別的目錄,然後將 JAR 檔案解壓縮到此目錄中。

具有參數的構件轉換

構件轉換可能需要參數,例如用於篩選的 String 或用於支援輸入構件轉換的檔案集合。若要將這些參數傳遞到轉換動作,您必須使用所需的參數定義一個新類型。此類型必須實作標記介面 TransformParameters

參數必須使用 受管理屬性 表示,並且參數類型必須是 受管理類型。您可以使用介面或抽象類別來宣告 getter,Gradle 將產生實作。所有 getter 都需要具有適當的輸入註釋,如 增量建置註釋 表格中所述。

以下是 Minify 轉換的實作,它透過僅保留某些類別來縮小 JAR 的大小。Minify 轉換需要知道每個 JAR 中要保留的類別,這些類別在其參數中以 Map 屬性提供

build.gradle.kts
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 ...
    }
}
build.gradle
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 當不需要縮小時,使用未更改的輸入構件

請注意,您可以透過 TransformAction.getParameters()transform() 方法中取得參數。transform() 方法的實作透過使用 TransformOutputs.file() 請求縮小的 JAR 的位置,然後在此位置建立縮小的 JAR。

請記住,輸入構件是一個依賴項,它可能具有自己的依賴關係。假設您的構件轉換需要存取這些傳遞依賴關係。在這種情況下,它可以宣告一個抽象 getter,該 getter 傳回 FileCollection 並使用 @InputArtifactDependencies 進行註釋。當您的轉換運行時,Gradle 將透過實作 getter 將傳遞依賴關係注入到 FileCollection 屬性中。

請注意,在轉換中使用輸入構件依賴關係會產生效能影響;僅在需要時才注入它們。

具有快取的構件轉換

構件轉換可以使用 建置快取 來儲存其輸出,並在已知結果時避免重新運行其轉換動作。

若要啟用建置快取以儲存構件轉換的結果,請在動作類別上新增 @CacheableTransform 註釋。

對於可快取的轉換,您必須使用正規化註釋(例如 @PathSensitive)來註釋其 @InputArtifact 屬性 — 以及任何標記有 @InputArtifactDependencies 的屬性。

以下範例示範了更複雜的轉換,該轉換將 JAR 中特定類別重新定位到不同的套件。此過程涉及重新編寫重新定位的類別和引用它們的任何類別的位元組碼(類別重新定位)

build.gradle.kts
@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()
    }
}
build.gradle
@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 使用參數

請注意,要重新定位的類別由檢查輸入構件及其依賴項的套件來確定。此外,轉換可確保外部類別路徑上的 JAR 檔案中包含的套件不會重新定位。

增量構件轉換

增量任務 類似,構件轉換可以透過僅處理自上次執行以來已更改的檔案來避免某些工作。這是透過使用 InputChanges 介面完成的。

對於構件轉換,只有輸入構件是增量輸入;因此,轉換只能查詢該處的變更。若要在轉換動作中使用 InputChanges,請將其注入到動作中。

有關如何使用 InputChanges 的更多資訊,請參閱 增量任務 的對應文件。

以下是增量轉換的範例,該轉換計算 Java 原始碼檔案中的程式碼行數

build.gradle.kts
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()
                }
            }
        }
    }
}
build.gradle
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 查詢輸入構件中的變更

此轉換將僅在自上次運行以來已更改的原始碼檔案上運行,否則不需要重新計算行數。

註冊構件轉換

您需要註冊構件轉換動作,並在必要時提供參數,以便可以在解析依賴關係時選擇它們。

若要註冊構件轉換,您必須在 dependencies {} 區塊中使用 registerTransform()

使用 registerTransform() 時,需要考慮以下幾點

  • 至少需要一個 fromto 屬性。

  • 每個 to 屬性都必須具有對應的 from 屬性。

  • 可以包含額外的 from 屬性,這些屬性需要有對應的 to 屬性。

  • 轉換動作本身可以有組態選項。您可以使用 parameters {} 區塊來設定它們。

  • 您必須在將解析組態的專案上註冊轉換。

  • 您可以提供任何實作 TransformAction 的類型給 registerTransform() 方法。

例如,假設您想要解壓縮一些 dependencies,並將解壓縮後的目錄和檔案放在類別路徑上。您可以透過註冊類型為 Unzip 的 artifact 轉換動作來做到這一點,如下所示

build.gradle.kts
dependencies {
    registerTransform(Unzip::class.java) {
        from.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named<LibraryElements>(LibraryElements.JAR))
        from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.JAR_TYPE)
        to.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named<LibraryElements>(LibraryElements.CLASSES_AND_RESOURCES))
        to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.DIRECTORY_TYPE)
    }
}
build.gradle
dependencies {
    registerTransform(Unzip) {
        from.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.JAR))
        from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.JAR_TYPE)
        to.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.CLASSES_AND_RESOURCES))
        to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.DIRECTORY_TYPE)
    }
}

另一個例子是您想要透過僅保留某些 class 檔案來縮小 JAR 檔。請注意使用 parameters {} 區塊來提供要保留在縮小後的 JAR 檔中的類別給 Minify 轉換

build.gradle.kts
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
        }
    }
}
build.gradle
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
        }
    }
}

執行 Artifact 轉換

在命令列中,Gradle 執行的是 tasks;而不是 Artifact Transforms:./gradlew build. 那麼它何時以及如何執行轉換呢?

Gradle 執行轉換的方式有兩種

  1. 專案 dependencies 的 Artifact Transforms 執行可以在 task 執行之前被發現,因此可以在 task 執行之前排程。

  2. 外部模組 dependencies 的 Artifact Transforms 執行無法在 task 執行之前被發現,因此會在 task 執行期間排程。

在良好宣告的建置中,專案 dependencies 可以在 task 執行排程之前的 task 組態期間完全被發現。如果專案 dependency 宣告不佳(例如,缺少 task 輸入),轉換執行將會在 task 內部發生。

請務必記住 Artifact Transforms

  • 只有在沒有符合請求的變體存在時才會執行

  • 可以平行執行

  • 如果可能,將不會重新執行(如果多個解析請求需要在相同的 artifacts 上執行相同的轉換,且該轉換是可快取的,則該轉換只會執行一次,且後續每個請求都會從快取中取得結果)

`TransformAction` 只有在輸入 artifacts 存在時才會被實例化和執行。如果轉換的輸入變體中沒有 artifacts 存在,則該轉換將會被略過。這可能會發生在動作鏈的中間,導致所有後續的轉換都被略過。