从仓库中拉取的每个组件都包含元数据,例如其组、名称、版本以及它提供的各种变体及其构件和依赖项。

有时,此元数据可能不完整或不正确。

Gradle 提供了一个 API 来解决此问题,允许您直接在构建脚本中编写组件元数据规则。这些规则在模块的元数据下载后应用,但在依赖项解析中使用之前应用。

编写组件元数据规则

组件元数据规则在构建脚本或设置脚本的dependencies块的components部分中应用。

这些规则可以通过两种方式定义

  1. 作为 Action 内联: 直接在 components 部分中。

  2. 作为单独的类: 实现 ComponentMetadataRule 接口。

虽然内联 Action 方便快速实验,但通常建议将规则定义为单独的类。

作为隔离类编写的规则可以使用 @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")
}

在此示例中,TargetJvmVersionRule 类实现了ComponentMetadataRule,并使用ActionConfiguration进一步配置。

Gradle 强制隔离 ComponentMetadataRule 的实例,要求所有参数都必须是 Serializable 或可识别的 Gradle 类型。

此外,诸如 ObjectFactory 之类的服务可以使用 @Inject 注入到您的规则构造函数中。

组件元数据规则可以使用 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 支持的功能。使用元数据规则和在构建脚本中定义依赖项/构件之间的主要区别在于,组件元数据规则直接操作变体,而构建脚本通常同时影响多个变体(例如,api 依赖项应用于 Java 库的 apiruntime 变体)。

变体可以通过以下方法修改

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

  • withVariant(name):修改由其名称标识的特定变体。

  • addVariant(name)addVariant(name, base):从头开始添加新变体或从现有变体(base)复制详细信息。

以下变体详细信息可以修改

  • 属性:使用 attributes {} 块调整标识变体的属性

  • 功能:使用 withCapabilities {} 块定义变体提供的功能

  • 依赖项:使用 withDependencies {} 块管理变体的依赖项,包括富版本约束。

  • 依赖项约束:使用 withDependencyConstraints {} 块定义变体的依赖项约束,包括富版本。

  • 已发布文件:使用 withFiles {} 块指定构成变体内容的文件位置。

此外,可以更改几个组件级属性

  • 组件属性:这里唯一有意义的属性org.gradle.status

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

  • BelongsTo 属性:用于通过虚拟平台进行版本对齐

模块元数据的格式会影响它如何映射到以变体为中心的表示

  • Gradle 模块元数据:数据结构类似于模块的 .module 文件。

  • POM 元数据:对于使用 .pom 元数据发布的模块,固定的变体如“将 POM 文件映射到变体”部分所述推导。

  • Ivy 元数据:如果模块使用 ivy.xml 文件发布,则可以访问 Ivy 配置代替变体。它们的依赖项、约束和文件可以修改。您还可以使用 addVariant(name, baseVariantOrConfiguration) 从 Ivy 配置中派生变体,例如为 Java 库插件定义compileruntime 变体

在使用组件元数据规则调整模块元数据之前,请确定该模块是使用 Gradle 模块元数据.module 文件)还是传统元数据.pomivy.xml)发布的

  • 带有 Gradle 模块元数据的模块:这些模块通常具有完整的元数据,但仍可能出现问题。仅当您明确识别出元数据存在问题时才应用组件元数据规则。对于依赖项解析问题,首先考虑使用带有富版本的依赖项约束。如果您正在开发库,请注意依赖项约束作为您自己的库元数据的一部分发布,从而更容易与消费者共享解决方案。相反,组件元数据规则仅适用于您自己的构建。

  • 带有传统元数据的模块.pomivy.xml):这些模块更有可能具有不完整的元数据,因为这些格式不支持变体和依赖项约束等功能。此类模块可能具有被省略或错误定义为依赖项的变体或约束。在以下部分中,我们将探讨具有不完整元数据的 OSS 模块示例以及添加缺失信息的规则。

通常,您应该考虑您正在编写的规则是否在您的构建上下文之外也有效。也就是说,如果将该规则应用于使用它所影响的模块的任何其他构建,它是否仍然会产生正确和有用的结果?

修复不正确的依赖项详细信息

考虑发布在 Maven Central 上的 Jaxen XPath Engine (版本 1.1.3)。它的 pom 文件在 compile 范围内声明了几个不必要的依赖项,这些依赖项后来在版本 1.1.4 中被删除。如果您需要使用版本 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 中,dom4jjdomxerces 依赖项仍然存在,但被标记为可选。可选依赖项不会被 Gradle 或 Maven 自动处理,因为它们表示需要额外依赖项的功能变体。但是,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) 方法创建了一个名为 runtime-dom4j 的新变体。此变体表示一个可选功能,由功能 jaxen-dom4j 定义。然后,我们为此功能添加了所需的依赖项 dom4j:dom4j:1.6.1

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") }
    }
}

通过应用这些规则,当需要 jaxen-dom4j 功能时,Gradle 会使用丰富的元数据正确解析可选依赖项。

明确将变体发布为带分类器的 jar

在现代构建中,变体通常作为单独的构件发布,每个构件都由其自己的 jar 文件表示。例如,库可能为不同的 Java 版本提供不同的 jar,确保根据环境在运行时或编译时使用正确的版本。

例如,异步编程库 Quasar 的版本 0.7.9,发布在 Maven Central 上,包含 quasar-core-0.7.9.jarquasar-core-0.7.9-jdk8.jar。使用分类器(例如 jdk8)发布 jar 在 Maven 仓库中很常见。但是,Maven 和 Gradle 元数据都没有提供有关这些分类 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)
                }
            }
        }
    }
}

在这种情况下,jdk8 分类器清楚地指示了目标 Java 版本,这与 Java 生态系统中的已知属性相对应。由于我们需要 Java 8 的编译和运行时变体,我们使用现有的编译和运行时变体作为基础创建了两个新变体。这确保所有其他 Java 生态系统属性都设置正确,并且依赖项被继承。

我们将 TARGET_JVM_VERSION_ATTRIBUTE 分配给这两个新变体的 8,使用 removeAllFiles() 删除任何现有文件,然后使用 addFile() 添加 jdk8 jar。删除文件是必要的,因为对主 jar quasar-core-0.7.9.jar 的引用是从基础变体复制的。

最后,我们使用 attribute(TARGET_JVM_VERSION_ATTRIBUTE, 7) 丰富了现有的编译和运行时变体,并告知它们的目标是 Java 7。

通过这些更改,您现在可以请求编译类路径上所有依赖项的 Java 8 版本,Gradle 将自动选择最合适的变体。在 Quasar 的情况下,这将是 jdk8Compile 变体,它公开了 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")
}

通过此配置,Gradle 将为编译类路径选择 Quasar 的 Java 8 变体。

明确版本中编码的变体

发布同一库的多个替代方案的另一种解决方案是使用版本模式,就像流行的 Guava 库所做的那样。在这里,每个新版本都发布两次,方法是将分类器附加到版本而不是 jar 构件。例如,在 Guava 28 的情况下,我们可以在 Maven Central 上找到 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 {
                        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 Central,除了主 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 {
                attribute(OperatingSystemFamily.OPERATING_SYSTEM_ATTRIBUTE, objects.named("none"))
                attribute(MachineArchitecture.ARCHITECTURE_ATTRIBUTE, objects.named("none"))
            }
        }
        nativeVariants.forEach { variantDefinition ->
            context.details.addVariant("${variantDefinition.classifier}-runtime", "runtime") {
                attributes {
                    attribute(OperatingSystemFamily.OPERATING_SYSTEM_ATTRIBUTE, objects.named(variantDefinition.os))
                    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 库示例非常相似。只是这次我们添加了五个不同的运行时变体,并且编译变体不需要任何更改。运行时变体都基于现有的 runtime 变体,我们不更改任何现有信息。所有 Java 生态系统属性、依赖项和主 jar 文件仍然是每个运行时变体的一部分。我们只设置了额外的属性 OPERATING_SYSTEM_ATTRIBUTEARCHITECTURE_ATTRIBUTE,这些属性是 Gradle 原生支持的一部分。并且我们添加了相应的原生 jar 文件,因此每个运行时变体现在都带有两个文件:主 jar 和原生 jar。

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

Gradle 能够理解缺少单个属性以消除歧义的常见情况。在这种情况下,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 无法选择变体,因为需要选择机器架构

failRuntimeClasspathResolve.out
> Could not resolve all files for configuration ':runtimeClasspath'.
   > Could not resolve org.lwjgl:lwjgl:3.2.3.
     Required by:
         root project 'component-metadata-rules'
      > The consumer was configured to find a library for use during runtime, compatible with Java 11, packaged as a jar, preferably optimized for standard JVMs, and its dependencies declared externally, as well as attribute 'org.gradle.native.operatingSystem' with value 'windows'. There are several available matching variants of org.lwjgl:lwjgl:3.2.3
        The only attribute distinguishing these variants is 'org.gradle.native.architecture'. Add this attribute to the consumer's configuration to resolve the ambiguity:
          - Value: 'x86-64' selects variant: 'natives-windows-runtime'
          - Value: 'x86' selects variant: 'natives-windows-x86-runtime'

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

因为很难将可选功能变体建模为带有 pom 元数据的单独 jar,所以库有时包含具有不同功能集的 jar。也就是说,您不是从不同的功能变体中组合您的库风味,而是选择一个预先组合的变体(在一个 jar 中提供所有内容)。其中一个库是著名的依赖注入框架 Guice,发布在 Maven Central 上,它提供了一个完整风味(主 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 配置作为基础。例如,这可以用于定义运行时和编译变体。可以在此处找到相应规则的示例。Ivy 配置的 Ivy 详细信息(例如依赖项和文件)也可以使用 withVariant(configurationName) API 进行修改。但是,修改 Ivy 配置上的属性或功能无效。

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

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 接口获得,并且可以使用 ComponentMetadataContext 上的 getDescriptor(PomModuleDescriptor) 进行访问。

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

org.gradle.status 模块属性表示模块或库的生命周期状态或成熟度级别

  1. integration:这表示模块处于活跃开发中,可能不稳定。

  2. milestone:具有此状态的模块比标记为 integration 的模块更成熟。

  3. release:此状态表示模块稳定并正式发布。

因此,建议仅在组件级别修改此属性(如果有的话)。为此提供了专用的 API setStatus(value)。要修改组件所有变体的另一个属性,应改用 withAllVariants { attributes {} }

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

可以通过 setStatusScheme(valueList) API 更改模块的状态方案来影响状态的解释。此概念模拟模块随着时间的推移通过不同发布而经历的不同成熟度级别。默认状态方案,从最低到最高成熟度状态排序,是 integrationmilestonereleaseorg.gradle.status 属性必须设置为组件状态方案中的一个值。因此,每个组件始终具有一个状态,该状态从元数据中确定如下

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

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

  • Pom 元数据:带有 SNAPSHOT 版本的模块为 integration,所有其他模块为 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 并将 integration 替换为 nightly。现有状态为 integration 的模块被映射到 nightly