从仓库中拉取的每个组件都包含元数据,例如其 group、name、version 以及它提供的各种变体及其 artifact 和依赖。

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

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

编写组件元数据规则

组件元数据规则应用于构建脚本或 settings 脚本中 components 部分的 dependencies 块内。

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

  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 类型。

此外,可以使用 @InjectObjectFactory 等服务注入到您的规则构造函数中。

组件元数据规则可以使用 all(rule) 应用于所有模块,或者使用 withModule(groupAndName, rule) 应用于特定模块。通常,规则是为丰富特定模块的元数据而定制的,因此首选 withModule API。

在一个中心位置声明规则

在 settings 中声明组件元数据规则是一项孵化中的功能。

组件元数据规则可以在 settings.gradle(.kts) 文件中为整个构建声明,而不是在每个子项目中单独声明。在 settings 中声明的规则默认应用于所有项目,除非被项目特定的规则覆盖。

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

默认情况下,项目特定规则优先于 settings 规则。但是,此行为可以调整。

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 支持的功能。使用元数据规则与在构建脚本中定义依赖/artifact 的关键区别在于,组件元数据规则直接操作变体,而构建脚本通常一次影响多个变体(例如,api 依赖会应用于 Java 库的 apiruntime 变体)。

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

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

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

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

以下变体详情可以修改:

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

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

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

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

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

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

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

  • Status Scheme: 影响在版本选择期间如何解释 org.gradle.status 属性。

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

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

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

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

  • Ivy 元数据: 如果模块是使用 ivy.xml 文件发布的,则可以访问 Ivy configuration 来代替变体。它们的依赖、约束和文件可以修改。您还可以使用 addVariant(name, baseVariantOrConfiguration) 从 Ivy configuration 中派生变体,例如为 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 的变体

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

例如,发布在 Maven Central 上的异步编程库 Quasar 的版本 0.7.9 包含 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 的 compile 和 runtime 变体,我们以现有的 compile 和 runtime 变体为基础创建了两个新变体。这确保了所有其他 Java 生态系统属性设置正确,并且依赖被继承。

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

最后,我们使用 attribute(TARGET_JVM_VERSION_ATTRIBUTE, 7) 丰富现有的 compile 和 runtime 变体,说明它们针对 Java 7。

通过这些更改,您现在可以请求编译 classpath 上所有依赖的 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 将为编译 classpath 选择 Quasar 的 Java 8 变体。

明确版本中编码的变体

另一种发布同一库多个替代方案的解决方案是使用流行的 Guava 库所采用的版本命名模式。在这里,每个新版本发布两次,通过将分类器附加到版本而不是 Jar artifact。例如,对于 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 {
                        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 版本添加了 runtime 和 compile 变体。然而,在 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 也用于将库中存在多种替代方案的部分(例如原生代码)与主 artifact 分离。例如,轻量级 Java 游戏库 (LWGJ) 就是这样做的,它向 Maven central 发布了多个特定于平台的 Jar,运行时总是需要其中一个 Jar,以及主 Jar。在 POM 元数据中无法传达此信息,因为元数据中没有通过元数据关联多个 artifact 的概念。在 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 库示例非常相似。不同之处在于这次我们添加了五种不同的 runtime 变体,而 compile 变体无需更改。所有 runtime 变体都基于现有的 runtime 变体,并且我们不更改任何现有信息。所有 Java 生态系统属性、依赖和主 Jar 文件都保留在每个 runtime 变体中。我们只设置了额外的属性 OPERATING_SYSTEM_ATTRIBUTEARCHITECTURE_ATTRIBUTE,这些属性是 Gradle 原生支持的一部分。我们还添加了相应的原生 Jar 文件,以便每个 runtime 变体现在包含两个文件:主 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 未能选择变体,因为需要选择一个机器架构。

> Could not resolve all files for configuration ':runtimeClasspath'.
   > Could not resolve org.lwjgl:lwjgl:3.2.3.
     Required by:
         project :
      > 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 元数据中没有提及这个带分类器的第二个变体。通过以下规则,我们基于该文件创建 compile 和 runtime 变体,并通过名为 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" }
                }
            }
        }
    }
}

新变体也移除了对标准化面向切面编程接口库 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 configurations 可以映射到变体,因为 addVariant(name, baseVariantOrConfiguration) 接受任何已发布为基础的 Ivy configuration。这可以用于例如定义 runtime 和 compile 变体。相应的规则示例可以在此处找到。Ivy configuration 的 Ivy 详情(例如依赖和文件)也可以使用 withVariant(configurationName) API 修改。但是,修改 Ivy configuration 上的属性或能力没有效果。

对于非常特定于 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