Managing Transitive Dependencies
- Managing versions of transitive dependencies with dependency constraints
- Excluding transitive module dependencies
- Enforcing a particular dependency version
- Disabling resolution of transitive dependencies
- Importing version recommendations from a Maven BOM
- Dependency version alignment
- Component capabilities
Resolution behavior for transitive dependencies can be customized to a high degree to meet enterprise requirements.
Managing versions of transitive dependencies with dependency constraints
Dependency constraints allow you to define the version or the version range of both dependencies declared in the build script and transitive dependencies. It is the preferred method to express constraints that should be applied to all dependencies of a configuration. When Gradle attempts to resolve a dependency to a module version, all dependency declarations with version, all transitive dependencies and all dependency constraints for that module are taken into consideration. The highest version that matches all conditions is selected. If no such version is found, Gradle fails with an error showing the conflicting declarations. If this happens you can adjust your dependencies or dependency constraints declarations, or make other adjustments to the transitive dependencies if needed. Similar to dependency declarations, dependency constraint declarations are scoped by configurations and can therefore be selectively defined for parts of a build. If a dependency constraint influenced the resolution result, any type of dependency resolve rules may still be applied afterwards.
Groovy
Kotlin
dependencies {
implementation 'org.apache.httpcomponents:httpclient'
constraints {
implementation('org.apache.httpcomponents:httpclient:4.5.3') {
because 'previous versions have a bug impacting this application'
}
implementation('commons-codec:commons-codec:1.11') {
because 'version 1.9 pulled from httpclient has bugs affecting this application'
}
}
}
In the example, all versions are omitted from the dependency declaration. Instead, the versions are defined in the constraints block. The version definition for commons-codec:1.11
is only taken into account if commons-codec
is brought in as transitive dependency, since commons-codec
is not defined as dependency in the project. Otherwise, the constraint has no effect.
✨
|
Dependency constraints are not yet published, but that will be added in a future release. This means that their use currently only targets builds that do not publish artifacts to maven or ivy repositories. |
Dependency constraints themselves can also be added transitively.
Excluding transitive module dependencies
Declared dependencies in a build script can pull in a lot of transitive dependencies. You might decide that you do not want a particular transitive dependency as part of the dependency graph for a good reason.
-
The dependency is undesired due to licensing constraints.
-
The dependency is not available in any of the declared repositories.
-
The metadata for the dependency exists but the artifact does not.
-
The metadata provides incorrect coordinates for a transitive dependency.
Transitive dependencies can be excluded on the level of a declared dependency or a configuration. Let’s demonstrate both use cases. In the following two examples the build script declares a dependency on Log4J, a popular logging framework in the Java world. The metadata of the particular version of Log4J also defines transitive dependencies.
Groovy
Kotlin
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'log4j:log4j:1.2.15'
}
If resolved from Maven Central some of the transitive dependencies provide metadata but not the corresponding binary artifact. As a result any task requiring the binary files will fail e.g. a compilation task.
> gradle -q compileJava * What went wrong: Could not resolve all files for configuration ':compileClasspath'. > Could not find jms.jar (javax.jms:jms:1.1). Searched in the following locations: https://repo.maven.apache.org/maven2/javax/jms/jms/1.1/jms-1.1.jar > Could not find jmxtools.jar (com.sun.jdmk:jmxtools:1.2.1). Searched in the following locations: https://repo.maven.apache.org/maven2/com/sun/jdmk/jmxtools/1.2.1/jmxtools-1.2.1.jar > Could not find jmxri.jar (com.sun.jmx:jmxri:1.2.1). Searched in the following locations: https://repo.maven.apache.org/maven2/com/sun/jmx/jmxri/1.2.1/jmxri-1.2.1.jar
The situation can be fixed by adding a repository containing those dependencies. In the given example project, the source code does not actually use any of Log4J’s functionality that require the JMS (e.g. JMSAppender
) or JMX libraries. It’s safe to exclude them from the dependency declaration.
Exclusions need to spelled out as a key/value pair via the attributes group
and/or module
. For more information, refer to ModuleDependency.exclude(java.util.Map).
Groovy
Kotlin
dependencies {
implementation('log4j:log4j:1.2.15') {
exclude group: 'javax.jms', module: 'jms'
exclude group: 'com.sun.jdmk', module: 'jmxtools'
exclude group: 'com.sun.jmx', module: 'jmxri'
}
}
You may find that other dependencies will want to pull in the same transitive dependency that misses the artifacts. Alternatively, you can exclude the transitive dependencies for a particular configuration by calling the method Configuration.exclude(java.util.Map).
Groovy
Kotlin
configurations {
implementation {
exclude group: 'javax.jms', module: 'jms'
exclude group: 'com.sun.jdmk', module: 'jmxtools'
exclude group: 'com.sun.jmx', module: 'jmxri'
}
}
dependencies {
implementation 'log4j:log4j:1.2.15'
}
✨
|
As a build script author you often times know that you want to exclude a dependency for all configurations available in the project. You can use the method DomainObjectCollection.all(org.gradle.api.Action) to define a global rule. |
You might encounter other use cases that don’t quite fit the bill of an exclude rule. For example you want to automatically select a version for a dependency with a specific requested version or you want to select a different group for a requested dependency to react to a relocation. Those use cases are better solved by the ResolutionStrategy API. Some of these use cases are covered in Customizing Dependency Resolution Behavior.
Enforcing a particular dependency version
Gradle resolves any dependency version conflicts by selecting the latest version found in the dependency graph. Some projects might need to divert from the default behavior and enforce an earlier version of a dependency e.g. if the source code of the project depends on an older API of a dependency than some of the external libraries.
✨
|
Enforcing a version of a dependency requires a conscious decision. Changing the version of a transitive dependency might lead to runtime errors if external libraries do not properly function without them. Consider upgrading your source code to use a newer version of the library as an alternative approach. |
Let’s say a project uses the HttpClient library for performing HTTP calls. HttpClient pulls in Commons Codec as transitive dependency with version 1.10. However, the production source code of the project requires an API from Commons Codec 1.9 which is not available in 1.10 anymore. A dependency version can be enforced by declaring it in the build script and setting ExternalDependency.setForce(boolean) to true
.
Groovy
Kotlin
dependencies {
implementation 'org.apache.httpcomponents:httpclient:4.5.4'
implementation('commons-codec:commons-codec:1.9') {
force = true
}
}
If the project requires a specific version of a dependency on a configuration-level then it can be achieved by calling the method ResolutionStrategy.force(java.lang.Object[]).
Groovy
Kotlin
configurations {
compileClasspath {
resolutionStrategy.force 'commons-codec:commons-codec:1.9'
}
}
dependencies {
implementation 'org.apache.httpcomponents:httpclient:4.5.4'
}
Disabling resolution of transitive dependencies
By default Gradle resolves all transitive dependencies specified by the dependency metadata. Sometimes this behavior may not be desirable e.g. if the metadata is incorrect or defines a large graph of transitive dependencies. You can tell Gradle to disable transitive dependency management for a dependency by setting ModuleDependency.setTransitive(boolean) to false
. As a result only the main artifact will be resolved for the declared dependency.
Groovy
Kotlin
dependencies {
implementation('com.google.guava:guava:23.0') {
transitive = false
}
}
✨
|
Disabling transitive dependency resolution will likely require you to declare the necessary runtime dependencies in your build script which otherwise would have been resolved automatically. Not doing so might lead to runtime classpath issues. |
A project can decide to disable transitive dependency resolution completely. You either don’t want to rely on the metadata published to the consumed repositories or you want to gain full control over the dependencies in your graph. For more information, see Configuration.setTransitive(boolean).
Groovy
Kotlin
configurations.all {
transitive = false
}
dependencies {
implementation 'com.google.guava:guava:23.0'
}
Importing version recommendations from a Maven BOM
Gradle provides support for importing bill of materials (BOM) files, which are effectively .pom
files that use <dependencyManagement>
to control the dependency versions of direct and transitive dependencies. The BOM support in Gradle works similar to using <scope>import</scope>
when depending on a BOM in Maven. In Gradle however, it is done via a regular dependency declaration on the BOM:
Groovy
Kotlin
dependencies {
// import a BOM
implementation platform('org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE')
// define dependencies without versions
implementation 'com.google.code.gson:gson'
implementation 'dom4j:dom4j'
}
In the example, the versions of gson
and dom4j
are provided by the Spring Boot BOM. This way, if you are developing for a platform like Spring Boot, you do not have to declare any versions yourself but can rely on the versions the platform provides.
Gradle treats all entries in the <dependencyManagement>
block of a BOM similar to Gradle’s dependency constraints. This means that any version defined in the <dependencyManagement>
block can impact the dependency resolution result. In order to qualify as a BOM, a .pom
file needs to have <packaging>pom</packaging>
set.
However often BOMs are not only providing versions as recommendations, but also a way to override any other version found in the graph. You can enable this behavior by using the enforcedPlatform
keyword, instead of platform
, when importing the BOM:
Groovy
Kotlin
dependencies {
// import a BOM. The versions used in this file will override any other version found in the graph
implementation enforcedPlatform('org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE')
// define dependencies without versions
implementation 'com.google.code.gson:gson'
implementation 'dom4j:dom4j'
// this version will be overridden by the one found in the BOM
implementation 'org.codehaus.groovy:groovy:1.8.6'
}
Dependency version alignment
Dependency version alignment allows different modules belonging to the same logical group (a platform) to have identical versions in a dependency graph.
Handling inconsistent module versions
Gradle supports aligning versions of modules which belong to the same "platform".
It is often preferable, for example, that the API and implementation modules of a component are using the same version.
However, because of the game of transitive dependency resolution, it is possible that different modules belonging to the same platform end up using different versions.
For example, your project may depend on the jackson-databind
and vert.x
libraries, as illustrated below:
Groovy
Kotlin
dependencies {
// a dependency on Jackson Databind
implementation 'com.fasterxml.jackson.core:jackson-databind:2.8.9'
// and a dependency on vert.x
implementation 'io.vertx:vertx-core:3.5.3'
}
Because vert.x
depends on jackson-core
, we would actually resolve the following dependency versions:
-
jackson-core
version2.9.5
(brought byvertx-core
) -
jackson-databind
version2.9.5
(by conflict resolution) -
jackson-annotation
version2.9.0
(dependency ofjackson-databind:2.9.5
)
It’s easy to end up with a set of versions which do not work well together. To fix this, Gradle supports dependency version alignment, which is supported by the concept of platform. A platform represents a set of modules which "work well together". Either because they are actually published as a whole (when one of the members of the platform is published, all other modules are also published with the same version), or because someone tested modules and indicates that they work well together (typically, the Spring Platform).
Aligning versions natively with Gradle
Gradle natively supports alignment of modules produced by Gradle. This is a direct consequence of the transitivity of dependency constraints. So if you have a multi-project build, and that you wish that consumers get the same version of all your modules, Gradle provides a simple way to do this using the Java Platform Plugin.
For example, if you have a project that consists of 3 modules:
-
lib
-
utils
-
core
, depending onlib
andutils
And a consumer that declares the following dependencies:
-
core
version 1.0 -
lib
version 1.1
then by default resolution would select core:1.0
and lib:1.1
, because lib
has no dependency on core
.
We can fix this by adding a new module in our project, a platform, that will add constraints on all the modules of your project:
Groovy
Kotlin
plugins {
id 'java-platform'
}
dependencies {
// The platform declares constraints on all components that
// require alignment
constraints {
api(project(":core"))
api(project(":lib"))
api(project(":utils"))
}
}
Once this is done, we need to make sure that all modules now depend on the platform, like this:
Groovy
Kotlin
dependencies {
// Each project has a dependency on the platform
api(platform(project(":platform")))
// And any additional dependency required
implementation(project(":lib"))
implementation(project(":utils"))
}
It is important that the platform contains a constraint on all the components, but also that each component has a dependency on the platform. By doing this, whenever Gradle will add a dependency to a module of the platform on the graph, it will also include constraints on the other modules of the platform. This means that if we see another module belonging to the same platform, we will automatically upgrade to the same version.
In our example, it means that we first see core:1.0
, which brings a platform 1.0
with constraints on lib:1.0
and lib:1.0
.
Then we add lib:1.1
which has a dependency on platform:1.1
.
By conflict resolution, we select the 1.1
platform, which has a constraint on core:1.1
.
Then we conflict resolve between core:1.0
and core:1.1
, which means that core
and lib
are now aligned properly.
✨
|
This behavior is enforced for published components only if you use Gradle Module Metadata. |
Aligning versions of modules not published with Gradle
Whenever the publisher doesn’t use Gradle, like in our Jackson example, we can explain to Gradle that that all Jackson modules "belong to" the same platform and benefit from the same behavior as with native alignment:
Groovy
Kotlin
class JacksonAlignmentRule implements ComponentMetadataRule {
void execute(ComponentMetadataContext ctx) {
ctx.details.with {
if (id.group.startsWith("com.fasterxml.jackson")) {
// declare that Jackson modules all belong to the Jackson virtual platform
belongsTo("com.fasterxml.jackson:jackson-platform:${id.version}")
}
}
}
}
By using the belongsTo
keyword, we declare that all modules belong to the same virtual platform, which is treated specially by the engine, in particular with regards to alignment. We can use the rule we just created by registering it:
Groovy
Kotlin
dependencies {
components.all(JacksonAlignmentRule)
}
Then all versions in the example above would align to 2.9.5
. However, Gradle would let you override that choice by specifying a dependency on the Jackson platform:
Groovy
Kotlin
dependencies {
// Forcefully downgrade the Jackson platform to 2.8.9
implementation enforcedPlatform('com.fasterxml.jackson:jackson-platform:2.8.9')
}
Virtual vs published platforms
A platform defined by a component metadata rule for which the belongsTo
target module isn’t published on a repository is called a virtual platform.
A virtual platform is considered specially by the engine and participates in dependency resolution like a published module, but triggers dependency version alignment.
On the other hand, we can find "real" platforms published on public repositories. Typical examples include BOMs, like the Spring BOM. They differ in the sense that a published platform may refer to modules which are effectively different things.
For example the Spring BOM declares dependencies on Spring as well as Apache Groovy. Obviously those things are versioned differently, so it doesn’t make sense to align in this case. In other words, if a platform is published, Gradle trusts its metadata, and will not try to align dependency versions of this platform.
Component capabilities
Introduction to capabilities
Often a dependency graph would accidentally contain multiple implementations of the same API. This is particularly common with logging frameworks, where multiple bindings are available, and that one library chooses a binding when another transitive dependency chooses another. Because those implementations live at different GAV coordinates, the build tool has usually no way to find out that there’s a conflict between those libraries. To solve this, Gradle provides the concept of capability.
It’s illegal to find two components providing the same capability in a single dependency graph. Intuitively, it means that if Gradle finds two components that provide the same thing on classpath, it’s going to fail with an error indicating what modules are in conflict. In our example, it means that different bindings of a logging framework provide the same capability.
Capability coordinates
A capability is defined by a (group, module, version)
triplet.
Each component defines an implicit capability corresponding to its GAV coordinates (group, artifact, version).
For example, the org.apache.commons:commons-lang3:3.8
module has an implicit capability with group org.apache.commons
, name commons-lang3
and version 3.8
.
It is important to realize that capabilities are versioned.
Declaring component capabilities
✨
|
Capabilities are a core feature of the experimental Gradle metadata file format. This means that components published with the experimental Gradle metadata file format can declare capabilities, but also that this feature is only natively understood by Gradle. However, it’s possible to declare capabilities on components which were not built by Gradle, as explained in this section. |
If your build file contains the following dependencies:
Groovy
Kotlin
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 is, it’s pretty hard to figure out that you will end up with two logging frameworks on the classpath.
In fact, zookeeper
will bring in log4j
, where what we want to use is log4j-over-slf4j
.
We can preemptively detect the conflict by adding a rule which will declare that both logging frameworks provide the same capability:
Groovy
Kotlin
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("org.slf4j", "slf4j-capability", "1.0")
}
}
}
}
}
}
By adding this rule, we will make sure that Gradle will detect conflicts and properly fail:
Cannot choose between log4j:log4j:1.2.16 and org.slf4j:log4j-over-slf4j:1.7.10 because they provide the same capability: org.slf4j:slf4j-capability:1.0
It does not, however, choose what component to use for you: detecting a conflict is the first step, then you have to fix it.
Solving capability conflicts
By default, Gradle will automatically choose the component with the highest version of a capability.
This can be useful whenever a component is relocated at different coordinates in a new release.
For example, the ASM library lived at asm:asm
coordinates until version 3.3.1
, then changed to org.ow2.asm:asm
since 4.0
.
It is illegal to have both ASM ⇐3.3.1 and 4.0+ on the classpath, because they provide the same feature, it’s just that the component has been relocated.
Because each component has an implicit capability corresponding to its GAV coordinates, we can fix this by having a rule that will declare that the asm:asm
module provides the org.ow2.asm:asm
capability:
Groovy
Kotlin
@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)
}
}
}
}
}
}
However, fixing by automatic capability version conflict resolution is not always suitable. In our logging example, it doesn’t matter what version of the logging frameworks we use, we should always select the slf4j bridge.
In this case, we can fix it by using dependency substitution:
Groovy
Kotlin
configurations.compileClasspath.resolutionStrategy.dependencySubstitution {
substitute(module("log4j:log4j"))
.because("Prefer SLF4J for logging")
.with(module("org.slf4j:log4j-over-slf4j:1.7.10"))
}