工件转换
如果您想在使用依赖项之前对其包含的文件进行更改,该怎么办?
例如,您可能希望解压一个压缩文件,调整 JAR 的内容,或在使用结果之前从包含多个文件的依赖项中删除不必要的文件。
Gradle 为此提供了一个内置功能,称为 Artifact Transforms。借助 Artifact Transforms,您可以修改、添加或删除依赖项中包含的文件集(或 Artifact),例如 JAR 文件。这是在解析 Artifact 的最后一步完成的,在任务或 IDE 等工具使用 Artifact 之前。
Artifact Transforms 概览
每个组件都公开一组变体(variant),每个变体都由一组属性(attribute)(即,debug=true
等键值对)标识。
当 Gradle 解析一个配置时,它会查看每个依赖项,将其解析为一个组件,并从该组件中选择与请求属性匹配的相应变体。该变体包含一个或多个 Artifact,它们表示组件产生的具体输出(例如 JAR 文件、资源或本地二进制文件)。但是,消费者可能需要 Artifact 的格式与任何可用变体不直接匹配。Gradle 不要求生产者明确发布所有可能的变体,而是提供了一种强大的机制,可以动态地将 Artifact 适配到所需的格式。
Artifact Transforms 是一种在构建过程中将一种类型的 Artifact 转换为另一种类型的机制。它们为消费者提供了一种高效灵活的机制,可以将给定生产者的 Artifact 转换为所需的格式,而无需生产者以该格式公开变体。

Artifact Transforms 非常像任务。它们是具有一些输入和输出的工作单元。像 UP-TO-DATE
和缓存这样的机制也适用于 transforms。

任务和 transforms 的主要区别在于它们如何被调度并放入 Gradle 在构建配置和运行时执行的操作链中。从高层次来看,transforms 总是先于任务运行,因为它们在依赖解析期间执行。Transforms 在 Artifact 成为任务输入之前修改 Artifact。
以下是创建和使用 Artifact Transforms 的简要概述

-
实现 Transform:通过创建一个实现
TransformAction
接口的类来定义 Artifact Transform。此类指定应如何将输入 Artifact 转换为输出 Artifact。 -
声明请求属性:属性(用于描述组件不同变体的键值对),例如
org.gradle.usage=java-api
和org.gradle.usage=java-runtime
用于指定所需的 Artifact 格式或类型。 -
注册 Transform:通过使用
dependencies
块的registerTransform()
方法来注册 transform。此方法告诉 Gradle,transform 可以用于修改具有给定“from”属性的任何变体的 Artifact。它还告诉 Gradle 新的“to”属性集将描述结果 Artifact 的格式或类型。 -
使用 Transform:当解析需要一个在所选组件中尚未存在的 Artifact 时(因为没有一个实际 Artifact 具有与请求属性兼容的属性),Gradle 不会直接放弃!相反,Gradle 首先自动搜索所有注册的 transforms,以查看它是否可以构建一个最终产生匹配的转换链。如果 Gradle 找到这样的链,它就会按顺序运行每个 transform,并交付转换后的 Artifact 作为结果。
1. 实现 Transform
Transform 通常被编写为一个实现TransformAction
接口的抽象类。它可以选择在单独的接口中定义参数。
每个 transform 恰好有一个输入 Artifact。它必须用@InputArtifact
注解。
然后,实现TransformAction
接口的transform(TransformOutputs)
方法。此方法的实现定义了 transform 在触发时应该做什么。该方法有一个TransformOutputs
参数,您可以使用它来告诉 Gradle transform 产生什么 Artifact。
这里,MyTransform
是将 jar
Artifact 转换为 transformed-jar
Artifact 的自定义 transform action。
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)
}
}
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
格式。
configurations.named("runtimeClasspath") {
attributes {
attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "transformed-jar")
}
}
configurations.named("runtimeClasspath") {
attributes {
attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "transformed-jar")
}
}
3. 注册 Transform
必须使用dependencies.registerTransform()
方法注册 transform。
这里,我们的 transform 在dependencies
块中注册。
dependencies {
registerTransform(MyTransform::class) {
from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "jar")
to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "transformed-jar")
}
}
dependencies {
registerTransform(MyTransform) {
from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "jar")
to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "transformed-jar")
}
}
“to”属性用于描述此 transform 可以用作输入的 Artifact 的格式或类型,“from”属性用于描述它作为输出产生的 Artifact 的格式或类型。
4. 使用 Transform
在构建过程中,如果直接无法获得匹配项,Gradle 会自动运行注册的 transforms 以满足解析请求。
由于不存在提供请求格式 Artifact 的变体(因为没有一个包含值为 "transformed-jar"
的 artifactType
属性),Gradle 会尝试构建一个转换链,该链将提供与请求属性匹配的 Artifact。
Gradle 的搜索发现 MyTransform
已注册为生成请求的格式,因此它将自动运行。运行此 transform action 会修改现有源变体的 Artifact,以生成以请求格式交付给消费者的新 Artifact。
作为此过程的一部分,Gradle 会生成组件的“虚拟 Artifact 集”。
理解 Artifact Transforms
依赖项可以有不同的变体(variant),本质上是相同依赖项的不同版本或形式。这些变体可以各自提供不同的 Artifact 集,旨在满足不同的用例,例如编译代码、浏览文档或运行应用程序。
每个变体都由一组属性(attribute)标识。属性是描述变体特定特征的键值对。

让我们使用以下示例,其中外部 Maven 依赖项有两个变体。
变体 | 描述 |
---|---|
|
用于针对依赖项进行编译。 |
|
用于运行使用依赖项的应用程序。 |
一个项目依赖项有更多的变体。
变体 | 描述 |
---|---|
|
表示类目录。 |
|
表示打包的 JAR 文件,包含类和资源。 |
依赖项的变体可能在它们的传递依赖项或它们包含的 Artifact 集方面有所不同,或者两者都不同。
例如,Maven 依赖项的 java-api
和 java-runtime
变体仅在其传递依赖项方面有所不同,并且都使用相同的 Artifact——JAR 文件。对于项目依赖项,java-api,classes
和 java-api,jars
变体具有相同的传递依赖项,但 Artifact 不同——分别是 classes
目录和 JAR
文件。
当 Gradle 解析一个配置时,它使用定义的属性来选择每个依赖项的适当变体。Gradle 用来确定选择哪个变体的属性称为请求属性(requested attributes)。
例如,如果一个配置请求 org.gradle.usage=java-api
和 org.gradle.libraryelements=classes
,Gradle 将选择每个依赖项中与这些属性匹配的变体(在这种情况下,是用于编译时 API 的类目录)。匹配不一定是精确的,因为某些属性值可以被 Gradle 识别为与其他值兼容,并在匹配期间互换使用。
有时,依赖项可能没有与请求属性匹配的变体。在这种情况下,Gradle 可以通过修改其 Artifact 而不改变其传递依赖项,将一个变体的 Artifact 转换为另一个“虚拟 Artifact 集”。
当依赖项中已存在与请求属性匹配的变体时,Gradle 不会尝试选择或运行 Artifact Transforms。 |
例如,如果请求的变体是 java-api,classes
,但依赖项只有 java-api,jar
,Gradle 可能会通过使用已注册这些属性的 Artifact Transform 将 JAR
文件解压为 classes
目录。
Gradle 将转换应用于 Artifact,而不是变体。 |
执行 Artifact Transforms
Gradle 会根据需要自动选择 Artifact Transforms 以满足解析请求。
要运行 Artifact Transform,您可以配置自定义Artifact View,以请求目标组件的任何变体都未公开的 Artifact 集。
在解析 ArtifactView
时,Gradle 将根据视图中请求的属性搜索适当的 Artifact Transforms。Gradle 将对在目标组件变体中找到的原始 Artifact 执行这些转换,以生成与视图中属性兼容的结果。
在下面的示例中,TestTransform
类定义了一个转换,该转换已注册以将“jar”类型的 Artifact 处理为“stub”类型的 Artifact。
// 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
inputs.files(resolvedArtifacts.artifactFiles)
doLast {
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)}")
}
}
}
// 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
inputs.files(resolvedArtifacts.artifactFiles)
doLast {
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。这是通过创建 ArtifactView
实现的,该视图过滤具有 ARTIFACT_TYPE_ATTRIBUTE = "stub"
的 Artifact。
理解 Artifact Transforms 链
当 Gradle 解析一个配置,并且图中一个变体没有具有请求属性的 Artifact 集时,它会尝试查找一个或多个 Artifact Transforms 的链,这些链可以按顺序运行以创建所需的 Artifact 集。此过程称为Artifact Transform 选择。

Artifact Transform 选择过程:
-
从请求属性开始:
-
Gradle 从正在解析的配置上指定的属性开始,附加
ArtifactView
上指定的任何属性,最后附加直接在依赖项上声明的任何属性。 -
它会考虑所有修改这些属性的注册 transforms。
-
-
找到现有变体的路径:
-
Gradle 反向工作,试图找到从请求属性到现有变体的路径。
-
例如,如果 minified
属性的值为 true
和 false
,并且 transform 可以将 minified=false
更改为 minified=true
,那么如果只有 minified=false
变体可用但请求 minified=true
,Gradle 将使用此 transform。
Gradle 使用以下过程选择 transforms 链
-
如果只有一个可能产生请求属性的链,则选择该链。
-
如果存在多个此类链,则只考虑最短的链。
-
如果仍然存在多个同样适用但产生不同结果的链,则选择失败并报告错误。
-
如果所有剩余的链都产生相同的 Artifact 结果集,Gradle 将任意选择一个。
多个链如何产生不同的合适结果?Transforms 可以一次更改多个属性。转换链的合适结果是具有与请求属性兼容的属性的结果。但结果也可能包含其他未请求且与结果无关的属性。
例如:如果请求属性 A=a
和 B=b
,并且变体 V1
包含属性 A=a
、B=b
和 C=c
,而变体 V2
包含属性 A=a
、B=b
和 D=d
,那么由于 A
和 B
的所有值都相同(或兼容),因此 V1
或 V2
都可以满足请求。
一个完整示例
让我们继续探索上面开始的 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
transform 将 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 并运行 transform 对其进行压缩。 -
Gradle 执行
producer:jar
任务以生成 JAR,然后运行 transform 对其进行压缩。 -
这些任务和 transforms 尽可能并行执行。
要设置 minified
属性以使其正常工作,您必须将该属性添加到所有正在生成的 JAR 变体中,并将其添加到所有正在请求的可解析配置中。您还应该在属性 schema 中注册该属性。
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"))
}
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 | 将属性添加到 schema |
2 | 所有 JAR 文件均未压缩 |
3 | 请求运行时 classpath 已压缩 |
4 | 添加将被转换的依赖项 |
5 | 添加需要转换后 Artifact 的任务 |
您现在可以看到当我们运行 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
实现 Artifact Transforms
与任务类型类似,Artifact Transform 由一个动作和一些可选参数组成。与自定义任务类型的主要区别在于,动作和参数作为两个单独的类实现。
不带参数的 Artifact Transforms
Artifact Transform 动作由实现 TransformAction 的类提供。此类实现了 transform()
方法,该方法将输入 Artifact 转换为零个、一个或多个输出 Artifact。
大多数 Artifact Transforms 是一对一的,因此 transform
方法将用于将“from”变体中包含的每个输入 Artifact 转换为一个输出 Artifact。
Artifact Transform 动作的实现需要通过调用 TransformOutputs.dir() 或 TransformOutputs.file() 来注册每个输出 Artifact。
您可以向 dir
或 file
方法提供两种类型的路径
-
输入 Artifact 的绝对路径或输入 Artifact 内部的路径(对于输入目录)。
-
相对路径。
Gradle 使用绝对路径作为输出 Artifact 的位置。例如,如果输入 Artifact 是一个解压后的 WAR,transform action 可以对 WEB-INF/lib
目录中的所有 JAR 文件调用 TransformOutputs.file()
。转换的输出将是 Web 应用程序的库 JAR。
对于相对路径,dir()
或 file()
方法会向 transform action 返回一个工作空间。transform action 需要在提供的工作空间位置创建转换后的 Artifact。
输出 Artifact 会按注册顺序替换转换后变体中的输入 Artifact。例如,如果选择的输入变体包含 Artifact lib1.jar
、lib2.jar
、lib3.jar
,并且 transform action 为每个输入 Artifact 注册一个压缩后的输出 Artifact <artifact-name>-min.jar
,则转换后的配置将包含 Artifact lib1-min.jar
、lib2-min.jar
和 lib3-min.jar
。
这是 Unzip
transform 的实现,它将 JAR 文件解压到 classes
目录中。Unzip
transform 不需要任何参数。
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...
}
}
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 | 如果 transform 不使用参数,则使用 TransformParameters.None 。 |
2 | 注入输入 Artifact。 |
3 | 请求解压文件输出位置。 |
4 | 执行 transform 的实际工作。 |
请注意,实现如何使用 @InputArtifact
将要转换的 Artifact 注入到 action 类中,以便在 transform
方法中访问它。此方法通过使用 TransformOutputs.dir()
请求解压后的类目录,然后将 JAR 文件解压到此目录中。
带参数的 Artifact Transforms
Artifact Transform 可能需要参数,例如用于过滤的 String
或用于支持输入 Artifact 转换的文件集合。要将这些参数传递给 transform action,您必须定义一个包含所需参数的新类型。此类型必须实现标记接口 TransformParameters。
这是 Minify
transform 的实现,它通过仅保留 JAR 中的某些类来缩小 JAR 的大小。Minify
transform 需要了解每个 JAR 中要保留的类,这些类以 Map
属性的形式在其参数中提供。
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 ...
}
}
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 | transform 参数接口 |
3 | 使用参数 |
4 | 当不需要压缩时,使用未更改的输入 Artifact。 |
观察如何通过 TransformAction.getParameters()
在 transform()
方法中获取参数。transform()
方法的实现通过使用 TransformOutputs.file()
请求压缩后的 JAR 的位置,然后在此位置创建压缩后的 JAR。
请记住,输入 Artifact 是一个依赖项,它可能有自己的依赖项。假设您的 Artifact transform 需要访问这些传递依赖项。在这种情况下,它可以声明一个返回 FileCollection
的抽象 getter,并用 @InputArtifactDependencies 注解它。当您的 transform 运行时,Gradle 将通过实现 getter 将传递依赖项注入到 FileCollection
属性中。
请注意,在 transform 中使用输入 Artifact 依赖项会对性能产生影响;仅在需要时才注入它们。
带缓存的 Artifact Transforms
Artifact Transforms 可以利用构建缓存来存储其输出,并在结果已知时避免重新运行其转换操作。
要启用构建缓存来存储 Artifact Transform 的结果,请在 action 类上添加 @CacheableTransform
注解。
对于可缓存的 transforms,您必须使用 @PathSensitive 等规范化注解来注解其 @InputArtifact 属性 — 以及任何用 @InputArtifactDependencies 标记的属性。
以下示例演示了一个更复杂的 transform,它将 JAR 中特定类重新定位到不同的包中。此过程涉及重写重新定位的类和任何引用它们的类的字节码(类重定位)。
@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()
}
}
@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 | transform 参数接口 |
3 | 为每个参数声明输入类型 |
4 | 声明输入 Artifact 的规范化 |
5 | 注入输入 Artifact 依赖项 |
6 | 使用参数 |
请注意,要重新定位的类是通过检查输入 Artifact 及其依赖项的包来确定的。此外,transform 确保外部类路径上的 JAR 文件中包含的包不会被重新定位。
增量式 Artifact Transforms
与增量任务类似,Artifact Transforms 可以通过仅处理自上次执行以来更改的文件来避免一些工作。这是通过使用InputChanges接口完成的。
对于 Artifact Transforms,只有输入 Artifact 是增量输入;因此,transform 只能查询那里的更改。要在 transform action 中使用InputChanges,将其注入到 action 中。
有关如何使用InputChanges的更多信息,请参阅增量任务的相应文档。
以下是计算 Java 源文件行数的增量 transform 示例
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()
}
}
}
}
}
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
属性。 -
可以包含不具有相应
to
属性的额外from
属性。 -
transform action 本身可以有配置选项。您可以使用
parameters {}
块来配置它们。 -
您必须在将要解析的配置的项目上注册 transform。
-
您可以向
registerTransform()
方法提供任何实现 TransformAction 的类型。
例如,假设您想解压一些依赖项并将解压后的目录和文件放到 classpath 上。您可以通过注册类型为 Unzip
的 Artifact Transform action 来实现,如下所示:
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)
}
}
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 中保留的类。
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
}
}
}
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 运行的是任务;而不是 Artifact Transforms:./gradlew build.
那么它何时何地运行 transforms 呢?
Gradle 执行 transform 有两种方式:
-
对于项目依赖项,Artifact Transforms 的执行可以在任务执行之前发现,因此可以在任务执行之前进行调度。
-
对于外部模块依赖项,Artifact Transforms 的执行无法在任务执行之前发现,因此会在任务执行内部进行调度。
在良好声明的构建中,项目依赖项可以在任务配置期间完全发现,并在任务执行调度之前完成。如果项目依赖项声明不当(例如,缺少任务输入),则 transform 执行将在任务内部发生。
重要的是要记住 Artifact Transforms
-
仅当不存在匹配的变体来满足请求时才会运行
-
可以并行运行
-
如果可能,不会重新运行(如果多个解析请求要求对同一 Artifact 执行相同的 transform,并且该 transform 是可缓存的,则该 transform 将只运行一次,并且在每个后续请求上从缓存中获取结果)
仅当输入 Artifact 存在时,才会实例化和运行 |
调试 Artifact Transforms
您可以使用 artifactTransforms
任务来检查构建中注册的 Artifact Transforms。此任务使调试意外的 transform 行为、了解应用了哪些 transforms 以及查看 Artifact 如何在构建中流动变得更容易。
要生成报告,请运行
$ ./gradlew artifactTransforms
这将生成列出所有注册 transforms 的输出,包括以下详细信息:
-
每个 transform 的输入和输出 Artifact 类型
-
transform 应用所需的属性
-
自定义 transforms 的实现细节
如果您遇到与 transforms 相关的 Artifact 选择或解析错误,此报告尤其有用。
需要注意的是,artifactTransforms
任务作用于单个项目。如果您在子项目上运行它,它将仅报告在该特定项目注册的 transforms。
例如,要查看 app
子项目中的 Artifact Transforms,您可以运行:
$ ./gradlew :app:artifactTransforms
> Task :app:artifactTransforms
--------------------------------------------------
CopyTransform
--------------------------------------------------
Type: dagger.hilt.android.plugin.transform.CopyTransform
Cacheable: No
From Attributes:
- artifactType = android-classes
To Attributes:
- artifactType = jar-for-dagger
--------------------------------------------------
CopyTransform
--------------------------------------------------
Type: dagger.hilt.android.plugin.transform.CopyTransform
Cacheable: No
From Attributes:
- artifactType = directory
To Attributes:
- artifactType = jar-for-dagger
--------------------------------------------------
AggregatedPackagesTransform
--------------------------------------------------
Type: dagger.hilt.android.plugin.transform.AggregatedPackagesTransform
Cacheable: Yes
From Attributes:
- artifactType = jar-for-dagger
To Attributes:
- artifactType = aggregated-jar-for-hilt
Some artifact transforms are not cacheable. This can have negative performance impacts.
由于任务仅检查当前项目,因此在根项目上运行它(该项目未注册任何 transforms)将产生空输出。
$ ./gradlew artifactTransforms
> Task :artifactTransforms
There are no transforms registered in root project 'gradle'.