不同类型的配置 所述,同一个依赖项可能存在不同的变体。例如,外部 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-apijava-runtime 变体仅在传递依赖项方面有所不同,并且两者都使用相同的工件——JAR 文件。对于项目依赖项,java-api,classesjava-api,jars 变体具有相同的传递依赖项和不同的工件——分别为类目录和 JAR 文件。

Gradle 通过其 属性 集来唯一地标识依赖项的变体。依赖项的 java-api 变体是通过 org.gradle.usage 属性标识的变体,其值为 java-api

当 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 属性:truefalseminified 属性表示依赖项的一个变体,其中删除了不必要的类文件。有一个注册的工件转换,可以将 minifiedfalse 转换为 true。当为依赖项请求 minified=true 时,并且只有具有 minified=false 的变体,则 Gradle 会选择注册的缩小转换。缩小转换能够将具有 minified=false 的依赖项的工件转换为具有 minified=true 的工件。

在所有找到的转换链中,Gradle 会尝试选择最佳的链。

  • 如果只有一个转换链,则选择该链。

  • 如果存在两个转换链,其中一个链是另一个链的后缀,则选择该链。

  • 如果存在最短的转换链,则选择该链。

  • 在所有其他情况下,选择失败并报告错误。

当已存在与请求属性匹配的依赖项变体时,Gradle 不会尝试选择工件转换。

artifactType 属性是特殊的,因为它只存在于已解析的工件中,而不存在于依赖项中。因此,任何只修改 artifactType 的转换在解析仅使用 artifactType 作为请求属性的配置时永远不会被选择。它只会在使用 ArtifactView 时被考虑。

选择所需的工件转换后,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 工件中,并在所有可解析配置中请求它。

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.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"))
}
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.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 配置)时会发生什么。观察到 Gradle 在 resolveRuntimeClasspath 任务开始之前转换项目依赖项。Gradle 在执行 resolveRuntimeClasspath 任务时转换二进制依赖项。

解析 runtimeClasspath 配置时的输出
> 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() 来注册每个输出工件。

您只能向 dirfile 方法提供两种类型的路径

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

  • 相对路径。

Gradle 使用绝对路径作为输出工件的位置。例如,如果输入工件是已展开的 WAR 文件,则转换操作可以对 WEB-INF/lib 目录中的所有 jar 文件调用 TransformOutputs.file()。然后,转换的输出将是 Web 应用程序的库 JAR 文件。

对于相对路径,dir()file() 方法会向转换操作返回一个工作区。转换操作的实现需要在提供的 workspace 位置创建转换后的工件。

输出工件将按照注册顺序替换已转换变体中的输入工件。例如,如果配置包含工件 lib1.jarlib2.jarlib3.jar,并且转换操作为输入工件注册了最小化输出工件 <artifact-name>-min.jar,则转换后的配置将包含工件 lib1-min.jarlib2-min.jarlib3-min.jar

以下是 Unzip 转换的实现,它通过解压缩将 JAR 文件转换为 classes 目录。Unzip 转换不需要任何参数。请注意,实现如何使用 @InputArtifact 将要转换的工件注入到操作中。它通过使用 TransformOutputs.dir() 请求解压缩类的目录,然后将 JAR 文件解压缩到该目录中。

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)                              (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)                              (3)
        unzipTo(input, unzipDir)                                            (4)
    }

    private static void unzipTo(File zipFile, File unzipDir) {
        // implementation...
    }
}
1 如果转换不使用参数,请使用 TransformParameters.None
2 注入输入工件
3 请求解压缩文件的输出位置
4 执行转换的实际工作

工件转换可能需要参数,例如确定某些过滤器的String,或用于支持输入工件转换的某些文件集合。为了将这些参数传递给转换操作,您需要使用所需参数定义一个新类型。该类型需要实现标记接口TransformParameters。参数必须使用托管属性表示,参数类型必须是托管类型。您可以使用声明 getter 的接口或抽象类,Gradle 将生成实现。所有 getter 都需要具有适当的输入注释,请参见增量构建注释表。

您可以在开发自定义 Gradle 类型中了解更多关于实现工件转换参数的信息。

以下是Minify转换的实现,它通过仅保留其中的某些类来使 JAR 更小。Minify转换需要将要保留的类作为参数。请注意,您如何在transform()方法中通过TransformAction.getParameters()获取参数。transform()方法的实现通过使用TransformOutputs.file()请求缩小 JAR 的位置,然后在该位置创建缩小 JAR。

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 在不需要缩小时使用未更改的输入工件

请记住,输入工件是一个依赖项,它可能具有自己的依赖项。如果您的工件转换需要访问这些传递依赖项,它可以声明一个返回FileCollection的抽象 getter,并使用@InputArtifactDependencies对其进行注释。当您的转换运行时,Gradle 将通过实现 getter 将传递依赖项注入到该FileCollection属性中。请注意,在转换中使用输入工件依赖项会影响性能,只有在真正需要时才注入它们。

此外,工件转换可以使用构建缓存来处理其输出。要为工件转换启用构建缓存,请在操作类上添加@CacheableTransform注释。对于可缓存的转换,您必须使用规范化注释(例如@PathSensitive)对其@InputArtifact属性(以及任何使用@InputArtifactDependencies标记的属性)进行注释。

以下示例展示了一个更复杂的转换。它将 JAR 中的一些选定类移动到不同的包中,并重写移动类和所有使用移动类的类的字节码(类重定位)。为了确定要重定位的类,它会查看输入工件的包和输入工件的依赖项。它也不会重定位外部类路径中 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 使用参数

注册工件转换

您需要注册工件转换操作,并在必要时提供参数,以便在解析依赖项时可以选择它们。

为了注册工件转换,您必须在dependencies {}块中使用registerTransform()

在使用registerTransform()时,需要考虑以下几点:

  • fromto属性是必需的。

  • 转换操作本身可以具有配置选项。您可以使用parameters {}块来配置它们。

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

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

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

build.gradle.kts
val artifactType = Attribute.of("artifactType", String::class.java)

dependencies {
    registerTransform(Unzip::class) {
        from.attribute(artifactType, "jar")
        to.attribute(artifactType, "java-classes-directory")
    }
}
build.gradle
def artifactType = Attribute.of('artifactType', String)

dependencies {
    registerTransform(Unzip) {
        from.attribute(artifactType, 'jar')
        to.attribute(artifactType, 'java-classes-directory')
    }
}

另一个示例是,您想通过只保留其中一些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
        }
    }
}

实现增量工件转换

类似于 增量任务,工件转换可以通过仅处理上次执行中更改的文件来避免工作。这是通过使用 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 查询输入工件中的更改