功能作为一级概念

组件提供许多功能,这些功能通常与提供这些功能的软件架构无关。例如,一个库可能在一个工件中包含多个功能。但是,这样的库将在单个 GAV(组、工件和版本)坐标下发布。这意味着,在单个坐标下,组件的潜在“功能”可能共存。

使用 Gradle,显式声明组件提供的功能变得很有趣。为此,Gradle 提供了功能的概念。

一个功能通常是通过组合不同的功能来构建的。

在理想情况下,组件不应该声明对显式 GAV 的依赖关系,而应该用功能来表达它们的依赖关系

  • “给我一个提供日志记录功能的组件”

  • “给我一个脚本引擎”

  • “给我一个支持 Groovy 的脚本引擎”

通过对功能进行建模,依赖关系管理引擎可以更智能,并在依赖关系图中存在不兼容的功能时通知你,或者在图中的不同模块提供相同功能时要求你选择。

为外部模块声明功能

值得注意的是,Gradle 支持为构建的组件声明功能,也支持为外部组件声明功能,以防它们没有声明。

例如,如果你的构建文件包含以下依赖关系

build.gradle.kts
dependencies {
    // This dependency will bring log4:log4j transitively
    implementation("org.apache.zookeeper:zookeeper:3.4.9")

    // We use log4j over slf4j
    implementation("org.slf4j:log4j-over-slf4j:1.7.10")
}
build.gradle
dependencies {
    // This dependency will bring log4:log4j transitively
    implementation 'org.apache.zookeeper:zookeeper:3.4.9'

    // We use log4j over slf4j
    implementation 'org.slf4j:log4j-over-slf4j:1.7.10'
}

按现状,很难弄清楚最终会在类路径中获得两个日志框架。实际上,zookeeper 会引入 log4j,而我们想要使用的是 log4j-over-slf4j。我们可以通过添加一条规则来预先检测冲突,该规则将声明这两个日志框架提供相同的功能

build.gradle.kts
dependencies {
    // Activate the "LoggingCapability" rule
    components.all(LoggingCapability::class.java)
}

class LoggingCapability : ComponentMetadataRule {
    val loggingModules = setOf("log4j", "log4j-over-slf4j")

    override
    fun execute(context: ComponentMetadataContext) = context.details.run {
        if (loggingModules.contains(id.name)) {
            allVariants {
                withCapabilities {
                    // Declare that both log4j and log4j-over-slf4j provide the same capability
                    addCapability("log4j", "log4j", id.version)
                }
            }
        }
    }
}
build.gradle
dependencies {
    // Activate the "LoggingCapability" rule
    components.all(LoggingCapability)
}

@CompileStatic
class LoggingCapability implements ComponentMetadataRule {
    final static Set<String> LOGGING_MODULES = ["log4j", "log4j-over-slf4j"] as Set<String>

    void execute(ComponentMetadataContext context) {
        context.details.with {
            if (LOGGING_MODULES.contains(id.name)) {
                allVariants {
                    it.withCapabilities {
                        // Declare that both log4j and log4j-over-slf4j provide the same capability
                        it.addCapability("log4j", "log4j", id.version)
                    }
                }
            }
        }
    }
}

通过添加此规则,我们将确保 Gradle *将* 检测到冲突并适当地失败

> Could not resolve all files for configuration ':compileClasspath'.
   > Could not resolve org.slf4j:log4j-over-slf4j:1.7.10.
     Required by:
         project :
      > Module 'org.slf4j:log4j-over-slf4j' has been rejected:
           Cannot select module with conflict on capability 'log4j:log4j:1.7.10' also provided by [log4j:log4j:1.2.16(compile)]
   > Could not resolve log4j:log4j:1.2.16.
     Required by:
         project : > org.apache.zookeeper:zookeeper:3.4.9
      > Module 'log4j:log4j' has been rejected:
           Cannot select module with conflict on capability 'log4j:log4j:1.2.16' also provided by [org.slf4j:log4j-over-slf4j:1.7.10(compile)]

请参阅 文档中的功能部分,了解如何解决功能冲突。

为本地组件声明附加功能

所有组件都具有与组件相同的 GAV 坐标相对应的 *隐式功能*。但是,也可以为组件声明额外的 *显式功能*。当以不同 GAV 坐标发布的库是同一 API 的 *替代实现* 时,这很方便

build.gradle.kts
configurations {
    apiElements {
        outgoing {
            capability("com.acme:my-library:1.0")
            capability("com.other:module:1.1")
        }
    }
    runtimeElements {
        outgoing {
            capability("com.acme:my-library:1.0")
            capability("com.other:module:1.1")
        }
    }
}
build.gradle
configurations {
    apiElements {
        outgoing {
            capability("com.acme:my-library:1.0")
            capability("com.other:module:1.1")
        }
    }
    runtimeElements {
        outgoing {
            capability("com.acme:my-library:1.0")
            capability("com.other:module:1.1")
        }
    }
}

功能必须附加到 *传出配置*,即组件的 可消费配置

此示例显示我们声明了两个功能

  1. com.acme:my-library:1.0,对应于库的 *隐式功能*

  2. com.other:module:1.1,对应于该库的另一个功能

值得注意的是,我们需要这样做 1. 因为一旦你开始声明 *显式* 功能,那么 *所有* 功能都需要声明,包括 *隐式* 功能。

第二个功能可以特定于此库,也可以对应于外部组件提供的功能。在这种情况下,如果 com.other:module 出现在同一个依赖关系图中,构建将失败,并且消费者 将不得不选择使用哪个模块

功能发布到 Gradle 模块元数据。但是,它们在 POM 或 Ivy 元数据文件中 *没有等效项*。因此,在发布此类组件时,Gradle 会警告你,此功能仅供 Gradle 消费者使用

Maven publication 'maven' contains dependencies that cannot be represented in a published pom file.
  - Declares capability com.acme:my-library:1.0
  - Declares capability com.other:module:1.1