如果在使用依赖之前,想对其包含的文件进行修改,该怎么办?

例如,你可能想解压缩文件、调整 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 注解进行标注。

然后,你实现 TransformAction 接口中的 transform(TransformOutputs) 方法。该方法的实现定义了转换被触发时应执行的操作。该方法有一个 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 会自动运行已注册的转换来满足解析请求。

由于不存在提供请求格式构件的变体(因为没有变体包含值为 "transformed-jar"artifactType 属性),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 会根据需要自动选择构件转换来满足解析请求。

要运行构件转换,你可以配置一个自定义的构件视图(Artifact View)来请求目标组件的任何变体都未暴露的构件集。

解析 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" 的构件。这是通过创建一个过滤 ARTIFACT_TYPE_ATTRIBUTE = "stub" 的构件的 ArtifactView 来实现的。

理解构件转换链

当 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=a, B=bC=c,而变体 V2 包含属性 A=a, B=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 请求运行时 classpath 被压缩
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,并且转换操作为每个输入构件注册一个压缩后的输出构件 <artifact-name>-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

参数必须使用受管属性(managed properties)表示,并且参数类型必须是受管类型(managed type)。您可以使用接口或抽象类来声明 getter 方法,Gradle 将生成实现。所有 getter 方法都需要具有适当的输入注解,如增量构建注解表中所述。

这里是 Minify 转换的实现,它通过只保留 JAR 中的特定类来减小 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 当无需压缩时使用未更改的输入构件

注意看如何在 transform() 方法中通过 TransformAction.getParameters() 获取参数。transform() 方法的实现通过使用 TransformOutputs.file() 请求一个用于存放压缩后 JAR 的位置,然后在此位置创建压缩后的 JAR。

请记住,输入构件是一个依赖,它可能有自己的依赖。假设您的构件转换需要访问这些传递性依赖。在这种情况下,它可以声明一个返回 FileCollection 的抽象 getter 方法,并使用 @InputArtifactDependencies 注解进行标注。当您的转换运行时,Gradle 将通过实现该 getter 方法,将传递性依赖注入到 FileCollection 属性中。

请注意,在 transform 中使用输入 artifact 依赖项会对性能产生影响;仅在需要时注入它们。

带缓存的 Artifact Transforms

Artifact Transforms 可以利用构建缓存来存储其输出,并在已知结果时避免重新运行其 transform 操作。

要使构建缓存能够存储 artifact transform 的结果,请在 action 类上添加 @CacheableTransform 注解。

对于可缓存的 transforms,您必须为其 @InputArtifact 属性以及任何用 @InputArtifactDependencies 标记的属性,使用诸如 @PathSensitive 等规范化注解进行标注。

以下示例演示了一个更复杂的 transform,它将 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 声明 transform 是可缓存的
2 转换参数的接口
3 为每个参数声明输入类型
4 为输入 artifact 声明一个规范化
5 注入输入 artifact 依赖项
6 使用参数

注意,要重定位的类是通过检查输入 artifact 及其依赖项的包来确定的。此外,transform 确保外部 classpath 上 JAR 文件中包含的包不会被重定位。

增量 Artifact Transforms

增量任务类似,Artifact Transforms 可以通过仅处理自上次执行以来发生更改的文件来避免一些工作。这是通过使用 InputChanges 接口实现的。

对于 Artifact Transforms,只有输入 artifact 是增量输入;因此,transform 只能查询该处的更改。要在 transform action 中使用 InputChanges,请将其注入到 action 中。

有关如何使用 InputChanges 的更多信息,请参阅增量任务的相应文档。

以下是一个增量 transform 的示例,它计算 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 查询输入 artifact 中的更改

此 transform 只会在自上次运行以来发生更改的源文件上运行,因为否则不需要重新计算行数。

注册 Artifact Transforms

您需要注册 artifact transform actions,并在必要时提供参数,以便在解析依赖项时可以选择它们。

要注册 artifact transform,您必须在 dependencies {} 块内使用 registerTransform()

使用 registerTransform() 时需要考虑几个要点

  • 至少需要一个 from 和一个 to 属性。

  • 每个 to 属性必须有相应的 from 属性。

  • 可以包含额外的 from 属性,但这些属性没有相应的 to 属性。

  • transform action 本身可以有配置选项。您可以使用 parameters {} 块来配置它们。

  • 您必须在具有将被解析的 configuration 的项目上注册 transform。

  • 您可以将任何实现 TransformAction 的类型提供给 registerTransform() 方法。

例如,假设您想解压缩一些依赖项并将解压缩后的目录和文件放在 classpath 上。您可以通过注册一个类型为 Unzip 的 artifact transform action 来实现,如下所示

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 {} 块向 Minify transform 提供要在压缩后的 JAR 中保留的类

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 Transforms

在命令行上,Gradle 运行的是 tasks;而不是 Artifact Transforms:./gradlew build。那么,transforms 是如何以及何时运行的呢?

Gradle 执行 transform 有两种方式

  1. 针对项目依赖项的 Artifact Transforms 执行可以在任务执行之前发现,因此可以安排在任务执行之前进行。

  2. 针对外部模块依赖项的 Artifact Transforms 执行无法在任务执行之前发现,因此被安排在任务执行内部进行。

在声明良好的构建中,可以在任务配置期间完全发现项目依赖项,并在任务执行调度之前进行。如果项目依赖项声明不正确(例如,缺少任务输入),则 transform 执行将在任务内部进行。

重要的是要记住 Artifact Transforms

  • 只有在不存在匹配的变体来满足请求时才会运行

  • 可以并行运行

  • 如果可能,将不会重新运行(如果多个解析请求需要在同一 artifacts 上执行相同的 transform,并且该 transform 是可缓存的,则该 transform 只会运行一次,并且在后续的每个请求中都从缓存中获取结果)

`TransformAction` 仅在输入 artifacts 存在时才会被实例化和运行。如果在 transform 的输入 variant 中没有 artifacts,该 transform 将被跳过。这可能会发生在 action 链的中间,导致所有后续 transforms 被跳过。