正如不同种类的配置中所描述的,相同的依赖关系可能有不同的变体。例如,外部 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-api
和java-runtime
变体仅在传递依赖项上有所不同,并且都使用相同的工件 - JAR 文件。对于项目依赖项,java-api,classes
和java-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
变体。这种变换称为Artifact Transform。 Gradle 允许注册工件转换,当依赖项没有请求的变体时,Gradle 将尝试查找工件转换链来创建变体。
工件变换选择和执行
如上所述,当 Gradle 解析配置并且配置中的依赖项不具有具有所请求属性的变体时,Gradle 会尝试找到工件转换链来创建变体。寻找匹配的工件变换链的过程称为工件变换选择。每个注册的转换都会从一组属性转换为一组属性。例如,unzip 转换可以从 转换org.gradle.usage=java-api, org.gradle.libraryelements=jars
为org.gradle.usage=java-api, org.gradle.libraryelements=classes
。
为了找到一条链,Gradle 从请求的属性开始,然后将修改某些请求的属性的所有转换视为通向那里的可能路径。向后看,Gradle 尝试使用转换来获取某些现有变体的路径。
例如,考虑minified
具有两个值的属性:true
和false
。 minified 属性表示删除了不必要的类文件的依赖项的变体。注册了一个工件转换,可以minified
从转换false
为true
。当minified=true
请求依赖项,并且只有带 的变体时minified=false
,Gradle 选择注册的 minify 转换。缩小转换能够将依赖项 的工件转换minified=false
为 的工件minified=true
。
在所有找到的变换链中,Gradle 尝试选择最好的一个:
-
如果只有一个变换链,则选择它。
-
如果有两个变换链,并且其中一个是另一个的后缀,则选择它。
-
如果存在最短变换链,则选择它。
-
在所有其他情况下,选择都会失败并报告错误。
当已经存在与请求的属性匹配的依赖项变体时,Gradle 不会尝试选择工件转换。 |
该 |
选择所需的工件转换后,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
的变体转换guava
为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
-
还有一些。
同样,使用 minify 转换,Gradle 可以将org.gradle.usage=java-runtime,org.gradle.libraryelements=jar,minified=false
项目的变体转换producer
为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 工件并在所有可解析配置上请求它。
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"))
}
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
。
> 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()来注册每个输出工件。
您只能为dir
或file
方法提供两种类型的路径:
-
输入工件或输入工件中的绝对路径(对于输入目录)。
-
相对路径。
Gradle 使用绝对路径作为输出工件的位置。例如,如果输入工件是分解的 WAR,则转换操作可以调用目录TransformOutputs.file()
中的所有 jar 文件WEB-INF/lib
。转换的输出将是 Web 应用程序的库 JAR。
对于相对路径,dir()
orfile()
方法将工作空间返回给转换操作。转换操作的实现需要在提供的工作空间的位置创建转换后的工件。
输出工件按照注册顺序替换转换后的变体中的输入工件。例如,如果配置由工件lib1.jar
、lib2.jar
、组成lib3.jar
,并且转换操作<artifact-name>-min.jar
为输入工件注册了缩小的输出工件,则转换后的配置由工件lib1-min.jar
、lib2-min.jar
和组成lib3-min.jar
。
下面是一个转换的实现Unzip
,它通过解压缩 JAR 文件将其转换为类目录。该Unzip
转换不需要任何参数。请注意实现如何使用@InputArtifact
注入工件来转换为操作。它通过使用请求解压类的目录TransformOutputs.dir()
,然后将 JAR 文件解压到该目录中。
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...
}
}
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。
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 | 变换参数的接口 |
3 | 使用参数 |
4 | 当不需要缩小时,使用未更改的输入工件 |
请记住,输入工件是一个依赖项,它可能有自己的依赖项。如果您的工件转换需要访问这些传递依赖项,它可以声明一个返回 a 的抽象 getter并使用@InputArtifactDependencyFileCollection
对其进行注释。当您的转换运行时,Gradle 将通过实现 getter将传递依赖项注入到该属性中。请注意,在转换中使用输入工件依赖项会对性能产生影响,仅在真正需要时才注入它们。FileCollection
此外,工件转换可以利用构建缓存来输出。要为工件转换启用构建缓存,请在操作类上添加注释。对于可缓存的转换,您必须使用规范化注释(例如@PathSensitive)注释其@InputArtifact属性以及任何标有@InputArtifactDependency的属性。@CacheableTransform
以下示例显示了更复杂的转换。它将 JAR 的某些选定类移动到不同的包,重写移动类的字节码以及使用移动类的所有类(类重定位)。为了确定要重定位的类,它会查看输入工件的包以及输入工件的依赖项。它也不会重新定位外部类路径中 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 | 声明转换可缓存 |
2 | 变换参数的接口 |
3 | 声明每个参数的输入类型 |
4 | 声明输入工件的标准化 |
5 | 注入输入工件依赖项 |
6 | 使用参数 |
注册工件转换
您需要注册工件转换操作,并在必要时提供参数,以便在解析依赖项时可以选择它们。
为了注册工件转换,您必须在块内使用registerTransform()dependencies {}
。
使用时有几点需要注意registerTransform()
:
-
from
和属性to
是必需的。 -
转换操作本身可以有配置选项。您可以使用
parameters {}
块来配置它们。 -
您必须在具有要解析的配置的项目上注册转换。
-
您可以向该方法提供任何实现TransformAction 的
registerTransform()
类型。
例如,假设您想要解压一些依赖项并将解压后的目录和文件放在类路径上。您可以通过注册类型为 的工件转换操作来实现此目的Unzip
,如下所示:
val artifactType = Attribute.of("artifactType", String::class.java)
dependencies {
registerTransform(Unzip::class) {
from.attribute(artifactType, "jar")
to.attribute(artifactType, "java-classes-directory")
}
}
def artifactType = Attribute.of('artifactType', String)
dependencies {
registerTransform(Unzip) {
from.attribute(artifactType, 'jar')
to.attribute(artifactType, 'java-classes-directory')
}
}
另一个例子是,您希望通过仅保留class
JAR 中的一些文件来缩小 JAR 大小。请注意使用该parameters {}
块来提供要保留在转换的缩小 JAR 中的类Minify
。
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
}
}
}
实施增量工件转换
与增量任务类似,工件转换可以通过仅处理上次执行中更改的文件来避免工作。这是通过使用InputChanges接口来完成的。对于工件转换,只有输入工件是增量输入,因此转换只能查询其中的更改。为了在转换操作中使用InputChanges,请将其注入到操作中。有关如何使用InputChanges 的更多信息,请参阅增量任务的相应文档。
下面是一个增量转换的示例,用于计算 Java 源文件中的代码行数:
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 | 查询输入工件的更改 |