正如不同种类的配置中所描述的,相同的依赖关系可能有不同的变体。例如,外部 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.usagejava-api

当 Gradle 解析配置时,解析的配置上的属性决定了请求的属性。对于配置中的所有依赖项,在解析配置时会选择具有所请求属性的变体。例如,当配置请求org.gradle.usage=java-api, org.gradle.libraryelements=classes项目依赖项时,则选择类目录作为工件。

当依赖项不具有具有所请求属性的变体时,解析配置将失败。有时,可以将依赖项的工件转换为请求的变体,而不更改传递依赖项。例如,解压缩 JAR 会将java-api,jars变体的工件转换为java-api,classes变体。这种变换称为Artifact Transform。 Gradle 允许注册工件转换,当依赖项没有请求的变体时,Gradle 将尝试查找工件转换链来创建变体。

工件变换选择和执行

如上所述,当 Gradle 解析配置并且配置中的依赖项不具有具有所请求属性的变体时,Gradle 会尝试找到工件转换链来创建变体。寻找匹配的工件变换链的过程称为工件变换选择。每个注册的转换都会从一组属性转换为一组属性。例如,unzip 转换可以从 转换org.gradle.usage=java-api, org.gradle.libraryelements=jarsorg.gradle.usage=java-api, org.gradle.libraryelements=classes

为了找到一条链,Gradle 从请求的属性开始,然后将修改某些请求的属性的所有转换视为通向那里的可能路径。向后看,Gradle 尝试使用转换来获取某些现有变体的路径。

例如,考虑minified具有两个值的属性:truefalse。 minified 属性表示删除了不必要的类文件的依赖项的变体。注册了一个工件转换,可以minified从转换falsetrue。当minified=true请求依赖项,并且只有带 的变体时minified=false,Gradle 选择注册的 minify 转换。缩小转换能够将依赖项 的工件转换minified=false为 的工件minified=true

在所有找到的变换链中,Gradle 尝试选择最好的一个:

  • 如果只有一个变换链,则选择它。

  • 如果有两个变换链,并且其中一个是另一个的后缀,则选择它。

  • 如果存在最短变换链,则选择它。

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

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

artifactType属性很特殊,因为它仅存在于已解析的工件上,而不存在于依赖项上。因此,在解析仅具有请求属性的artifactType配置时,永远不会选择任何仅发生变化的转换。仅在使用ArtifactViewartifactType时才会考虑它。

选择所需的工件转换后,Gradle 会解析链中初始转换所需的依赖项变体。一旦 Gradle 通过下载外部依赖项或执行生成工件的任务完成解析变体的工件,Gradle 就会开始使用选定的工件转换链来转换变体的工件。 Gradle 在可能的情况下并行执行转换链。

选择上面的 minify 示例,考虑具有两个依赖项的配置:外部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

使用 minify 转换,Gradle 可以将org.gradle.usage=java-runtime,org.gradle.libraryelements=jar,minified=false的变体转换guavaorg.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

  • 还有一些。

同样,使用 minify 转换,Gradle 可以将org.gradle.usage=java-runtime,org.gradle.libraryelements=jar,minified=false项目的变体转换producerorg.gradle.usage=java-runtime,org.gradle.libraryelements=jar,minified=true,这是所请求的属性。

解决配置后,Gradle 需要下载guavaJAR 并将其缩小。 Gradle 还需要执行producer:jar任务来生成项目的 JAR 工件,然后对其进行缩小。下载和压缩guava.jarproducer: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,则转换操作可以调用目录TransformOutputs.file()中的所有 jar 文件WEB-INF/lib。转换的输出将是 Web 应用程序的库 JAR。

对于相对路径,dir()orfile()方法将工作空间返回给转换操作。转换操作的实现需要在提供的工作空间的位置创建转换后的工件。

输出工件按照注册顺序替换转换后的变体中的输入工件。例如,如果配置由工件lib1.jarlib2.jar、组成lib3.jar,并且转换操作<artifact-name>-min.jar为输入工件注册了缩小的输出工件,则转换后的配置由工件lib1-min.jarlib2-min.jar和组成lib3-min.jar

下面是一个转换的实现Unzip,它通过解压缩 JAR 文件将其转换为类目录。该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需要将类保留为参数。观察方法TransformAction.getParameters()中如何获取参数transform()。该方法的实现transform()通过使用请求缩小的 JAR 的位置TransformOutputs.file(),然后在该位置创建缩小的 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 当不需要缩小时,使用未更改的输入工件

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

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

以下示例显示了更复杂的转换。它将 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 使用参数

注册工件转换

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

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

使用时有几点需要注意registerTransform()

  • from和属性to是必需的。

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

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

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

例如,假设您想要解压一些依赖项并将解压后的目录和文件放在类路径上。您可以通过注册类型为 的工件转换操作来实现此目的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')
    }
}

另一个例子是,您希望通过仅保留classJAR 中的一些文件来缩小 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
        }
    }
}

实施增量工件转换

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