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

为什么要使用特性变体?

特性变体相比传统依赖管理具有以下优势:

  • 改进的模块化:清晰定义不同库功能之间的界限。

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

  • 支持多个变体:单个库可以公开针对不同用例定制的变体(例如,debugrelease 构建)。

常见用例包括:

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

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

  • 支持每个都有独特依赖项的可选运行时特性。

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

  • 启用可以与主库构件可选包含的附加构件。

步骤 1:通过功能选择特性

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

每个变体都提供一组功能,也由 GAV 坐标标识,但最好理解为特性描述,例如:

  • "我提供 SLF4J 绑定"

  • "我提供 MySQL 运行时支持"

  • "我提供 Groovy 运行时"

需要注意的是:

  • 默认情况下,每个变体都提供与组件 GAV 坐标匹配的功能

  • 提供相同功能的两个变体不能在依赖图中同时存在。

  • 如果单个组件的多个变体提供不同的功能,它们可以同时存在。

例如,一个典型的 Java 库具有 API 和运行时变体,两者都提供相同的功能。因此,将这两种变体同时包含在依赖图中是错误的。然而,同时使用运行时和测试夹具运行时变体是允许的,只要这些变体声明了不同的功能。

为此,消费者必须明确声明单独的依赖项:

  • 主库的一个依赖项("主"特性)。

  • 明确需要附加特性(例如测试夹具)功能的另一个依赖项。

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 库插件设置配置的方式非常相似。

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

  • featureNameApi 用于特性 API 依赖。

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

  • featureNameRuntimeOnly 用于仅运行时依赖。

  • featureCompileOnly 用于仅编译依赖。

在该示例中,名为 "mongodbSupport" 的特性自动创建了这些配置:

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

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

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

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

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

  • mongodbSupportRuntimeElements - 供消费者获取此特性的构件和 API 依赖

  • mongodbSupportApiElements - 供消费者获取此特性的构件和运行时依赖

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

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

注册特性时请勿使用源集。此行为将在 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 将特性变体的功能映射为与主组件相同的组和版本,而功能的名称由主组件名称后跟 - 和特性名称的 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 库插件会自动为您注册额外的变体,除了标准发布设置外无需额外配置:

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),特性表示为可选依赖项,并且特性构件以不同的分类器发布。

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

添加 Javadoc 和源 JAR

主 Javadoc 和源 JAR 类似,您可以配置特性变体以生成它们自己的 Javadoc 和源 JAR:

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

此设置自动将 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:处理互斥变体

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

需要记住的关键规则是:

依赖图中的任何两个变体都不能提供相同的功能。

此规则允许 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