从存储库中提取的每个模块都有与其关联的元数据,例如其组、名称、版本以及它提供的不同变体及其工件和依赖项。有时,此元数据不完整或不正确。为了在构建脚本中操作此类不完整的元数据,Gradle 提供了一个 API 来编写组件元数据规则。这些规则在模块的元数据下载之后、用于依赖关系解析之前生效。

编写组件元数据规则的基础知识

组件元数据规则应用于构建脚本的依赖项块 ( DependencyHandler ) 的组件 ( ComponentMetadataHandler ) 部分或设置脚本中。规则可以通过两种不同的方式定义:

  1. 当它们应用于组件部分时直接作为操作

  2. 作为实现ComponentMetadataRule接口的隔离类

虽然将内联规则定义为操作可以方便实验,但通常建议将规则定义为单独的类。可以对编写为独立类的规则进行注释,以@CacheableRule缓存其应用程序的结果,这样就不需要在每次解决依赖关系时重新执行它们。

build.gradle.kts
@CacheableRule
abstract class TargetJvmVersionRule @Inject constructor(val jvmVersion: Int) : ComponentMetadataRule {
    @get:Inject abstract val objects: ObjectFactory

    override fun execute(context: ComponentMetadataContext) {
        context.details.withVariant("compile") {
            attributes {
                attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, jvmVersion)
                attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_API))
            }
        }
    }
}
dependencies {
    components {
        withModule<TargetJvmVersionRule>("commons-io:commons-io") {
            params(7)
        }
        withModule<TargetJvmVersionRule>("commons-collections:commons-collections") {
            params(8)
        }
    }
    implementation("commons-io:commons-io:2.6")
    implementation("commons-collections:commons-collections:3.2.2")
}
build.gradle
@CacheableRule
abstract class TargetJvmVersionRule implements ComponentMetadataRule {
    final Integer jvmVersion
    @Inject TargetJvmVersionRule(Integer jvmVersion) {
        this.jvmVersion = jvmVersion
    }

    @Inject abstract ObjectFactory getObjects()

    void execute(ComponentMetadataContext context) {
        context.details.withVariant("compile") {
            attributes {
                attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, jvmVersion)
                attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_API))
            }
        }
    }
}
dependencies {
    components {
        withModule("commons-io:commons-io", TargetJvmVersionRule) {
            params(7)
        }
        withModule("commons-collections:commons-collections", TargetJvmVersionRule) {
            params(8)
        }
    }
    implementation("commons-io:commons-io:2.6")
    implementation("commons-collections:commons-collections:3.2.2")
}

从上面的示例中可以看出,组件元数据规则是通过实现ComponentMetadataRule来定义的,该规则具有接收ComponentMetadataContextexecute实例作为参数的单个方法。在此示例中,还通过ActionConfiguration进一步配置规则。这是通过在您的实现中使用构造函数来接受已配置的参数和需要注入的服务来支持的。ComponentMetadataRule

Gradle 强制隔离ComponentMetadataRule.这意味着所有参数必须是Serializable或者已知的可以隔离的 Gradle 类型。

此外,Gradle 服务可以注入到您的ComponentMetadataRule.因此,一旦有了构造函数,就必须用 进行注释@javax.inject.Inject。通常需要的服务是ObjectFactory ,用于创建强类型值对象的实例,例如用于设置Attribute 的值。RepositoryResourceAccessor是一项有助于高级使用自定义元数据的组件元数据规则的服务。

组件元数据规则可以应用于所有模块——all(rule)或者应用于选定的模块withModule(groupAndName, rule)。通常,规则是专门为了丰富某个特定模块的元数据而编写的,因此withModule应该首选 API。

在中心位置声明规则

在设置中声明组件元数据规则是一项正在孵化的功能

可以在文件中settings.gradle(.kts)为整个构建声明规则,而不是单独为每个子项目声明规则。设置中声明的规则是应用于每个项目的常规规则:如果项目未声明任何规则,则将使用设置脚本中的规则。

settings.gradle.kts
dependencyResolutionManagement {
    components {
        withModule<GuavaRule>("com.google.guava:guava")
    }
}
settings.gradle
dependencyResolutionManagement {
    components {
        withModule("com.google.guava:guava", GuavaRule)
    }
}

默认情况下,项目中声明的规则将覆盖设置中声明的任何内容。可以更改此默认值,例如始终首选设置规则:

settings.gradle.kts
dependencyResolutionManagement {
    rulesMode = RulesMode.PREFER_SETTINGS
}
settings.gradle
dependencyResolutionManagement {
    rulesMode = RulesMode.PREFER_SETTINGS
}

如果调用此方法并且项目或插件声明了规则,则会发出警告。您可以使用以下替代方案来使其失败:

settings.gradle.kts
dependencyResolutionManagement {
    rulesMode = RulesMode.FAIL_ON_PROJECT_RULES
}
settings.gradle
dependencyResolutionManagement {
    rulesMode = RulesMode.FAIL_ON_PROJECT_RULES
}

默认行为相当于调用此方法:

settings.gradle.kts
dependencyResolutionManagement {
    rulesMode = RulesMode.PREFER_PROJECT
}
settings.gradle
dependencyResolutionManagement {
    rulesMode = RulesMode.PREFER_PROJECT
}

元数据的哪些部分可以修改?

组件元数据规则 API 面向Gradle 模块元数据和构建脚本中的依赖项API 支持的功能。在构建脚本中编写规则与定义依赖项和工件之间的主要区别在于,组件元数据规则遵循 Gradle 模块元数据的结构,直接对变体进行操作。相反,在构建脚本中,您经常会同时影响多个变体的形状(例如,将api依赖项添加到Java 库的api运行时变体中,由jar任务生成的工件也添加到这两个变体中) 。

可以通过以下方法对变体进行修改:

  • allVariants:修改组件的所有变体

  • withVariant(name):修改由名称标识的单个变体

  • addVariant(name)或:从头开始或通过复制现有变体(基础)的详细信息addVariant(name, base)向组件添加新变体

每个变体的以下细节可以调整:

  • 识别变体的属性——attributes {}

  • 该变体提供的功能—withCapabilities { }阻止

  • 变体的依赖,包括丰富的版本——blockwithDependencies {}

  • 变体的依赖约束,包括丰富的版本withDependencyConstraints {}block

  • 构成变体实际内容的已发布文件的位置 —withFiles { }

整个组件还有一些属性可以更改:

  • 组件级属性,目前唯一有意义的属性org.gradle.status

  • 在版本选择期间影响属性解释的状态方案org.gradle.status

  • 通过虚拟平台进行版本对齐的belongsTo属性

根据模块元数据的格式,它会以不同的方式映射到元数据的以变体为中心的表示形式:

  • 如果模块具有 Gradle 模块元数据,则规则操作的数据结构与您在模块.module文件中找到的数据结构非常相似。

  • 如果模块仅使用.pom元数据发布,则会派生出许多固定变体,如POM 文件到变体的映射部分中所述。

  • 如果模块仅通过文件发布ivy.xml,则可以访问文件中定义的Ivy 配置而不是变体。它们的依赖关系、依赖关系约束和文件都可以修改。此外,如果需要,该addVariant(name, baseVariantOrConfiguration) { }API 还可用于从Ivy 配置派生变体(例如,可以使用此定义Java 库插件的编译运行时变体)。

何时使用组件元数据规则?

一般来说,如果您考虑使用组件元数据规则来调整某个模块的元数据,您应该首先检查该模块是否是使用 Gradle 模块元数据(.module文件)或仅传统元数据(.pomivy.xml)发布的。

如果模块是使用 Gradle 模块元数据发布的,则元数据可能是完整的,尽管仍然可能存在明显错误的情况。对于这些模块,如果您已明确识别元数据本身存在问题,则应仅使用组件元数据规则。如果您对依赖项解析结果有问题,您应该首先检查是否可以通过声明丰富版本的依赖项约束来解决该问题。特别是,如果您正在开发要发布的库,您应该记住,与组件元数据规则相反,依赖性约束是作为您自己的库的元数据的一部分发布的。因此,通过依赖关系约束,您可以自动与消费者共享依赖关系解决问题的解决方案,而组件元数据规则仅应用于您自己的构建。

如果模块是使用传统元数据(.pomivy.xml仅没有.module文件)发布的,则元数据很可能不完整,因为这些格式不支持变体或依赖性约束等功能。尽管如此,从概念上讲,此类模块可以包含不同的变体,或者可能具有它们刚刚省略的依赖关系约束(或错误地定义为依赖关系)。在接下来的部分中,我们将探讨一些具有此类不完整元数据的现有 oss 模块以及添加缺失元数据信息的规则。

根据经验,您应该考虑您正在编写的规则是否也可以在构建的上下文之外工作。也就是说,如果应用于使用其影响的模块的任何其他构建,该规则是否仍然会产生正确且有用的结果?

修复错误的依赖细节

让我们以Maven Central上的 Jaxen XPath Engine 发布为例。 1.1.3版本的pom在编译范围内声明了许多编译实际上不需要的依赖项。这些已在 1.1.4 pom 中删除。假设由于某种原因我们需要使用 1.1.3,我们可以使用以下规则修复元数据:

build.gradle.kts
@CacheableRule
abstract class JaxenDependenciesRule: ComponentMetadataRule {
    override fun execute(context: ComponentMetadataContext) {
        context.details.allVariants {
            withDependencies {
                removeAll { it.group in listOf("dom4j", "jdom", "xerces",  "maven-plugins", "xml-apis", "xom") }
            }
        }
    }
}
build.gradle
@CacheableRule
abstract class JaxenDependenciesRule implements ComponentMetadataRule {
    void execute(ComponentMetadataContext context) {
        context.details.allVariants {
            withDependencies {
                removeAll { it.group in ["dom4j", "jdom", "xerces",  "maven-plugins", "xml-apis", "xom"] }
            }
        }
    }
}

在该withDependencies块中,您可以访问完整的依赖项列表,并可以使用 Java 集合接口上可用的所有方法来检查和修改该列表。此外,还有add(notation, configureAction)一些方法接受类似于在构建脚本中声明依赖项的常用符号。可以在块中以相同的方式检查和修改依赖约束withDependencyConstraints

如果我们仔细查看 Jaxen 1.1.4 pom,我们会发现dom4jjdomxerces依赖项仍然存在,但标记为可选。 Gradle 或 Maven 不会自动处理 poms 中的可选依赖项。原因是它们表明 Jaxen 库提供了一些可选功能变体,这些变体需要一个或多个这些依赖项,但缺少这些功能是什么以及哪个依赖项属于哪个功能的信息。此类信息不能在 pom 文件中表示,而是通过变量和功能在 Gradle 模块元数据中表示。因此,我们也可以在规则中添加这些信息。

build.gradle.kts
@CacheableRule
abstract class JaxenCapabilitiesRule: ComponentMetadataRule {
    override fun execute(context: ComponentMetadataContext) {
        context.details.addVariant("runtime-dom4j", "runtime") {
            withCapabilities {
                removeCapability("jaxen", "jaxen")
                addCapability("jaxen", "jaxen-dom4j", context.details.id.version)
            }
            withDependencies {
                add("dom4j:dom4j:1.6.1")
            }
        }
    }
}
build.gradle
@CacheableRule
abstract class JaxenCapabilitiesRule implements ComponentMetadataRule {
    void execute(ComponentMetadataContext context) {
        context.details.addVariant("runtime-dom4j", "runtime") {
            withCapabilities {
                removeCapability("jaxen", "jaxen")
                addCapability("jaxen", "jaxen-dom4j", context.details.id.version)
            }
            withDependencies {
                add("dom4j:dom4j:1.6.1")
            }
        }
    }
}

在这里,我们首先使用该addVariant(name, baseVariant)方法创建一个附加变体,通过定义新功能jaxen-dom4j来表示 Jaxen 的可选 dom4j 集成功能,将其识别为功能变体。这与在构建脚本中定义可选功能变体类似。然后,我们使用添加依赖项的方法之一来定义此可选功能需要哪些依赖项。add

在构建脚本中,我们可以添加对可选功能的依赖项,Gradle 将使用丰富的元数据来发现正确的传递依赖项。

build.gradle.kts
dependencies {
    components {
        withModule<JaxenDependenciesRule>("jaxen:jaxen")
        withModule<JaxenCapabilitiesRule>("jaxen:jaxen")
    }
    implementation("jaxen:jaxen:1.1.3")
    runtimeOnly("jaxen:jaxen:1.1.3") {
        capabilities { requireCapability("jaxen:jaxen-dom4j") }
    }
}
build.gradle
dependencies {
    components {
        withModule("jaxen:jaxen", JaxenDependenciesRule)
        withModule("jaxen:jaxen", JaxenCapabilitiesRule)
    }
    implementation("jaxen:jaxen:1.1.3")
    runtimeOnly("jaxen:jaxen:1.1.3") {
        capabilities { requireCapability("jaxen:jaxen-dom4j") }
    }
}

使变体明确发布为分类罐子

虽然在前面的示例中,所有变体、“主要变体”和可选功能都打包在一个 jar 文件中,但通常将某些变体发布为单独的文件。特别是,当变体是互斥的时——即它们不是特征变体,而是提供替代选择的不同变体。所有基于 pom 的库都已经拥有的一个例子是运行时编译变体,其中 Gradle 只能根据手头的任务选择一个。 Java 生态系统中经常发现的另一种替代方案是针对不同 Java 版本的 jar。

作为示例,我们看一下Mavencentral上发布的异步编程库 Quasar 0.7.9 版本。如果我们检查目录列表,我们会发现quasar-core-0.7.9-jdk8.jar除了quasar-core-0.7.9.jar.使用分类器(此处为jdk8 )发布其他 jar是 Maven 存储库中的常见做法。虽然 Maven 和 Gradle 都允许您通过分类器引用此类 jar,但元数据中根本没有提及它们。因此,没有任何信息表明这些 jar 存在,也不知道这些 jar 代表的变体之间是否存在任何其他差异(例如不同的依赖关系)。

在 Gradle 模块元数据中,会出现此变体信息,对于已经发布的 Quasar 库,我们可以使用以下规则添加它:

build.gradle.kts
@CacheableRule
abstract class QuasarRule: ComponentMetadataRule {
    override fun execute(context: ComponentMetadataContext) {
        listOf("compile", "runtime").forEach { base ->
            context.details.addVariant("jdk8${base.capitalize()}", base) {
                attributes {
                    attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8)
                }
                withFiles {
                    removeAllFiles()
                    addFile("${context.details.id.name}-${context.details.id.version}-jdk8.jar")
                }
            }
            context.details.withVariant(base) {
                attributes {
                    attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 7)
                }
            }
        }
    }
}
build.gradle
@CacheableRule
abstract class QuasarRule implements ComponentMetadataRule {
    void execute(ComponentMetadataContext context) {
        ["compile", "runtime"].each { base ->
            context.details.addVariant("jdk8${base.capitalize()}", base) {
                attributes {
                    attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8)
                }
                withFiles {
                    removeAllFiles()
                    addFile("${context.details.id.name}-${context.details.id.version}-jdk8.jar")
                }
            }
            context.details.withVariant(base) {
                attributes {
                    attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 7)
                }
            }
        }
    }
}

在这种情况下,很明显分类器代表目标 Java 版本,这是一个已知的 Java 生态系统属性。因为我们还需要Java 8 的编译运行时,所以我们创建了两个新变体,但使用现有的编译运行时变体作为基础。这样,所有其他 Java 生态系统属性都已正确设置,并且所有依赖项都已保留。然后我们将两个变体设置TARGET_JVM_VERSION_ATTRIBUTE8,从新变体中删除任何现有文件removeAllFiles(),并添加 jdk8 jar 文件addFile()。这removeAllFiles()是必需的,因为对主 jar 的引用quasar-core-0.7.5.jar是从相应的基本变体复制的。

我们还通过它们针对 Java 7 的信息丰富了现有的编译运行时attribute(TARGET_JVM_VERSION_ATTRIBUTE, 7)变体 — .

现在,我们可以为构建脚本中编译类路径的所有依赖项请求 Java 8 版本,Gradle 会自动为每个库选择最合适的变体。对于 Quasar,这现在将是公开quasar-core-0.7.9-jdk8.jar.

build.gradle.kts
configurations["compileClasspath"].attributes {
    attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8)
}
dependencies {
    components {
        withModule<QuasarRule>("co.paralleluniverse:quasar-core")
    }
    implementation("co.paralleluniverse:quasar-core:0.7.9")
}
build.gradle
configurations.compileClasspath.attributes {
    attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8)
}
dependencies {
    components {
        withModule("co.paralleluniverse:quasar-core", QuasarRule)
    }
    implementation("co.paralleluniverse:quasar-core:0.7.9")
}

使版本中编码的变体显式化

为同一库发布多个替代方案的另一个解决方案是使用版本控制模式,如流行的 Guava 库所做的那样。在这里,通过将分类器而不是 jar 工件附加到版本,每个新版本都会发布两次。以 Guava 28 为例,我们可以在Mavencentral上找到28.0-jre (Java 8) 和28.0-android (Java 6) 版本。仅使用 pom 元数据时使用此模式的优点是两种变体都可以通过版本发现。缺点是没有信息说明不同版本后缀的语义含义。因此,在发生冲突的情况下,Gradle 在比较版本字符串时只会选择最高版本。

将其转换为适当的变体有点棘手,因为 Gradle 首先选择模块的版本,然后选择最合适的变体。因此,不直接支持将变体编码为版本的概念。但是,由于这两个变体总是一起发布,我们可以假设这些文件物理上位于同一存储库中。由于它们是按照 Maven 存储库约定发布的,因此如果我们知道模块名称和版本,我们就知道每个文件的位置。我们可以写出以下规则:

build.gradle.kts
@CacheableRule
abstract class GuavaRule: ComponentMetadataRule {
    override fun execute(context: ComponentMetadataContext) {
        val variantVersion = context.details.id.version
        val version = variantVersion.substring(0, variantVersion.indexOf("-"))
        listOf("compile", "runtime").forEach { base ->
            mapOf(6 to "android", 8 to "jre").forEach { (targetJvmVersion, jarName) ->
                context.details.addVariant("jdk$targetJvmVersion${base.capitalize()}", base) {
                    attributes {
                        attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, targetJvmVersion)
                    }
                    withFiles {
                        removeAllFiles()
                        addFile("guava-$version-$jarName.jar", "../$version-$jarName/guava-$version-$jarName.jar")
                    }
                }
            }
        }
    }
}
build.gradle
@CacheableRule
abstract class GuavaRule implements ComponentMetadataRule {
    void execute(ComponentMetadataContext context) {
        def variantVersion = context.details.id.version
        def version = variantVersion.substring(0, variantVersion.indexOf("-"))
        ["compile", "runtime"].each { base ->
            [6: "android", 8: "jre"].each { targetJvmVersion, jarName ->
                context.details.addVariant("jdk$targetJvmVersion${base.capitalize()}", base) {
                    attributes {
                        attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, targetJvmVersion)
                    }
                    withFiles {
                        removeAllFiles()
                        addFile("guava-$version-${jarName}.jar", "../$version-$jarName/guava-$version-${jarName}.jar")
                    }
                }
            }
        }
    }
}

与前面的示例类似,我们为两个 Java 版本添加运行时和编译变体。然而,在该withFiles块中,我们现在还指定了相应 jar 文件的相对路径,这使得 Gradle 可以找到该文件,无论它选择的是-jre还是-android版本。该路径始终相对于pom选择模块版本的元数据(在本例中)文件的位置。因此,根据此规则,两个 Guava 28“版本”都带有jdk6jdk8变体。因此,Gradle 解析为哪一个并不重要。该变体以及正确的 jar 文件是根据请求的TARGET_JVM_VERSION_ATTRIBUTE值确定的。

build.gradle.kts
configurations["compileClasspath"].attributes {
    attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 6)
}
dependencies {
    components {
        withModule<GuavaRule>("com.google.guava:guava")
    }
    // '23.3-android' and '23.3-jre' are now the same as both offer both variants
    implementation("com.google.guava:guava:23.3+")
}
build.gradle
configurations.compileClasspath.attributes {
    attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 6)
}
dependencies {
    components {
        withModule("com.google.guava:guava", GuavaRule)
    }
    // '23.3-android' and '23.3-jre' are now the same as both offer both variants
    implementation("com.google.guava:guava:23.3+")
}

添加原生 jar 的变体

带有分类器的 jar 还用于将存在多个替代方案的库部分(例如本机代码)与主要工件分开。例如,这是由轻量级 Java 游戏库 (LWGJ) 完成的,该库将多个特定于平台的 jar 发布到Maven 中心,除了主 jar 之外,在运行时始终需要其中一个。不可能在 pom 元数据中传达此信息,因为不存在通过元数据关联多个工件的概念。在 Gradle 模块元数据中,每个变体可以有任意多个文件,我们可以通过编写以下规则来利用它:

build.gradle.kts
@CacheableRule
abstract class LwjglRule: ComponentMetadataRule {
    data class NativeVariant(val os: String, val arch: String, val classifier: String)

    private val nativeVariants = listOf(
        NativeVariant(OperatingSystemFamily.LINUX,   "arm32",  "natives-linux-arm32"),
        NativeVariant(OperatingSystemFamily.LINUX,   "arm64",  "natives-linux-arm64"),
        NativeVariant(OperatingSystemFamily.WINDOWS, "x86",    "natives-windows-x86"),
        NativeVariant(OperatingSystemFamily.WINDOWS, "x86-64", "natives-windows"),
        NativeVariant(OperatingSystemFamily.MACOS,   "x86-64", "natives-macos")
    )

    @get:Inject abstract val objects: ObjectFactory

    override fun execute(context: ComponentMetadataContext) {
        context.details.withVariant("runtime") {
            attributes {
                attributes.attribute(OperatingSystemFamily.OPERATING_SYSTEM_ATTRIBUTE, objects.named("none"))
                attributes.attribute(MachineArchitecture.ARCHITECTURE_ATTRIBUTE, objects.named("none"))
            }
        }
        nativeVariants.forEach { variantDefinition ->
            context.details.addVariant("${variantDefinition.classifier}-runtime", "runtime") {
                attributes {
                    attributes.attribute(OperatingSystemFamily.OPERATING_SYSTEM_ATTRIBUTE, objects.named(variantDefinition.os))
                    attributes.attribute(MachineArchitecture.ARCHITECTURE_ATTRIBUTE, objects.named(variantDefinition.arch))
                }
                withFiles {
                    addFile("${context.details.id.name}-${context.details.id.version}-${variantDefinition.classifier}.jar")
                }
            }
        }
    }
}
build.gradle
@CacheableRule
abstract class LwjglRule implements ComponentMetadataRule { //val os: String, val arch: String, val classifier: String)
    private def nativeVariants = [
        [os: OperatingSystemFamily.LINUX,   arch: "arm32",  classifier: "natives-linux-arm32"],
        [os: OperatingSystemFamily.LINUX,   arch: "arm64",  classifier: "natives-linux-arm64"],
        [os: OperatingSystemFamily.WINDOWS, arch: "x86",    classifier: "natives-windows-x86"],
        [os: OperatingSystemFamily.WINDOWS, arch: "x86-64", classifier: "natives-windows"],
        [os: OperatingSystemFamily.MACOS,   arch: "x86-64", classifier: "natives-macos"]
    ]

    @Inject abstract ObjectFactory getObjects()

    void execute(ComponentMetadataContext context) {
        context.details.withVariant("runtime") {
            attributes {
                attributes.attribute(OperatingSystemFamily.OPERATING_SYSTEM_ATTRIBUTE, objects.named(OperatingSystemFamily, "none"))
                attributes.attribute(MachineArchitecture.ARCHITECTURE_ATTRIBUTE, objects.named(MachineArchitecture, "none"))
            }
        }
        nativeVariants.each { variantDefinition ->
            context.details.addVariant("${variantDefinition.classifier}-runtime", "runtime") {
                attributes {
                    attributes.attribute(OperatingSystemFamily.OPERATING_SYSTEM_ATTRIBUTE, objects.named(OperatingSystemFamily, variantDefinition.os))
                    attributes.attribute(MachineArchitecture.ARCHITECTURE_ATTRIBUTE, objects.named(MachineArchitecture, variantDefinition.arch))
                }
                withFiles {
                    addFile("${context.details.id.name}-${context.details.id.version}-${variantDefinition.classifier}.jar")
                }
            }
        }
    }
}

该规则与上面的 Quasar 库示例非常相似。只是这一次我们添加了五个不同的运行时变体,并且不需要对编译变体进行任何更改。运行时变体均基于现有运行时变体,我们不会更改任何现有信息。所有 Java 生态系统属性、依赖项和主 jar 文件仍然是每个运行时变体的一部分。我们只设置附加属性OPERATING_SYSTEM_ATTRIBUTE和 ,ARCHITECTURE_ATTRIBUTE它们被定义为 Gradle原生支持的一部分。我们添加相应的本机 jar 文件,以便每个运行时变体现在都带有两个文件:主 jar 和本机 jar。

在构建脚本中,我们现在可以请求特定的变体,如果需要更多信息来做出决定,Gradle 将失败并出现选择错误。

build.gradle.kts
configurations["runtimeClasspath"].attributes {
    attribute(OperatingSystemFamily.OPERATING_SYSTEM_ATTRIBUTE, objects.named("windows"))
}
dependencies {
    components {
        withModule<LwjglRule>("org.lwjgl:lwjgl")
    }
    implementation("org.lwjgl:lwjgl:3.2.3")
}
build.gradle
configurations["runtimeClasspath"].attributes {
    attribute(OperatingSystemFamily.OPERATING_SYSTEM_ATTRIBUTE, objects.named(OperatingSystemFamily, "windows"))
}
dependencies {
    components {
        withModule("org.lwjgl:lwjgl", LwjglRule)
    }
    implementation("org.lwjgl:lwjgl:3.2.3")
}
Gradle 无法选择变体,因为需要选择机器架构
> Could not resolve all files for configuration ':runtimeClasspath'.
   > Could not resolve org.lwjgl:lwjgl:3.2.3.
     Required by:
         project :
      > Cannot choose between the following variants of org.lwjgl:lwjgl:3.2.3:
          - natives-windows-runtime
          - natives-windows-x86-runtime

通过功能提供不同风格的库

由于很难将可选功能变体建模为具有 pom 元数据的单独 jar,因此库有时会使用不同的功能集组合不同的 jar。也就是说,您不必从不同的功能变体中组合您的库风格,而是选择预先组合的变体之一(在一个罐子中提供所有内容)。一个这样的库是著名的依赖注入框架 Guice,发布在Maven 中心,它提供了一个完整的风格(主 jar)和一个没有面向方面编程支持的简化变体(guice-4.2.2-no_aop.jar)。 pom 元数据中没有提到带有分类器的第二个变体。使用以下规则,我们基于该文件创建编译和运行时变体,并使其可以通过名为 的功能进行选择com.google.inject:guice-no_aop

build.gradle.kts
@CacheableRule
abstract class GuiceRule: ComponentMetadataRule {
    override fun execute(context: ComponentMetadataContext) {
        listOf("compile", "runtime").forEach { base ->
            context.details.addVariant("noAop${base.capitalize()}", base) {
                withCapabilities {
                    addCapability("com.google.inject", "guice-no_aop", context.details.id.version)
                }
                withFiles {
                    removeAllFiles()
                    addFile("guice-${context.details.id.version}-no_aop.jar")
                }
                withDependencies {
                    removeAll { it.group == "aopalliance" }
                }
            }
        }
    }
}
build.gradle
@CacheableRule
abstract class GuiceRule implements ComponentMetadataRule {
    void execute(ComponentMetadataContext context) {
        ["compile", "runtime"].each { base ->
            context.details.addVariant("noAop${base.capitalize()}", base) {
                withCapabilities {
                    addCapability("com.google.inject", "guice-no_aop", context.details.id.version)
                }
                withFiles {
                    removeAllFiles()
                    addFile("guice-${context.details.id.version}-no_aop.jar")
                }
                withDependencies {
                    removeAll { it.group == "aopalliance" }
                }
            }
        }
    }
}

新变体还删除了对标准化 aop 接口库的依赖aopalliance:aopalliance,因为这些变体显然不需要这一点。同样,这是无法用 pom 元数据表达的信息。我们现在可以选择一个guice-no_aop变体,并将获得正确的 jar 文件正确的依赖项。

build.gradle.kts
dependencies {
    components {
        withModule<GuiceRule>("com.google.inject:guice")
    }
    implementation("com.google.inject:guice:4.2.2") {
        capabilities { requireCapability("com.google.inject:guice-no_aop") }
    }
}
build.gradle
dependencies {
    components {
        withModule("com.google.inject:guice", GuiceRule)
    }
    implementation("com.google.inject:guice:4.2.2") {
        capabilities { requireCapability("com.google.inject:guice-no_aop") }
    }
}

添加缺失的功能来检测冲突

功能的另一种用法是表示两个不同的模块(例如log4jlog4j-over-slf4j)提供同一事物的替代实现。通过声明两者提供相同的功能,Gradle 在依赖关系图中仅接受其中之一。该示例以及如何使用组件元数据规则来处理该示例在特征建模部分中进行了详细描述。

使 Ivy 模块具有变体感知能力

具有 Ivy 元数据的模块默认没有变体。但是,Ivy 配置可以映射到变体,因为它addVariant(name, baseVariantOrConfiguration)接受作为基础发布的任何 Ivy 配置。例如,这可用于定义运行时和编译变体。可以在此处找到相应规则的示例。也可以使用 API 修改 Ivy 配置的 Ivy 详细信息(例如依赖项和文件)withVariant(configurationName)。但是,修改 Ivy 配置上的属性或功能没有任何效果。

对于 Ivy 特定的用例,组件元数据规则 API 还提供对仅在 Ivy 元数据中找到的其他详细信息的访问。这些可以通过IvyModuleDescriptor接口获得并且可以使用getDescriptor(IvyModuleDescriptor)ComponentMetadataContext进行访问。

build.gradle.kts
@CacheableRule
abstract class IvyComponentRule : ComponentMetadataRule {
    override fun execute(context: ComponentMetadataContext) {
        val descriptor = context.getDescriptor(IvyModuleDescriptor::class)
        if (descriptor != null && descriptor.branch == "testing") {
            context.details.status = "rc"
        }
    }
}
build.gradle
@CacheableRule
abstract class IvyComponentRule implements ComponentMetadataRule {
    void execute(ComponentMetadataContext context) {
        def descriptor = context.getDescriptor(IvyModuleDescriptor)
        if (descriptor != null && descriptor.branch == "testing") {
            context.details.status = "rc"
        }
    }
}

使用 Maven 元数据进行过滤

对于 Maven 特定用例,组件元数据规则 API 还提供对仅在 POM 元数据中找到的其他详细信息的访问。这些可以通过PomModuleDescriptor接口获得并且可以使用getDescriptor(PomModuleDescriptor)ComponentMetadataContext进行访问。

build.gradle.kts
@CacheableRule
abstract class MavenComponentRule : ComponentMetadataRule {
    override fun execute(context: ComponentMetadataContext) {
        val descriptor = context.getDescriptor(PomModuleDescriptor::class)
        if (descriptor != null && descriptor.packaging == "war") {
            // ...
        }
    }
}
build.gradle
@CacheableRule
abstract class MavenComponentRule implements ComponentMetadataRule {
    void execute(ComponentMetadataContext context) {
        def descriptor = context.getDescriptor(PomModuleDescriptor)
        if (descriptor != null && descriptor.packaging == "war") {
            // ...
        }
    }
}

修改组件级别的元数据以进行对齐

虽然上面的所有示例都对组件的变体进行了修改,但还可以对组件本身的元数据进行一组有限的修改。该信息可以影响依赖项解析期间模块的版本选择过程,该过程在选择组件的一个或多个变体之前执行。

该组件上可用的第一个 API 是belongsTo()创建虚拟平台,用于在没有 Gradle 模块元数据的情况下对齐多个模块的版本。有关对齐未随 Gradle 发布的模块版本的部分对此进行了详细解释。

修改组件级别的元数据以根据状态选择版本

Gradle 和 Gradle 模块元数据还允许在整个组件而不是单个变体上设置属性。这些属性中的每一个都带有特殊的语义,因为它们影响在变体选择之前完成的版本选择。虽然变体选择可以处理任何自定义属性,但版本选择仅考虑实现特定语义的属性。目前,这里唯一有意义的属性是org.gradle.status。因此,建议仅在组件级别修改此属性(如果有)。setStatus(value)为此可以使用专用 API 。withAllVariants { attributes {} }应该使用修改组件所有变体的另一个属性。

当解析最新版本选择器时,会考虑模块的状态。具体来说,将解析为具有状态或更成熟状态latest.someStatus的最高模块版本。someStatus例如,latest.integration将选择最高的模块版本,无论其状态如何(因为integration是最不成熟的状态,如下所述),而latest.release将选择状态为的最高模块版本release

通过API 更改模块的状态方案可以影响状态的解释setStatusScheme(valueList)。这个概念模拟了模块随着时间的推移通过不同的出版物过渡的不同成熟度级别。默认状态方案(从最不成熟状态到最成熟状态排序)为integration, milestone, release。必须将该org.gradle.status属性设置为组件状态方案中的值之一。因此,每个组件始终具有根据元数据确定的状态,如下所示:

  • org.gradle.statusGradle 模块元数据:为组件上的属性发布的值

  • ivy元数据:status在ivy.xml中定义,integration如果缺失则默认为

  • Pom 元数据:integration适用于具有 SNAPSHOT 版本的模块,release适用于所有其他模块

以下示例演示了latest基于在适用于所有模块的组件元数据规则中声明的自定义状态方案的选择器:

build.gradle.kts
@CacheableRule
abstract class CustomStatusRule : ComponentMetadataRule {
    override fun execute(context: ComponentMetadataContext) {
        context.details.statusScheme = listOf("nightly", "milestone", "rc", "release")
        if (context.details.status == "integration") {
            context.details.status = "nightly"
        }
    }
}

dependencies {
    components {
        all<CustomStatusRule>()
    }
    implementation("org.apache.commons:commons-lang3:latest.rc")
}
build.gradle
@CacheableRule
abstract class CustomStatusRule implements ComponentMetadataRule {
    void execute(ComponentMetadataContext context) {
        context.details.statusScheme = ["nightly", "milestone", "rc", "release"]
        if (context.details.status == "integration") {
            context.details.status = "nightly"
        }
    }
}

dependencies {
    components {
        all(CustomStatusRule)
    }
    implementation("org.apache.commons:commons-lang3:latest.rc")
}

与默认方案相比,该规则插入了新的状态rc并替换integrationnightly。具有状态的现有模块integration被映射到nightly.