如在不同类型的配置中所述 ,对于相同的依赖项可能有不同的变体. 例如,外部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变体具有相同的传递依赖项和不同的工件-分别是classes目录和JAR文件.

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

当Gradle解析配置时,解析的配置上的属性将确定请求的属性 . 对于配置中的所有依赖关系,在解析配置时都会选择具有请求属性的变量. 例如,当配置在项目依赖项上请求org.gradle.usage=java-api, org.gradle.libraryelements=classes时,将选择classes目录作为工件.

如果依赖项没有具有所请求属性的变量,则解析配置失败. 有时可以在不更改传递依赖项的情况下将依赖项的工件转换为请求的变体. 例如,解压缩JAR会将java-api,jars变体的工件转换为java-api,classes变体. 这种转换称为" 伪影转换" . Gradle允许注册工件转换,并且当依赖项没有所请求的变体时,Gradle将尝试查找一系列工件转换以创建变体.

Artifact transform selection and execution

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

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

例如,考虑具有两个值的minified属性: truefalse . minified属性表示依赖项的变体,其中删除了不必要的类文件. 有一个伪像转换已注册,可以将minifiedfalse转换为true . 当请求minified=true作为依赖项时,并且只有带有minified=false变体,则Gradle选择注册的minify变换. 该缩小变换是能够与依赖的神器变换minified=false与神器minified=true .

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

  • 如果只有一个转换链,则选择它.

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

  • 如果存在最短的变换链,则将其选中.

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

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

artifactType属性是特殊的,因为它仅出现在已解析的工件上,而不存在于依赖项上. 结果,当解析仅以artifactType作为请求属性的配置时,将永远不会选择仅使artifactType突变的任何变换. 仅在使用ArtifactView时才考虑使用.

选择所需的工件变换后,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 and

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

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

  • 还有一些.

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

解决了配置后,Gradle需要下载guava JAR并将其缩小. Gradle还需要执行producer:jar任务来生成项目的JAR工件,然后将其最小化. guava.jar的下载和guava.jar化与guava.jar producer:jar任务的执行以及生成的JAR的guava.jar同时进行.

这是设置minified属性的方法,以便上面的方法起作用. 您需要在模式中注册新属性,将其添加到所有JAR工件中,并在所有可解析配置上请求它.

例子1.工件转换属性设置
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'))
}
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"))
}
  1. 将属性添加到架构

  2. 未缩小所有JAR文件

  3. 在所有可解析的配置上请求minified=true

  4. 添加将要转换的依赖项

现在,您可以看到当我们运行resolveRuntimeClasspath任务来解析runtimeClasspath配置时会发生什么. 请注意,在resolveRuntimeClasspath任务开始之前,Gradle会转换项目依赖项. 当Gradle执行resolveRuntimeClasspath任务时,它会转换二进制依赖项.

解决runtimeClasspath配置时的输出
> gradle resolveRuntimeClasspath

> Task :producer:compileJava
> Task :producer:processResources NO-SOURCE
> Task :producer:classes
> Task :producer:jar

> Transform artifact 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

Implementing artifact transforms

与任务类型相似,工件转换由动作和一些参数组成. 与自定义任务类型的主要区别在于,操作和参数被实现为两个单独的类.

工件转换动作的实现是实现TransformAction的类. 您需要在该操作上实现transform()方法,该方法将输入工件转换为零,一个或多个输出工件. 大多数伪像转换将是一对一的,因此transform方法会将输入伪像转换为恰好一个输出伪像.

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

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

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

  • 相对路径.

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

对于相对路径, dir()file()方法将工作空间返回到转换动作. 转换动作的实现需要在提供的工作空间的位置处创建转换后的工件.

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

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

例子2.没有参数的工件转换
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...
    }
}
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...
    }
}
  1. 如果不使用参数则使用TransformParameters.None

  2. 注入输入工件

  3. Request an output location for the unzipped files

  4. 做转换的实际工作

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

您可以在开发自定义Gradle类型中找到有关实现工件转换参数的更多信息.

Here is the implementation of a Minify transform that makes JARs smaller by only keeping certain classes in them. The Minify transform requires the classes to keep as parameters. Observe how you can obtain the parameters by TransformAction.getParameters() in the transform() method. The implementation of the transform() method requests a location for the minified JAR by using TransformOutputs.file() and then creates the minified JAR at this location.

例子3.缩小转换实现
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 ...
    }
}
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 ...
    }
}
  1. 声明参数类型

  2. 转换参数接口

  3. 使用参数

  4. 当不需要缩小时使用不变的输入工件

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

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

下面的示例显示一个更复杂的转换. 它将JAR的某些选定类移动到不同的包,并使用移动后的类重写类和所有类的字节码(类重定位). 为了确定要重定位的类,它查看输入工件的包和输入工件的依赖项. 它还不会在外部类路径中重新放置JAR文件中包含的包.

例子4.用于类重定位的工件转换
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()
    }
}
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()
    }
}
  1. 声明可缓存的转换

  2. 转换参数接口

  3. 声明每个参数的输入类型

  4. 声明输入工件的规范化

  5. 注入输入工件依赖项

  6. 使用参数

Registering artifact transforms

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

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

使用registerTransform()时需要注意以下几点:

  • fromto属性是必需的.

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

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

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

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

例子5.没有参数的工件变换配准
build.gradle
def artifactType = Attribute.of('artifactType', String)

dependencies {
    registerTransform(Unzip) {
        from.attribute(artifactType, 'jar')
        to.attribute(artifactType, 'java-classes-directory')
    }
}
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")
    }
}

另一个示例是您想通过只保留一些class文件来缩小JAR. 请注意,使用parameters {}块来提供要保留在Minify转换的Minify JAR中的类.

例子6.带参数的工件转换配准
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
        }
    }
}
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
        }
    }
}

Implementing incremental artifact transforms

增量任务类似,工件转换可以通过仅处理上次执行后的更改文件来避免工作. 这是通过使用InputChanges接口完成的. 对于工件转换,只有输入工件是增量输入,因此转换只能查询那里的更改. 为了在转换动作中使用InputChanges ,请将其注入到动作中. 有关如何使用InputChanges的更多信息,请参见有关增量任务的相应文档.

这是一个增量转换的示例,它计算Java源文件中的代码行:

Example 7. Artifact transform for lines of code counting
build.gradle
abstract class CountLoc implements TransformAction<TransformParameters.None> {

    @Inject
    abstract InputChanges getInputChanges()

    @PathSensitive(PathSensitivity.RELATIVE)
    @InputArtifact
    abstract Provider<FileSystemLocation> getInput()

    @Override
    void transform(TransformOutputs outputs) {                          // (1)
        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()

            }
        }
    }
}
build.gradle.kts
abstract class CountLoc : TransformAction<TransformParameters.None> {

    @get:Inject
    abstract val inputChanges: InputChanges

    @get:PathSensitive(PathSensitivity.RELATIVE)
    @get:InputArtifact
    abstract val input: Provider<FileSystemLocation>

    override
    fun transform(outputs: TransformOutputs) {                          // (1)
        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()
                }
            }
        }
    }
}
  1. Inject InputChanges

  2. 查询输入工件中的更改