Gradle 支持**特性变体**的概念,允许将单个库拆分为多个相关但独立的模块。每个**特性变体**可以声明自己的一组依赖项,并且可以与主库一同单独消费。

例如

producer/build.gradle.kts
plugins {
    id("java-library")
}

java {
    registerFeature("jsonSupport") {
        usingSourceSet(sourceSets.create("jsonSupport"))
    }
}

dependencies {
    "jsonSupportApi"("com.fasterxml.jackson.core:jackson-databind:2.16.0")
}
consumer/build.gradle.kts
plugins {
    id("application")
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.example:library:1.0") {
        capabilities {
            requireCapability("org.example:library-json-support")
        }
    }
}

为什么要使用特性变体?

特性变体相比传统的依赖管理提供了几个优势

  • 改进的模块化:在不同的库功能之间有清晰的边界。

  • 细粒度的依赖管理:消费者只包含他们特别需要的依赖项。

  • 支持多种变体:单个库可以暴露针对不同用例定制的变体(例如,debug 版本 vs. release 版本构建)。

常见用例包括

  • 提供可选依赖项(是 Maven 可选依赖项 的强大替代方案)。

  • 提供运行时特性的多个互斥实现,要求用户精确选择一个变体。

  • 支持各自具有独特依赖项的可选运行时特性。

  • 分发补充模块,例如测试 fixture 或集成支持。

  • 启用可以与主库制品可选地一同包含的其他制品。

步骤 1:通过能力 (Capabilities) 选择特性

依赖项通常使用被称为 GAV(group、artifact、version)的坐标声明,这些坐标标识了组件。然而,单个组件可以提供多个变体,每个变体适合不同的用途——例如编译或运行时执行。

每个变体提供一组能力 (capabilities),这些能力也通过 GAV 坐标标识,但最好理解为特性描述,例如

  • “我提供一个 SLF4J 绑定”

  • “我提供对 MySQL 的运行时支持”

  • “我提供一个 Groovy 运行时”

重要的是要注意

  • 默认情况下,每个变体都提供一个与组件的 GAV 坐标相匹配的能力 (capability)

  • 在依赖图中,提供相同能力的两个变体不能共存。

  • 如果单个组件的多个变体提供了不同的能力,它们可以共存。

例如,一个典型的 Java 库有 API 和运行时变体,两者都提供相同的能力。因此,在依赖图中同时包含这两个变体是错误的。然而,只要这些变体声明了不同的能力,就可以同时使用运行时和 test-fixtures 运行时变体。

为了实现这一点,消费者必须显式声明单独的依赖项

  • 主库的一个依赖项(“main”特性)。

  • 另一个显式要求附加特性(例如 test fixtures)能力的依赖项。

java {
    registerFeature("testFixtures") {
        usingSourceSet(sourceSets.create("testFixtures"))
        capability("com.example", "library-test-fixtures", version.toString())
    }
}
虽然解析引擎独立于生态系统支持多变体组件,但**特性变体**目前**仅由 Java 插件支持**。

步骤 2:注册特性

可以通过应用 java-library 插件来声明特性。

以下代码演示了如何声明一个名为 mongodbSupport 的特性

build.gradle.kts
sourceSets {
    create("mongodbSupport") {
        java {
            srcDir("src/mongodb/java")
        }
    }
}

java {
    registerFeature("mongodbSupport") {
        usingSourceSet(sourceSets["mongodbSupport"])
    }
}
build.gradle
sourceSets {
    mongodbSupport {
        java {
            srcDir 'src/mongodb/java'
        }
    }
}

java {
    registerFeature('mongodbSupport') {
        usingSourceSet(sourceSets.mongodbSupport)
    }
}

Gradle 将自动为您设置许多东西,这与 Java Library Plugin 设置配置的方式非常相似。

创建特性变体时,Gradle 会自动配置以下依赖范围

  • featureNameApi 用于特性的 API 依赖项。

  • featureNameImplementation 用于特定于实现的依赖项。

  • featureNameRuntimeOnly 用于仅运行时依赖项。

  • featureCompileOnly 用于仅编译时依赖项。

在此示例中,名为“mongodbSupport”的特性会自动创建这些配置

  • mongodbSupportApi - 用于*声明此特性的 API 依赖项*

  • mongodbSupportImplementation - 用于*声明此特性的实现依赖项*

  • mongodbSupportRuntimeOnly - 用于*声明此特性的仅运行时依赖项*

  • mongodbSupportCompileOnly - 用于*声明此特性的仅编译时依赖项*

此外,Gradle 还公开了两个特定于变体的配置供外部消费

  • mongodbSupportRuntimeElements - 消费者用于获取此特性的制品和 API 依赖项

  • mongodbSupportApiElements - 消费者用于获取此特性的制品和运行时依赖项

特性变体应具有一个名称相同的相应*源集*。

Gradle 会为每个特性的源集自动创建一个 Jar 任务,并使用与特性名称匹配的分类器 (classifier)。

注册特性时,请勿使用 *main* 源集。此行为将在未来版本的 Gradle 中弃用。

大多数用户只会关心依赖范围配置,用于声明此特性的特定依赖项

build.gradle.kts
dependencies {
    "mongodbSupportImplementation"("org.mongodb:mongodb-driver-sync:3.9.1")
}
build.gradle
dependencies {
    mongodbSupportImplementation 'org.mongodb:mongodb-driver-sync:3.9.1'
}

按照约定,Gradle 使用与主组件相同的 group 和 version 映射特性变体的能力,而能力的名称则由主组件名称后跟一个 - 和特性名称的 kebab-case 版本构成。

例如,如果您的组件具有

  • group: org.gradle.demo

  • name: provider

  • version: 1.0

并且您定义了一个名为 mongodbSupport 的特性,则该特性的能力将是

  • org.gradle.demo:provider-mongodb-support:1.0

如果您选择自定义能力名称或添加其他能力,建议遵循此约定。

步骤 3:发布特性

仅支持使用 maven-publishivy-publish 插件发布特性变体。

Java Library 插件会自动为您注册其他变体,除了标准的发布设置外,无需额外配置

build.gradle.kts
plugins {
    `java-library`
    `maven-publish`
}
// ...
publishing {
    publications {
        create("myLibrary", MavenPublication::class.java) {
            from(components["java"])
        }
    }
}
build.gradle
plugins {
    id 'java-library'
    id 'maven-publish'
}
// ...
publishing {
    publications {
        myLibrary(MavenPublication) {
            from components.java
        }
    }
}

根据所使用的元数据格式,发布特性可能会有所不同

  • 使用 Gradle 模块元数据,所有特性都得到完全保留,消费者可以充分利用特性变体。

  • 使用 POM 元数据(Maven),特性被表示为**可选依赖项**,特性制品会使用不同的分类器 (classifier) 发布。

  • 使用 Ivy 元数据,特性作为附加配置发布,默认配置不会自动包含这些附加配置。

添加 Javadoc 和 Sources JAR

主要的 Javadoc 和 sources JARs 类似,您可以配置特性变体来生成自己的 Javadoc 和 sources JARs

build.gradle.kts
java {
    registerFeature("mongodbSupport") {
        usingSourceSet(sourceSets["mongodbSupport"])
        withJavadocJar()
        withSourcesJar()
    }
}
build.gradle
java {
    registerFeature('mongodbSupport') {
        usingSourceSet(sourceSets.mongodbSupport)
        withJavadocJar()
        withSourcesJar()
    }
}

步骤 4:特性依赖

消费特性变体时,重要的是要注意特性的支持可能是有限的或“有损的”,具体取决于特性如何发布。消费者项目可以在这些条件下依赖特性变体

  • 在多项目 Gradle 构建中使用项目依赖。

  • 使用 Gradle 模块元数据,这必须由生产者显式发布。

  • 使用 Ivy 元数据,显式指定对表示所需特性的配置的依赖项。

消费者通过显式要求其能力来声明特定的特性依赖项。例如,如果生产者定义了一个用于“MySQL 支持”的特性变体,如下所示

build.gradle.kts
group = "org.gradle.demo"

sourceSets {
    create("mysqlSupport") {
        java {
            srcDir("src/mysql/java")
        }
    }
}

java {
    registerFeature("mysqlSupport") {
        usingSourceSet(sourceSets["mysqlSupport"])
    }
}

dependencies {
    "mysqlSupportImplementation"("mysql:mysql-connector-java:8.0.14")
}
build.gradle
group = 'org.gradle.demo'

sourceSets {
    mysqlSupport {
        java {
            srcDir 'src/mysql/java'
        }
    }
}

java {
    registerFeature('mysqlSupport') {
        usingSourceSet(sourceSets.mysqlSupport)
    }
}

dependencies {
    mysqlSupportImplementation 'mysql:mysql-connector-java:8.0.14'
}

消费者项目可以通过要求其能力来显式依赖“MySQL 支持”特性

build.gradle.kts
dependencies {
    // This project requires the main producer component
    implementation(project(":producer"))

    // But we also want to use its MySQL support
    runtimeOnly(project(":producer")) {
        capabilities {
            requireCapability("org.gradle.demo:producer-mysql-support")
        }
    }
}
build.gradle
dependencies {
    // This project requires the main producer component
    implementation(project(":producer"))

    // But we also want to use its MySQL support
    runtimeOnly(project(":producer")) {
        capabilities {
            requireCapability("org.gradle.demo:producer-mysql-support")
        }
    }
}

此设置会自动在消费者的运行时 classpath 中包含 mysql-connector-java 依赖项。如果多个依赖项被分组在特性变体下,当需要该能力时,所有这些依赖项都会被包含。

类似地,当使用 Gradle 模块元数据 发布具有特性变体的外部库时,消费者可以显式依赖这些外部特性

build.gradle.kts
dependencies {
    // This project requires the main producer component
    implementation("org.gradle.demo:producer:1.0")

    // But we also want to use its MongoDB support
    runtimeOnly("org.gradle.demo:producer:1.0") {
        capabilities {
            requireCapability("org.gradle.demo:producer-mongodb-support")
        }
    }
}
build.gradle
dependencies {
    // This project requires the main producer component
    implementation('org.gradle.demo:producer:1.0')

    // But we also want to use its MongoDB support
    runtimeOnly('org.gradle.demo:producer:1.0') {
        capabilities {
            requireCapability("org.gradle.demo:producer-mongodb-support")
        }
    }
}

步骤 5:处理互斥变体

使用**能力 (capabilities)** 来管理**特性变体**提供了一种处理变体之间兼容性的精确方法。

需要记住的关键规则是

在依赖图中,没有任何两个变体可以提供相同的能力。

这条规则允许 Gradle 在互斥变体之间强制执行排他性。

例如,假设您有一个库提供数据库特性的多个互斥实现(例如 MySQLPostgreSQLMongoDB)。通过为每个变体分配一个共享能力,您可以确保这些变体不能在同一个依赖图中共存。

例如,生产者可能定义变体如下

build.gradle.kts
java {
    registerFeature("mysqlSupport") {
        usingSourceSet(sourceSets["mysqlSupport"])
        capability("org.gradle.demo", "producer-db-support", "1.0")
        capability("org.gradle.demo", "producer-mysql-support", "1.0")
    }
    registerFeature("postgresSupport") {
        usingSourceSet(sourceSets["postgresSupport"])
        capability("org.gradle.demo", "producer-db-support", "1.0")
        capability("org.gradle.demo", "producer-postgres-support", "1.0")
    }
    registerFeature("mongoSupport") {
        usingSourceSet(sourceSets["mongoSupport"])
        capability("org.gradle.demo", "producer-db-support", "1.0")
        capability("org.gradle.demo", "producer-mongo-support", "1.0")
    }
}

dependencies {
    "mysqlSupportImplementation"("mysql:mysql-connector-java:8.0.14")
    "postgresSupportImplementation"("org.postgresql:postgresql:42.2.5")
    "mongoSupportImplementation"("org.mongodb:mongodb-driver-sync:3.9.1")
}
build.gradle
java {
    registerFeature('mysqlSupport') {
        usingSourceSet(sourceSets.mysqlSupport)
        capability('org.gradle.demo', 'producer-db-support', '1.0')
        capability('org.gradle.demo', 'producer-mysql-support', '1.0')
    }
    registerFeature('postgresSupport') {
        usingSourceSet(sourceSets.postgresSupport)
        capability('org.gradle.demo', 'producer-db-support', '1.0')
        capability('org.gradle.demo', 'producer-postgres-support', '1.0')
    }
    registerFeature('mongoSupport') {
        usingSourceSet(sourceSets.mongoSupport)
        capability('org.gradle.demo', 'producer-db-support', '1.0')
        capability('org.gradle.demo', 'producer-mongo-support', '1.0')
    }
}

dependencies {
    mysqlSupportImplementation 'mysql:mysql-connector-java:8.0.14'
    postgresSupportImplementation 'org.postgresql:postgresql:42.2.5'
    mongoSupportImplementation 'org.mongodb:mongodb-driver-sync:3.9.1'
}

这里

  • mysql-support 变体提供能力:db-supportmysql-support

  • postgres-support 变体提供能力:db-supportpostgres-support

  • mongo-support 变体提供能力:db-supportmongo-support

如果消费者尝试包含多个冲突的特性,例如同时包含 MySQLPostgreSQL 支持

build.gradle.kts
dependencies {
    // This project requires the main producer component
    implementation(project(":producer"))

    // Let's try to ask for both MySQL and Postgres support
    runtimeOnly(project(":producer")) {
        capabilities {
            requireCapability("org.gradle.demo:producer-mysql-support")
        }
    }
    runtimeOnly(project(":producer")) {
        capabilities {
            requireCapability("org.gradle.demo:producer-postgres-support")
        }
    }
}
build.gradle
dependencies {
    implementation(project(":producer"))

    // Let's try to ask for both MySQL and Postgres support
    runtimeOnly(project(":producer")) {
        capabilities {
            requireCapability("org.gradle.demo:producer-mysql-support")
        }
    }
    runtimeOnly(project(":producer")) {
        capabilities {
            requireCapability("org.gradle.demo:producer-postgres-support")
        }
    }
}

依赖解析将失败,并显示清晰且信息丰富的错误消息

Cannot choose between
   org.gradle.demo:producer:1.0 variant mysqlSupportRuntimeElements and
   org.gradle.demo:producer:1.0 variant postgresSupportRuntimeElements
   because they provide the same capability: org.gradle.demo:producer-db-support:1.0