Capabilities
In a dependency graph, it’s common for multiple implementations of the same API to be accidentally included, especially with libraries like logging frameworks where different bindings are selected by various transitive dependencies.
Since these implementations typically reside at different group, artifact, and version (GAV) coordinates, build tools often can’t detect the conflict.
To address this, Gradle introduces the concept of capability.
Understanding capabilities
A capability is essentially a way to declare that different components (dependencies) offer the same functionality.
It’s illegal for Gradle to include more than one component providing the same capability in a single dependency graph. If Gradle detects two components providing the same capability (e.g., different bindings for a logging framework), it will fail the build with an error, indicating the conflicting modules. This ensures that conflicting implementations are resolved, avoiding issues on the classpath.
For instance, suppose you have dependencies on two different libraries for database connection pooling:
dependencies {
implementation("com.zaxxer:HikariCP:4.0.3") // A popular connection pool
implementation("org.apache.commons:commons-dbcp2:2.8.0") // Another connection pool
}
configurations.all {
resolutionStrategy.capabilitiesResolution.withCapability("database:connection-pool") {
select("com.zaxxer:HikariCP")
}
}
In this case, both HikariCP
and commons-dbcp2
provide the same functionality (connection pooling).
Gradle will fail if both are on the classpath.
Since only one should be used, Gradle’s resolution strategy allows you to select HikariCP
, resolving the conflict.
Understanding capability coordinates
A capability is identified by a (group, module, version)
triplet.
Every component defines an implicit capability based on its GAV coordinates: group, artifact, and version.
For instance, the org.apache.commons:commons-lang3:3.8
module has an implicit capability with the group org.apache.commons
, name commons-lang3
, and version 3.8
:
dependencies {
implementation("org.apache.commons:commons-lang3:3.8")
}
It’s important to note that capabilities are versioned.
Declaring component capabilities
To detect conflicts early, it’s useful to declare component capabilities through rules, allowing conflicts to be caught during the build instead of at runtime.
One common scenario is when a component is relocated to different coordinates in a newer release.
For example, the ASM library was published under asm:asm
until version 3.3.1
, and then relocated to org.ow2.asm:asm
starting with version 4.0.
Including both versions on the classpath is illegal because they provide the same feature, under different coordinates.
Since each component has an implicit capability based on its GAV coordinates, we can address this conflict by using a rule that declares the asm:asm
module as providing the org.ow2.asm:asm
capability:
class AsmCapability : ComponentMetadataRule {
override
fun execute(context: ComponentMetadataContext) = context.details.run {
if (id.group == "asm" && id.name == "asm") {
allVariants {
withCapabilities {
// Declare that ASM provides the org.ow2.asm:asm capability, but with an older version
addCapability("org.ow2.asm", "asm", id.version)
}
}
}
}
}
@CompileStatic
class AsmCapability implements ComponentMetadataRule {
void execute(ComponentMetadataContext context) {
context.details.with {
if (id.group == "asm" && id.name == "asm") {
allVariants {
it.withCapabilities {
// Declare that ASM provides the org.ow2.asm:asm capability, but with an older version
it.addCapability("org.ow2.asm", "asm", id.version)
}
}
}
}
}
}
With this rule in place, the build will fail if both asm:asm
( < = 3.3.1
) and org.ow2.asm:asm
(4.0+
) are present in the dependency graph.
Gradle won’t resolve the conflict automatically, but this helps you realize that the problem exists. It’s recommended to package such rules into plugins for use in builds, allowing users to decide which version to use or to fix the classpath conflict. |
Declaring capabilities for external modules
Gradle allows you to declare capabilities not only for components you build but also for external components that don’t define them.
For example, consider the following dependencies in your build file:
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")
}
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'
}
As it stands, it’s not obvious that this setup results in two logging frameworks on the classpath.
Specifically, zookeeper
brings in log4j
, but we want to use log4j-over-slf4j
.
To proactively detect this conflict, we can define a rule stating that both frameworks provide the same capability:
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)
}
}
}
}
}
dependencies {
// Activate the "LoggingCapability" rule
components.all(LoggingCapability)
}
@CompileStatic
class LoggingCapability implements ComponentMetadataRule {
final static Set LOGGING_MODULES = ["log4j", "log4j-over-slf4j"] as Set
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)
}
}
}
}
}
}
This ensures that Gradle detects the conflict and fails with a clear error message:
> 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)]
Declaring capabilities for a local component
Every component has an implicit capability matching its GAV coordinates. H owever, you can also declare additional explicit capabilities, which is useful when a library published under different GAV coordinates serves as an alternate implementation of the same API:
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")
}
}
}
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")
}
}
}
Capabilities must be attached to outgoing configurations, which are consumable configurations of a component.
In this example, we declare two capabilities:
-
com.acme:my-library:1.0
- the implicit capability of the library. -
com.other:module:1.1
- an additional capability assigned to this library.
It’s important to declare the implicit capability explicitly because once you define any explicit capability, all capabilities must be declared, including the implicit one.
The second capability can either be specific to this library or match a capability provided by an external component.
If com.other:module
appears elsewhere in the dependency graph, the build will fail, and consumers must choose which module to use.
Capabilities are published in Gradle Module Metadata but have no equivalent in POM or Ivy metadata files. As a result, when publishing such a component, Gradle warns that this feature is only supported for Gradle consumers:
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
Selecting between candidates
At some point, a dependency graph is going to include either incompatible modules, or modules which are mutually exclusive.
For example, you may have different logger implementations, and you need to choose one binding. Capabilities help understand the conflict, then Gradle provides you with tools to solve the conflicts.
Selecting between different capability candidates
In the relocation example above, Gradle was able to tell you that you have two versions of the same API on classpath: an "old" module and a "relocated" one. We can solve the conflict by automatically choosing the component which has the highest capability version:
configurations.all {
resolutionStrategy.capabilitiesResolution.withCapability("org.ow2.asm:asm") {
selectHighestVersion()
}
}
configurations.all {
resolutionStrategy.capabilitiesResolution.withCapability('org.ow2.asm:asm') {
selectHighestVersion()
}
}
However, choosing the highest capability version conflict resolution is not always suitable.
For a logging framework, for example, it doesn’t matter what version of the logging frameworks we use.
In this case, we explicitly select slf4j
as the preferred option:
configurations.all {
resolutionStrategy.capabilitiesResolution.withCapability("log4j:log4j") {
val toBeSelected = candidates.firstOrNull { it.id.let { id -> id is ModuleComponentIdentifier && id.module == "log4j-over-slf4j" } }
if (toBeSelected != null) {
select(toBeSelected)
}
because("use slf4j in place of log4j")
}
}
configurations.all {
resolutionStrategy.capabilitiesResolution.withCapability("log4j:log4j") {
def toBeSelected = candidates.find { it.id instanceof ModuleComponentIdentifier && it.id.module == 'log4j-over-slf4j' }
if (toBeSelected != null) {
select(toBeSelected)
}
because 'use slf4j in place of log4j'
}
}
This approach works also well if you have multiple slf4j
bindings on the classpath; bindings are basically different logger implementations, and you need only one.
However, the selected implementation may depend on the configuration being resolved.
For instance, in testing environments, the lightweight slf4j-simple
logging implementation might be sufficient, while in production, a more robust solution like logback
may be preferable.
Resolution can only be made in favor of a module that is found in the dependency graph.
The select
method accepts only a module from the current set of candidates.
If the desired module is not part of the conflict, you can choose not to resolve that particular conflict, effectively leaving it unresolved.
Another conflict in the graph may have the module you want to select.
If no resolution is provided for all conflicts on a given capability, the build will fail because the module chosen for resolution was not found in the graph.
Additionally, calling select(null)
will result in an error and should be avoided.
For more information, refer to the capabilities resolution API.