从存储库中提取的每个模块都与元数据相关联,例如其组、名称、版本以及它提供的不同变体及其工件和依赖项。有时,这些元数据不完整或不正确。为了在构建脚本中操作此类不完整的元数据,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 来定义,该接口只有一个 execute 方法,接收一个 ComponentMetadataContext 实例作为参数。在本例中,该规则还通过 ActionConfiguration 进行进一步配置。这是通过在 ComponentMetadataRule 的实现中添加一个构造函数来支持的,该构造函数接受配置的参数和需要注入的服务。

Gradle 强制隔离 ComponentMetadataRule 的实例。这意味着所有参数必须是 Serializable 或已知的 Gradle 类型,这些类型可以被隔离。

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

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

在中央位置声明规则

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

除了为每个子项目单独声明规则之外,还可以为整个构建在 settings.gradle(.kts) 文件中声明规则。在 settings 中声明的规则是应用于每个项目的常规规则:如果项目没有声明任何规则,则将使用 settings 脚本中的规则。

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 库的apiruntime 变体中,由jar 任务生成的工件也会添加到这两个变体中)。

可以通过以下方法解决要修改的变体。

  • 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 文件到变体的映射 部分中所述,推导出一些固定变体。

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

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

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

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

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

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

修复错误的依赖项详细信息

让我们以在 Maven 中央仓库 上发布 Jaxen XPath 引擎为例。版本 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 依赖项仍然存在,但被标记为 可选。pom 文件中的可选依赖项不会被 Gradle 或 Maven 自动处理。原因是它们表明 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 的变体明确化

在之前的例子中,所有变体,包括“主要变体”和可选功能,都打包在一个 jar 文件中。但通常情况下,会将某些变体发布为单独的文件。特别是当变体是互斥的时,即它们不是功能变体,而是提供替代选择的不同变体。一个例子是所有基于 pom 的库都具有的运行时编译变体,Gradle 可以根据当前任务选择其中一个。在 Java 生态系统中经常发现的另一种变体是针对不同 Java 版本的 jar 文件。

例如,我们看一下异步编程库 Quasar 的 0.7.9 版本,它发布在 Maven 中央仓库 上。如果我们检查目录列表,会发现除了 quasar-core-0.7.9.jar 之外,还发布了一个 quasar-core-0.7.9-jdk8.jar。在 Maven 仓库中,发布带有分类器(这里为jdk8)的额外 jar 文件是一种常见做法。虽然 Maven 和 Gradle 都允许你通过分类器引用这些 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_ATTRIBUTE 设置为 8,用于这两个变体,使用 removeAllFiles() 删除新变体中任何现有的文件,并使用 addFile() 添加 jdk8 jar 文件。需要 removeAllFiles(),因为对主 jar quasar-core-0.7.5.jar 的引用是从相应的基变体复制过来的。

我们还用它们针对 Java 7 的信息来丰富现有的编译运行时变体——attribute(TARGET_JVM_VERSION_ATTRIBUTE, 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")
}

将编码在版本中的变体显式化

另一种为同一个库发布多个备选方案的解决方案是使用版本控制模式,就像流行的 Guava 库所做的那样。在这里,每个新版本都会通过在 jar 文件的版本号后面添加分类器来发布两次。例如,在 Guava 28 的情况下,我们可以在 Maven 中央仓库 中找到 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 也用于将库的某些部分(例如原生代码)与主工件分开,这些部分存在多个备选方案。例如,Lightweight Java Game Library (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 库示例非常相似。只是这次我们添加了五个不同的运行时变体,并且不需要对编译变体进行任何更改。所有运行时变体都基于现有的 runtime 变体,我们不会更改任何现有信息。所有 Java 生态系统属性、依赖项和主 jar 文件都保留在每个运行时变体中。我们只设置了额外的属性 OPERATING_SYSTEM_ATTRIBUTEARCHITECTURE_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 组合在一起。也就是说,您不是从不同的功能变体中组合库的版本,而是选择一个预先组合的版本(在一个 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 配置。例如,这可以用来定义运行时和编译变体。相应的规则示例可以在此处找到。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 模块元数据还允许在整个组件上设置属性,而不是单个变体。这些属性中的每一个都具有特殊的语义,因为它们会影响版本选择,而版本选择是在变体选择之前完成的。虽然变体选择可以处理任何自定义属性,但版本选择只考虑具有特定语义的属性。目前,这里唯一有意义的属性是org.gradle.status。因此,建议只修改组件级别的此属性(如果有)。为此提供了一个专用 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