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

例如,您可能希望解压缩压缩文件,调整 JAR 的内容,或从包含多个文件的依赖项中删除不必要的文件,然后再在任务中使用结果。

Gradle 具有一个称为构件转换的内置功能。借助构件转换,您可以修改、添加或删除依赖项中包含的文件集(或构件),例如 JAR 文件。这在解析构件的最后一步完成,在任务或 IDE 等工具可以使用构件之前。

构件转换概述

每个组件都公开一组变体 (variants),其中每个变体都由一组属性 (attributes)(即,键值对,例如 debug=true)标识。

当 Gradle 解析配置时,它会查看每个依赖项,将其解析为组件,并从该组件中选择与请求的属性匹配的相应变体。如果组件没有匹配的变体,则解析会失败,除非 Gradle 可以构建一系列转换,将现有构件修改为创建有效匹配项(而不更改其传递依赖项)。

构件转换是一种在构建过程中将一种类型的构件转换为另一种类型的机制。它们为消费者提供了一种高效且灵活的机制,用于将给定生产者的构件转换为所需的格式,而无需生产者以该格式公开变体。

artifact transform 2

构件转换非常像 task。它们是具有一些输入和输出的工作单元。诸如 UP-TO-DATE 和缓存之类的机制也适用于转换。

artifact transform 1

task 和转换之间的主要区别在于它们在 Gradle 配置和运行时如何被调度和放入操作链中。在较高的层面上,转换总是在 task 之前运行,因为它们在依赖解析期间执行。转换在构件成为 task 的输入之前修改构件。

以下是如何创建和使用构件转换的简要概述

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 会生成组件的“虚拟构件集”。

理解构件转换

依赖项可以具有不同的变体 (variants),本质上是同一依赖项的不同版本或形式。这些变体可以各自提供不同的构件集,旨在满足不同的用例,例如编译代码、浏览文档或运行应用程序。

每个变体都由一组属性 (attributes)标识。属性是描述变体特定特征的键值对。

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 文件,从而有可能将 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 task 使用 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 task 以生成 JAR,然后运行转换以缩小它。

  • 这些 task 和转换尽可能并行执行。

要设置 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 添加需要转换后的构件的 task

您现在可以看到当我们运行 resolveRuntimeClasspath task(它解析 runtimeClasspath 配置)时会发生什么。Gradle 在 resolveRuntimeClasspath task 启动之前转换项目依赖项。Gradle 在执行 resolveRuntimeClasspath task 时转换二进制依赖项

$ 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

实现构件转换

与 task 类型类似,构件转换由一个操作和一些可选参数组成。与自定义 task 类型的主要区别在于,操作和参数是作为两个单独的类实现的。

不带参数的构件转换

构件转换操作由实现 TransformAction 的类提供。这样的类实现了 transform() 方法,该方法将输入构件转换为零个、一个或多个输出构件。

大多数构件转换是一对一的,因此 transform 方法将用于将 from 变体中包含的每个输入构件转换为恰好一个输出构件。

构件转换操作的实现需要通过调用 TransformOutputs.dir()TransformOutputs.file() 来注册每个输出构件。

您可以为 dirfile 方法提供两种类型的路径

  • 输入构件的绝对路径或输入构件内(对于输入目录)。

  • 相对路径。

Gradle 使用绝对路径作为输出构件的位置。例如,如果输入构件是 exploded 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

参数必须使用托管属性表示,并且参数类型必须是托管类型。您可以使用接口或抽象类来声明 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 当不需要缩小时,使用未更改的输入构件

请注意如何在 transform() 方法中使用 TransformAction.getParameters() 获取参数。transform() 方法的实现通过使用 TransformOutputs.file() 请求缩小后的 JAR 的位置,然后在该位置创建缩小后的 JAR。

请记住,输入构件是一个依赖项,它可能有自己的依赖项。假设您的构件转换需要访问这些传递依赖项。在这种情况下,它可以声明一个返回 FileCollection 的抽象 getter,并使用 @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 {} 代码块配置它们。

  • 您必须在具有将被解析的配置的项目上注册转换。

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

例如,假设您要解压缩一些依赖项并将解压缩的目录和文件放在类路径上。您可以通过注册类型为 Unzip 的工件转换操作来做到这一点,如下所示

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)
    }
}

另一个例子是您想通过仅保留 JAR 中的一些 class 文件来缩小 JAR。请注意使用 parameters {} 代码块来向 Minify 转换提供要在缩小的 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
        }
    }
}

执行工件转换

在命令行中,Gradle 运行的是任务;而不是工件转换:./gradlew build. 那么它是如何以及何时运行转换的呢?

Gradle 有两种执行转换的方式

  1. 项目依赖项的工件转换执行可以在任务执行之前被发现,因此可以在任务执行之前进行调度。

  2. 外部模块依赖项的工件转换执行无法在任务执行之前被发现,因此在任务执行内部进行调度。

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

重要的是要记住工件转换

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

  • 可以并行运行

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

只有当输入工件存在时,才会实例化和运行 TransformAction。如果转换的输入变体中没有工件,则将跳过该转换。这可能发生在操作链的中间,导致所有后续转换被跳过。